Векторизованное выполнение
Почему ClickHouse обрабатывает миллиарды строк в секунду на одном сервере? Ответ начинается не с индексов и не с сжатия — он начинается с модели выполнения. ClickHouse использует векторизованное (batch) выполнение: все операторы работают не на отдельных строках, а на пакетах значений столбцов (column blocks).
Операторы работают на пакетах (векторах) значений столбцов. Обработка данных порциями кардинально снижает накладные расходы и улучшает использование CPU-кэша.
Block: единица данных в памяти
Block — это контейнер, представляющий подмножество таблицы в оперативной памяти. Каждый Block — это набор троек:
| Компонент | Тип | Назначение |
|---|---|---|
| IColumn | Массив значений | Непрерывный массив данных одного столбца (UInt64, String и т.д.) |
| IDataType | Метаданные типа | Описывает как сериализовать, десериализовать и сравнивать значения |
| column_name | Строка | Имя столбца для привязки к схеме таблицы |
Block обычно содержит до 65 536 строк (настраивается через max_block_size). Именно Block передаётся между процессорами в pipeline — не строки, не файлы, не целые таблицы.
Строчное vs. Векторизованное выполнение
Классические СУБД (PostgreSQL, MySQL) обрабатывают данные по одной строке: каждая строка проходит через цепочку операторов, каждый оператор вызывается для каждой строки отдельно. При миллиардах строк накладные расходы на вызов функций, проверки типов и переключения контекста становятся доминирующими.
ClickHouse инвертирует этот подход: каждый оператор получает целый блок (тысячи значений одного столбца) и обрабатывает его за один вызов функции.
Почему это работает: три механизма
1. Амортизация накладных расходов
Каждый вызов функции несёт фиксированные расходы: подготовка аргументов, virtual dispatch, проверки. В строчной модели эти расходы на каждую строку. В блочной модели — на каждый блок (65 536 строк). Экономия: 65 536x меньше вызовов.
2. CPU-кэш и data locality
IColumn хранит значения как непрерывный массив в памяти: [val_0, val_1, val_2, ..., val_65535]. Когда процессор загружает cache line (64 байта), он получает сразу 8 значений UInt64 или 16 значений UInt32. Следующее значение уже в кэше — нет cache miss.
В строчной модели столбцы разных типов перемешаны в одной строке. Cache line содержит фрагменты нескольких столбцов, но запрос читает только один — остальные байты потрачены впустую.
3. SIMD-инструкции
SIMD (Single Instruction, Multiple Data) — набор процессорных инструкций, обрабатывающих несколько значений за один такт. Пример: AVX-512 обрабатывает 8 значений Float64 параллельно.
Компилятор автоматически векторизует tight loops по непрерывным массивам. ClickHouse также использует SIMD вручную в критических путях:
// Упрощённый пример: суммирование блока UInt64
// Компилятор превращает этот loop в SIMD-инструкции
UInt64 sum = 0;
for (size_t i = 0; i < block_size; ++i)
sum += data[i];
При block_size = 65 536 и AVX-512: вместо 65 536 сложений — ~8 192 SIMD-операции (8 значений за раз).
Block в контексте pipeline
Данные в ClickHouse движутся через pipeline процессоров. Каждый процессор получает Block на вход и выдаёт Block на выход:
ReadFromMergeTree → Block → ExpressionTransform → Block → AggregatingTransform → Block → Output
Ключевые свойства:
- Block передаётся по значению между процессорами (zero-copy через shared pointers на IColumn)
- Размер блока контролируется настройкой
max_block_size(по умолчанию 65 536) - Параллелизм: несколько потоков обрабатывают разные Block одновременно (число потоков =
max_threads)
Практические следствия для пользователя
| Аспект | Следствие |
|---|---|
| Размер блока | max_block_size влияет на потребление памяти и эффективность SIMD. Обычно 65 536 оптимально |
| Количество столбцов в SELECT | Каждый столбец — отдельный IColumn в Block. SELECT * загружает все столбцы в память |
| Типы данных | Фиксированные типы (UInt64, Float64) эффективнее для SIMD, чем String (переменная длина) |
| Параллелизм | max_threads определяет сколько Block обрабатываются одновременно |
Проверить настройки блока текущей сессии:
SELECT
name, value, description
FROM system.settings
WHERE name IN ('max_block_size', 'max_threads')Ключевые выводы
- Block — это набор троек (IColumn, IDataType, column_name), представляющий подмножество таблицы в памяти. Все процессоры pipeline работают с Block.
- Амортизация: один вызов функции на 65 536 строк вместо 65 536 отдельных вызовов. Overhead сокращается на порядки.
- CPU cache locality: непрерывный массив значений одного столбца максимально эффективно использует cache line процессора.
- SIMD: компилятор автоматически векторизует tight loops по непрерывным массивам. AVX-512 обрабатывает до 8 Float64 за такт.
- Для пользователя:
max_block_sizeиmax_threads— два ключевых параметра, влияющих на производительность vectorized execution.