Перейти к основному содержанию
ClickHouse разработан для скорости. Он выполняет запросы с высокой степенью параллелизма, используя все доступные ядра процессора, распределяя данные по линиям обработки и нередко доводя оборудование почти до предела возможностей. В этом руководстве пошагово объясняется, как в ClickHouse работает параллелизм запросов и как его можно настраивать и отслеживать, чтобы повысить производительность при высоких нагрузках. Чтобы проиллюстрировать ключевые концепции, мы используем агрегационный запрос к набору данных uk_price_paid_simple.

Пошагово: как ClickHouse распараллеливает агрегационный запрос

Когда ClickHouse ① выполняет агрегационный запрос с фильтром по первичному ключу таблицы, он ② загружает первичный индекс в память, чтобы ③ определить, какие гранулы нужно обработать, а какие можно безопасно пропустить:

Распределение работы между линиями обработки

Затем выбранные данные динамически распределяются между n параллельными линиями обработки, которые передают и обрабатывают данные блок за блоком, формируя итоговый результат:

Количество n параллельных линий обработки задаётся настройкой max_threads, которая по умолчанию соответствует числу ядер (потоков) одного CPU, доступных ClickHouse на сервере. В примере выше мы предполагаем 4 ядра. На машине с 8 ядрами пропускная способность выполнения запроса примерно удвоилась бы (но использование памяти также соответственно увеличилось бы), поскольку больше линий обработки обрабатывают данные параллельно:

Эффективное распределение данных по линиям обработки — ключевой фактор для максимального использования CPU и сокращения общего времени выполнения запроса.

Обработка запросов к таблицам, разбитым на сегменты

Когда данные таблицы распределены по нескольким серверам в виде сегментов, каждый сервер параллельно обрабатывает свой сегмент. На каждом сервере локальные данные обрабатываются с использованием параллельных линий обработки, как описано выше:

Сервер, который первым получает запрос, собирает все промежуточные результаты с сегментов и объединяет их в итоговый глобальный результат. Распределение нагрузки запросов по сегментам позволяет горизонтально масштабировать параллелизм, особенно в средах с высокой пропускной способностью.
ClickHouse Cloud использует параллельные реплики вместо сегментовВ ClickHouse Cloud такой же уровень параллелизма достигается с помощью параллельных реплик, которые работают аналогично сегментам в кластерах с архитектурой shared-nothing. Каждая реплика ClickHouse Cloud — не имеющий состояния вычислительный узел — параллельно обрабатывает часть данных и вносит свой вклад в итоговый результат, подобно независимому сегменту.

Мониторинг параллелизма запросов

Используйте эти инструменты, чтобы проверить, что ваш запрос полностью задействует доступные ресурсы процессора, и диагностировать случаи, когда этого не происходит. Мы выполняем это на тестовом сервере с 59 ядрами процессора, что позволяет ClickHouse в полной мере продемонстрировать свой параллелизм запросов. Чтобы увидеть, как выполняется пример запроса, мы можем указать серверу ClickHouse возвращать все записи журнала уровня trace во время агрегационного запроса. Для этой демонстрации мы убрали условие фильтрации из запроса — в противном случае было бы обработано только 3 гранулы, а этого недостаточно, чтобы ClickHouse мог задействовать больше нескольких параллельных линий обработки:
SELECT
   max(price)
FROM
   uk.uk_price_paid_simple
SETTINGS send_logs_level='trace';
① <Debug> ...: 3609 marks to read from 3 ranges
② <Trace> ...: Spreading mark ranges among streams
② <Debug> ...: Reading approx. 29564928 rows with 59 streams
Мы видим, что
  • ① ClickHouse должен прочитать 3 609 гранул (обозначенных как marks в трассировочных логах) в 3 диапазонах данных.
  • ② При наличии 59 ядер CPU он распределяет эту работу между 59 параллельными потоками обработки — по одному на каждую линию.
В качестве альтернативы можно использовать оператор EXPLAIN, чтобы изучить физический план операторов, также известный как “конвейер запроса”, для запроса с агрегацией:
EXPLAIN PIPELINE
SELECT
   max(price)
FROM
   uk.uk_price_paid_simple;
    ┌─explain───────────────────────────────────────────────────────────────────────────┐
 1. │ (Expression)                                                                      │
 2. │ ExpressionTransform × 59                                                          │
 3. │   (Aggregating)                                                                   │
 4. │   Resize 59 → 59                                                                  │
 5. │     AggregatingTransform × 59                                                     │
 6. │       StrictResize 59 → 59                                                        │
 7. │         (Expression)                                                              │
 8. │         ExpressionTransform × 59                                                  │
 9. │           (ReadFromMergeTree)                                                     │
10. │           MergeTreeSelect(pool: PrefetchedReadPool, algorithm: Thread) × 59 0 → 1 │
    └───────────────────────────────────────────────────────────────────────────────────┘
Примечание: Читайте план операторов выше снизу вверх. Каждая строка соответствует этапу физического плана выполнения: внизу — чтение данных из хранилища, вверху — финальные этапы обработки. Операторы с пометкой × 59 выполняются параллельно в непересекающихся областях данных по 59 параллельным линиям обработки. Это соответствует значению max_threads и показывает, как каждый этап запроса распараллеливается по ядрам CPU. Встроенный веб-интерфейс ClickHouse (доступный по конечной точке /play) может отображать приведённый выше физический план в виде графической визуализации. В этом примере мы устанавливаем max_threads в 4, чтобы визуализация оставалась компактной и показывала только 4 параллельные линии обработки: Примечание: Читайте визуализацию слева направо. Каждая строка соответствует параллельной линии обработки, которая передаёт данные блок за блоком, применяя такие преобразования, как фильтрация, агрегация и финальные этапы обработки. В этом примере показаны четыре параллельные линии, соответствующие настройке max_threads = 4.

Балансировка нагрузки между линиями обработки

Обратите внимание, что операторы Resize в приведённом выше физическом плане переразбивают и перераспределяют потоки блоков данных между линиями обработки, чтобы равномерно распределять нагрузку между ними. Такое перераспределение особенно важно, когда диапазоны данных различаются по числу строк, соответствующих предикатам запроса; иначе одни линии могут оказаться перегруженными, а другие — бездействовать. Благодаря такому перераспределению более быстрые линии фактически помогают более медленным, что оптимизирует общее время выполнения запроса.

Почему max_threads не всегда применяется

Как упоминалось выше, число n параллельных линий обработки задаётся настройкой max_threads, которая по умолчанию соответствует количеству ядер CPU, доступных ClickHouse на сервере:
SELECT getSetting('max_threads');
   ┌─getSetting('max_threads')─┐
1. │                        59 │
   └───────────────────────────┘
Однако значение max_threads может не учитываться в зависимости от объёма данных, выбранных для обработки:
EXPLAIN PIPELINE
SELECT
   max(price)
FROM
   uk.uk_price_paid_simple
WHERE town = 'LONDON';
...   
(ReadFromMergeTree)
MergeTreeSelect(pool: PrefetchedReadPool, algorithm: Thread) × 30
Как видно из приведённого выше фрагмента плана операторов, хотя max_threads задано значение 59, ClickHouse использует для чтения данных только 30 параллельных потоков. Теперь выполним запрос:
SELECT
   max(price)
FROM
   uk.uk_price_paid_simple
WHERE town = 'LONDON';
   ┌─max(price)─┐
1. │  594300000 │ -- 594,30 миллиона
   └────────────┘
   
1 row in set. Elapsed: 0.013 sec. Processed 2.31 million rows, 13.66 MB (173.12 million rows/s., 1.02 GB/s.)
Peak memory usage: 27.24 MiB.   
Как видно из вывода выше, запрос обработал 2,31 миллиона строк и прочитал 13,66 МБ данных. Это связано с тем, что на этапе анализа индекса ClickHouse выбрал для обработки 282 гранулы, каждая из которых содержала 8 192 строки, — всего примерно 2,31 миллиона строк:
EXPLAIN indexes = 1
SELECT
   max(price)
FROM
   uk.uk_price_paid_simple
WHERE town = 'LONDON';
    ┌─explain───────────────────────────────────────────────┐
 1. │ Expression ((Project names + Projection))             │
 2. │   Aggregating                                         │
 3. │     Expression (Before GROUP BY)                      │
 4. │       Expression                                      │
 5. │         ReadFromMergeTree (uk.uk_price_paid_simple)   │
 6. │         Indexes:                                      │
 7. │           PrimaryKey                                  │
 8. │             Keys:                                     │
 9. │               town                                    │
10. │             Condition: (town in ['LONDON', 'LONDON']) │
11. │             Parts: 3/3                                │
12. │             Granules: 282/3609                        │
    └───────────────────────────────────────────────────────┘  
Независимо от заданного значения max_threads, ClickHouse выделяет дополнительные параллельные линии обработки только тогда, когда для этого достаточно данных. «max» в max_threads означает верхний предел, а не гарантированное число используемых потоков. То, что считается «достаточным объёмом данных», в первую очередь определяется двумя настройками: они задают минимальное число строк (по умолчанию 163,840) и минимальное число байтов (по умолчанию 2,097,152), которые должна обрабатывать каждая линия обработки: Для кластеров shared-nothing: Для кластеров с общим хранилищем (например, ClickHouse Cloud): Кроме того, существует жёсткая нижняя граница размера задачи чтения, которая задаётся следующими настройками:
Не изменяйте эти настройкиМы не рекомендуем изменять эти настройки в production-среде. Здесь они приведены исключительно для того, чтобы показать, почему max_threads не всегда определяет фактический уровень параллелизма.
Для демонстрации давайте рассмотрим физический план, переопределив эти настройки так, чтобы принудительно включить максимальный параллелизм:
EXPLAIN PIPELINE
SELECT
   max(price)
FROM
   uk.uk_price_paid_simple
WHERE town = 'LONDON'
SETTINGS
  max_threads = 59,
  merge_tree_min_read_task_size = 0,
  merge_tree_min_rows_for_concurrent_read_for_remote_filesystem = 0, 
  merge_tree_min_bytes_for_concurrent_read_for_remote_filesystem = 0;
...   
(ReadFromMergeTree)
MergeTreeSelect(pool: PrefetchedReadPool, algorithm: Thread) × 59
Теперь ClickHouse использует 59 параллельных потоков для сканирования данных, строго соблюдая настроенное значение max_threads. Это показывает, что для запросов к небольшим наборам данных ClickHouse намеренно ограничивает параллелизм. Используйте переопределения настроек только для тестирования, а не в продакшене, так как это может привести к неэффективному выполнению запросов или конкуренции за ресурсы.

Ключевые выводы

  • ClickHouse распараллеливает запросы, используя линии обработки, число которых связано с max_threads.
  • Фактическое число линий зависит от объема данных, выбранных для обработки.
  • Используйте EXPLAIN PIPELINE и трассировочные логи, чтобы анализировать, как используются линии обработки.

Где найти дополнительную информацию

Если вы хотите глубже понять, как ClickHouse выполняет запросы параллельно и обеспечивает высокую производительность в большом масштабе, изучите следующие материалы:
Последнее изменение 10 июня 2026 г.