Перейти к основному содержанию
Это руководство входит в подборку выводов и рекомендаций, собранных на встречах сообщества. Больше практических решений и полезных наблюдений можно найти в подборке по конкретным проблемам. Возникли сложности с Materialized Views? Ознакомьтесь с руководством сообщества Materialized Views. Если у вас медленные запросы и вам нужно больше примеров, у нас также есть руководство Оптимизация запросов.

Упорядочивайте по мощности (от меньшей к большей)

Первичный индекс ClickHouse работает лучше всего, когда сначала идут столбцы с низкой мощностью: это позволяет эффективно пропускать большие фрагменты данных. Столбцы с высокой мощностью, расположенные дальше в ключе, обеспечивают более точную сортировку внутри этих фрагментов. Начинайте со столбцов с небольшим числом уникальных значений (например, status, category, country) и заканчивайте столбцами с большим числом уникальных значений (например, user_id, timestamp, session_id). Дополнительную информацию о мощности и первичных индексах см. в документации:

Важна гранулярность времени

При использовании временных меток в предложении ORDER BY учитывайте компромисс между мощностью и точностью. Временные метки с точностью до микросекунд создают очень высокую мощность (почти по одному уникальному значению на строку), что снижает эффективность разреженного первичного индекса ClickHouse. Округлённые временные метки создают меньшую мощность, благодаря чему индекс может эффективнее пропускать данные, но при этом вы теряете точность при выполнении запросов по времени.
runnable editable
-- Задание: Попробуйте другие временные функции, например toStartOfMinute или toStartOfWeek
-- Эксперимент: Сравните различия в мощности на своих данных с временными метками
SELECT 
    'Microsecond precision' as granularity,
    uniq(created_at) as unique_values,
    'Creates massive cardinality - bad for sort key' as impact
FROM github.github_events
WHERE created_at >= '2024-01-01'
UNION ALL
SELECT 
    'Hour precision',
    uniq(toStartOfHour(created_at)),
    'Much better for sort key - enables skip indexing'
FROM github.github_events
WHERE created_at >= '2024-01-01'
UNION ALL  
SELECT 
    'Day precision',
    uniq(toStartOfDay(created_at)),
    'Best for reporting queries'
FROM github.github_events
WHERE created_at >= '2024-01-01';

Сосредоточьтесь на отдельных запросах, а не на средних значениях

При отладке производительности ClickHouse не полагайтесь на среднее время выполнения запросов или общие системные метрики. Вместо этого выясняйте, почему медленно выполняются конкретные запросы. Система может в среднем работать хорошо, даже если отдельные запросы упираются в нехватку памяти, неэффективную фильтрацию или операции над множествами высокой мощности. По словам Алексея, CTO ClickHouse: “Правильный подход — спросить себя, почему именно этот запрос выполнялся пять секунд… Мне не важно, что медиана и другие запросы выполняются быстро. Меня волнует только мой запрос” Когда запрос выполняется медленно, не ограничивайтесь средними значениями. Спросите: “Почему ЭТОТ конкретный запрос был медленным?” — и изучите фактическое потребление ресурсов.

Память и сканирование строк

Sentry — это платформа для отслеживания ошибок, ориентированная на разработчиков и ежедневно обрабатывающая миллиарды событий от более чем 4 миллионов разработчиков. Их ключевое наблюдение: “Именно мощность ключа группировки в этой конкретной ситуации будет определять потребление памяти” — агрегации с высокой мощностью ухудшают производительность из-за исчерпания памяти, а не из-за сканирования строк. Когда запросы завершаются с ошибкой, определите, связана ли проблема с памятью (слишком много групп) или со сканированием (слишком много строк). Запрос вида GROUP BY user_id, error_message, url_path создает отдельное состояние в памяти для каждой уникальной комбинации всех трех значений. При большом числе пользователей, типов ошибок и URL-путей можно легко получить миллионы состояний агрегации, которые должны одновременно храниться в памяти. В крайних случаях Sentry использует детерминированное сэмплирование. Выборка в 10% снижает использование памяти на 90%, сохраняя при этом примерно 5% точности для большинства агрегаций:
WHERE cityHash64(user_id) % 10 = 0  -- Всегда одни и те же 10% пользователей
Это гарантирует, что в каждом запросе будут появляться одни и те же пользователи, обеспечивая согласованные результаты в разные периоды времени. Ключевая идея: cityHash64() выдает одинаковые хеш-значения для одного и того же входного значения, поэтому user_id = 12345 всегда будет хешироваться в одно и то же значение. Это гарантирует, что этот пользователь либо всегда попадает в вашу 10%-ную выборку, либо не попадает в нее никогда — без «мерцания» между запросами.

Оптимизация битовых масок в Sentry

При агрегации по столбцам с высокой мощностью (например, URL) каждое уникальное значение создаёт в памяти отдельное состояние агрегации, что может привести к её исчерпанию. Решение Sentry: вместо группировки по самим строкам URL использовать группировку по булевым выражениям, которые сворачиваются в битовые маски. Вот запрос, который можно попробовать на ваших таблицах, если у вас возникает такая ситуация:
-- Шаблон агрегации с экономным использованием памяти: каждое условие = одно целое число на группу
-- Ключевая идея: sumIf() ограничивает потребление памяти независимо от объёма данных
-- Память на группу: N целых чисел (N * 8 байт), где N = количество условий

SELECT 
    your_grouping_column,
    
    -- Каждый sumIf создаёт ровно один целочисленный счётчик на группу
    -- Объём памяти остаётся постоянным вне зависимости от того, сколько строк соответствует каждому условию
    sumIf(1, your_condition_1) as condition_1_count,
    sumIf(1, your_condition_2) as condition_2_count,
    sumIf(1, your_text_column LIKE '%pattern%') as pattern_matches,
    sumIf(1, your_numeric_column > threshold_value) as above_threshold,
    
    -- Сложные агрегации с несколькими условиями также используют постоянный объём памяти
    sumIf(1, your_condition_1 AND your_text_column LIKE '%pattern%') as complex_condition_count,
    
    -- Стандартные агрегации для справки
    count() as total_rows,
    avg(your_numeric_column) as average_value,
    max(your_timestamp_column) as latest_timestamp
    
FROM your_schema.your_table
WHERE your_timestamp_column >= 'start_date' 
  AND your_timestamp_column < 'end_date'
GROUP BY your_grouping_column
HAVING condition_1_count > minimum_threshold 
   OR condition_2_count > another_threshold
ORDER BY (condition_1_count + condition_2_count + pattern_matches) DESC
LIMIT 20
Вместо того чтобы хранить в памяти каждую уникальную строку, вы сохраняете в виде целых чисел ответы на вопросы об этих строках. В результате состояние агрегации остаётся строго ограниченным и очень компактным независимо от разнообразия данных. Из инженерной команды Sentry: “Эти тяжёлые запросы выполняются более чем в 10 раз быстрее, а использование памяти у нас в 100 раз ниже (и, что ещё важнее, остаётся ограниченным). Наши крупнейшие клиенты больше не сталкиваются с ошибками при поиске replay, и теперь мы можем поддерживать клиентов любого масштаба, не упираясь в ограничения памяти.”

Источники видео

Читайте далее:
Последнее изменение 10 июня 2026 г.