read_in_order: пропускаем сортировку
Сортировка — одна из самых дорогих операций в pipeline запроса. Она требует чтения всех данных в память, построения структуры сортировки и записи результата. Но если данные на диске уже отсортированы в нужном порядке, сортировку можно пропустить полностью.
ClickHouse делает это автоматически через оптимизацию read_in_order: если ORDER BY запроса совпадает с ORDER BY таблицы (или является его префиксом), данные читаются уже в правильном порядке.
Как работает read_in_order
MergeTree хранит данные физически отсортированными по ORDER BY ключу таблицы. Каждый part содержит строки в порядке, определённом при CREATE TABLE.
Когда запрос требует ORDER BY, ClickHouse проверяет: совпадает ли запрошенная сортировка с физическим порядком данных? Если да — SortingTransform в pipeline не нужен.
-- Таблица отсортирована по (country, event_date, user_id)
CREATE TABLE events (
country String,
event_date Date,
user_id UInt32,
value UInt32
) ENGINE = MergeTree()
ORDER BY (country, event_date, user_id);
Prefix matching: совпадение по префиксу
Настройка optimize_read_in_order включена по умолчанию. Она активируется, когда ORDER BY запроса является префиксом ORDER BY таблицы:
| ORDER BY таблицы | ORDER BY запроса | read_in_order? | Почему |
|---|---|---|---|
| (country, event_date, user_id) | country, event_date, user_id | Да | Полное совпадение |
| (country, event_date, user_id) | country, event_date | Да | Префикс: первые 2 столбца |
| (country, event_date, user_id) | country | Да | Префикс: первый столбец |
| (country, event_date, user_id) | event_date, country | Нет | Порядок не совпадает |
| (country, event_date, user_id) | user_id | Нет | Не префикс (3-й столбец) |
| (country, event_date, user_id) | country, user_id | Нет | Пропущен event_date |
Правило: ORDER BY запроса должен быть непрерывным префиксом ORDER BY таблицы. Пропуск столбца в середине нарушает порядок.
Constant column skipping
Интересный случай: столбец в ORDER BY таблицы зафиксирован через WHERE. В этой ситуации ClickHouse пропускает его и применяет read_in_order к следующему столбцу:
-- ORDER BY таблицы: (country, event_date, user_id)
-- country зафиксирован через WHERE = фактически константа
SELECT user_id, value
FROM events
WHERE country = 'RU'
ORDER BY country, event_date;
-- read_in_order: YES
-- country = 'RU' => константа, event_date -- следующий в ORDER BY
Ещё более интересный пример:
-- WHERE фиксирует country, ORDER BY начинается с event_date
SELECT user_id, value
FROM events
WHERE country = 'RU'
ORDER BY event_date;
-- read_in_order: YES
-- country -- константа (WHERE country = 'RU'), пропускается
-- event_date -- следующий в ORDER BY таблицы после country
-- Данные внутри partition country='RU' уже отсортированы по event_date
Без constant column skipping этот запрос потребовал бы полной сортировки. С ним — данные читаются в правильном порядке, потому что внутри фиксированного country строки уже упорядочены по event_date.
Подтверждение через EXPLAIN PIPELINE
Как убедиться, что read_in_order активен? Используйте EXPLAIN PIPELINE:
read_in_order активен (сортировка пропущена):
EXPLAIN PIPELINE
SELECT country, event_date, user_id
FROM events
ORDER BY country, event_date;
(Expression)
ExpressionTransform
(ReadFromMergeTree)
MergeTreeInOrder 0 → 1
Ключевой индикатор: MergeTreeInOrder вместо MergeTreeThread. Нет SortingTransform — данные читаются в правильном порядке, сортировка не требуется.
read_in_order НЕ активен (требуется сортировка):
EXPLAIN PIPELINE
SELECT country, event_date, user_id
FROM events
ORDER BY user_id, country;
(Expression)
ExpressionTransform
(Sorting)
MergeSortingTransform
(Expression)
ExpressionTransform
(ReadFromMergeTree)
MergeTreeThread 0 → 1
Ключевой индикатор: MergeSortingTransform — ClickHouse добавляет этап сортировки в pipeline, потому что ORDER BY запроса (user_id, country) не совпадает с ORDER BY таблицы (country, event_date, user_id).
Влияние на производительность
Для таблицы в 1 миллиард строк:
| Метрика | С read_in_order | Без read_in_order |
|---|---|---|
| Потребление RAM | Минимальное (потоковое чтение) | До нескольких ГБ (буфер сортировки) |
| Время выполнения | Пропорционально объёму данных | +30-50% на сортировку |
| CPU | Чтение + фильтрация | Чтение + фильтрация + сортировка |
Выигрыш особенно заметен при:
- LIMIT N запросах (ClickHouse может остановиться после N строк без чтения всей таблицы)
- Больших таблицах, где сортировка требует spill на диск
- Частых запросах с одинаковым ORDER BY
Практические рекомендации
Проектируйте ORDER BY таблицы с учётом типичных запросов. Если 80% запросов используют ORDER BY event_date, event_date должен быть в начале ORDER BY таблицы.
Выбор ORDER BY таблицы — это баланс между:
- Selectivity для фильтрации (primary key granule skipping)
- read_in_order для сортировки запросов
- Compression ratio (одинаковые значения подряд сжимаются лучше)
Если ORDER BY для фильтрации и для запросов конфликтуют, рассмотрите Projections: дополнительная копия данных с другим ORDER BY, автоматически выбираемая оптимизатором.
Ключевые выводы
- read_in_order пропускает SortingTransform в pipeline, когда ORDER BY запроса совпадает с ORDER BY таблицы или является его префиксом.
- Настройка
optimize_read_in_orderвключена по умолчанию — дополнительных действий не требуется. - Prefix matching: ORDER BY запроса должен быть непрерывным префиксом ORDER BY таблицы. Пропуск столбца в середине нарушает порядок.
- Constant column skipping: WHERE column = value фиксирует столбец как константу. ClickHouse пропускает его и применяет read_in_order к следующему столбцу ORDER BY.
- Проверка: EXPLAIN PIPELINE покажет MergeTreeInOrder (read_in_order активен) или MergeSortingTransform (сортировка требуется).