Перейти к основному содержанию
Перейти к основному содержанию

Оптимизация производительности: стратегии, проверенные сообществом

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

Располагаете столбцы по кардинальности (от низкой к высокой)

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

См. дополнительную документацию по кардинальности и первичным индексам:

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

При использовании меток времени в предложении ORDER BY учитывайте компромисс между кардинальностью и точностью. Метки времени с микросекундной точностью создают очень высокую кардинальность (почти одно уникальное значение на каждую строку), что снижает эффективность разреженного первичного индекса ClickHouse. Округлённые метки времени создают меньшую кардинальность, что позволяет эффективнее пропускать данные при чтении за счёт индекса, но при этом вы теряете точность для временных запросов.

-- Задание: Попробуйте различные функции времени, например toStartOfMinute или toStartOfWeek
-- Эксперимент: Сравните различия в кардинальности на ваших собственных данных временных меток
SELECT 
    'Точность до микросекунд' as granularity,
    uniq(created_at) as unique_values,
    'Создает огромную кардинальность - неэффективно для ключа сортировки' as impact
FROM github.github_events
WHERE created_at >= '2024-01-01'
UNION ALL
SELECT 
    'Точность до часа',
    uniq(toStartOfHour(created_at)),
    'Гораздо лучше для ключа сортировки - обеспечивает пропуск индексов'
FROM github.github_events
WHERE created_at >= '2024-01-01'
UNION ALL  
SELECT 
    'Точность до дня',
    uniq(toStartOfDay(created_at)),
    'Оптимально для отчетных запросов'
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 раз ниже (и, что ещё важнее, ограничено). Наши крупнейшие клиенты больше не сталкиваются с ошибками при поиске реплеев, и теперь мы можем поддерживать клиентов любого размера, не исчерпывая память».

Видеоматериалы

Читать далее: