EXPLAIN, анализ планов выполнения, оптимизация JOIN, предикаты, партиционирование, типичные антипаттерны
EXPLAIN, анализ планов выполнения, оптимизация JOIN, предикаты, типичные антипаттерны
EXPLAIN SELECT * FROM events WHERE user_id = 123;Результат:
Expression (Projection)
Filter (WHERE)
ReadFromMergeTree (events)
Этапы:
-- План с индексами
EXPLAIN indexes = 1
SELECT * FROM events WHERE user_id = 123;
-- План с проекциями
EXPLAIN projections = 1
SELECT user_id, count() FROM events GROUP BY user_id;
-- Полный план
EXPLAIN indexes = 1, projections = 1, json = 1
SELECT * FROM events WHERE user_id = 123;-- Визуализация конвейера выполнения
EXPLAIN PIPELINE
SELECT * FROM events WHERE user_id = 123;
-- С графиком выполнения
EXPLAIN PIPELINE graph = 1
SELECT * FROM events WHERE user_id = 123;Вывод:
ReadFromMergeTree
↓
Filter
↓
Expression
↓
Null (результат)
| Операция | Описание | На что обратить внимание |
|---|---|---|
| ReadFromMergeTree | Чтение из таблицы | Columns read, Parts read |
| Filter | Применение WHERE | Selectivity |
| Expression | Вычисления | Number of columns |
| Aggregating | GROUP BY/агрегаты | Memory usage |
| Join | JOIN операции | Type, Size |
| Sorting | ORDER BY | Memory, Disk |
| Limit | LIMIT | Rows before limit |
| Union | UNION ALL | Number of inputs |
EXPLAIN
SELECT
user_id,
count() AS events,
sum(value) AS total_value
FROM events
WHERE event_date = '2026-03-01'
AND event_type IN ('click', 'view')
GROUP BY user_id
HAVING events > 10
ORDER BY total_value DESC
LIMIT 100;План:
1. Limit (LIMIT 100)
2. Sorting (ORDER BY total_value DESC)
3. Filter (HAVING events > 10)
4. Aggregating (GROUP BY user_id)
5. Filter (WHERE event_date, event_type)
6. ReadFromMergeTree (events)
Оптимизации:
ClickHouse применяет условия WHERE как можно раньше:
-- Оптимизировано: фильтр применяется при чтении
SELECT * FROM events
WHERE event_date = '2026-03-01'
AND toHour(event_time) = 12;
-- Проблема: toHour() вычисляется для всех строк
-- Лучше:
SELECT * FROM events
WHERE event_date = '2026-03-01'
AND event_time >= '2026-03-01 12:00:00'
AND event_time < '2026-03-01 13:00:00';-- Плохо: условие с низкой селективностью первым
SELECT * FROM events
WHERE event_type = 'click' -- 80% строк
AND user_id = 123; -- 0.001% строк
-- Лучше: более селективное условие первым
-- (ClickHouse обычно оптимизирует автоматически)
SELECT * FROM events
WHERE user_id = 123
AND event_type = 'click';-- Использует индекс (event_date в начале ORDER BY)
SELECT * FROM events
WHERE event_date = '2026-03-01';
-- Не использует индекс (функция в WHERE)
SELECT * FROM events
WHERE toYYYYMM(event_time) = 202603;
-- Использует индекс (диапазон)
SELECT * FROM events
WHERE event_time >= '2026-03-01'
AND event_time < '2026-04-01';-- Самый быстрый: JOIN по ключу с локальными данными
SELECT e.*, u.country
FROM events_local e
JOIN users_local u ON e.user_id = u.id;
-- Медленнее: GLOBAL JOIN (рассылка данных)
SELECT e.*, u.country
FROM events_all e
GLOBAL JOIN users_all u ON e.user_id = u.id;
-- Ещё медленнее: JOIN без ключа
SELECT * FROM events_all
CROSS JOIN dimensions;1. Предварительная фильтрация:
-- Фильтрация перед JOIN уменьшает объём данных
SELECT e.*, u.country
FROM
(SELECT * FROM events_all WHERE event_date = '2026-03-01') e
GLOBAL JOIN
(SELECT * FROM users_all WHERE active = 1) u
ON e.user_id = u.id;2. Использование словарей:
-- Вместо JOIN
SELECT
user_id,
dictGet('users_dict', 'country', user_id) AS country
FROM events_all;-- USING эффективнее чем ON с одинаковыми колонками
SELECT * FROM events
JOIN users USING (user_id);
-- Вместо:
SELECT * FROM events
JOIN users ON events.user_id = users.user_id;-- Плохо: JOIN больших таблиц с последующей агрегацией
SELECT
u.country,
count() AS events
FROM events_all e
JOIN users_all u ON e.user_id = u.id
GROUP BY u.country;
-- Лучше: агрегация до JOIN
WITH events_by_user AS (
SELECT user_id, count() AS user_events
FROM events_all
GROUP BY user_id
)
SELECT
u.country,
sum(e.user_events) AS events
FROM events_by_user e
JOIN users_all u ON e.user_id = u.id
GROUP BY u.country;-- Точный подсчёт (медленно для больших данных)
SELECT count(DISTINCT user_id) AS users FROM events;
-- Приблизительный (быстро, ошибка ~1-2%)
SELECT uniq(user_id) AS users FROM events;
-- Комбинированный (авто-выбор)
SELECT uniqCombined(user_id) AS users FROM events;-- Создание проекции для ускорения
ALTER TABLE events ADD PROJECTION daily_stats (
SELECT
toDate(event_time) AS date,
event_type,
count() AS events
GROUP BY date, event_type
ORDER BY date
);
-- Запрос автоматически использует проекцию
SELECT
toDate(event_time) AS date,
event_type,
count() AS events
FROM events
GROUP BY date, event_type;-- ClickHouse оптимизирует: сортировка до LIMIT
SELECT * FROM events
ORDER BY event_time DESC
LIMIT 100;
-- Сложный случай: сортировка после агрегации
SELECT
user_id,
sum(value) AS total
FROM events
GROUP BY user_id
ORDER BY total DESC
LIMIT 100;-- WITH TIES возвращает все строки с последним значением
SELECT * FROM events
ORDER BY event_time DESC
LIMIT 100 WITH TIES;-- argMax для получения строки с максимумом
SELECT
user_id,
argMax(event_type, event_time) AS last_event
FROM events
GROUP BY user_id;
-- Вместо:
SELECT
e1.user_id,
e1.event_type
FROM events e1
JOIN (
SELECT user_id, max(event_time) AS max_time
FROM events
GROUP BY user_id
) e2 ON e1.user_id = e2.user_id AND e1.event_time = e2.max_time;-- Плохо: читает все колонки
SELECT * FROM events WHERE user_id = 123;
-- Хорошо: только нужные колонки
SELECT event_time, event_type, value
FROM events
WHERE user_id = 123;-- Плохо: функция предотвращает использование индекса
SELECT * FROM events
WHERE toYYYYMM(event_time) = 202603;
-- Хорошо: диапазонное условие
SELECT * FROM events
WHERE event_time >= '2026-03-01'
AND event_time < '2026-04-01';-- Плохо: DISTINCT без агрегации
SELECT DISTINCT user_id, event_type FROM events;
-- Лучше: GROUP BY
SELECT user_id, event_type FROM events GROUP BY user_id, event_type;-- Плохо: коррелированный подзапрос
SELECT
user_id,
(SELECT count() FROM events e WHERE e.user_id = u.id) AS events
FROM users u;
-- Лучше: JOIN
SELECT
u.id AS user_id,
count(e.user_id) AS events
FROM users u
LEFT JOIN events e ON u.id = e.user_id
GROUP BY u.id;-- Плохо: большая таблица слева
SELECT * FROM
events_all e -- 1 млрд строк
JOIN users_all u ON e.user_id = u.id -- 10 млн строк
WHERE u.country = 'RU';
-- Лучше: фильтрация до JOIN
SELECT * FROM
(SELECT * FROM users_all WHERE country = 'RU') u
JOIN events_all e ON u.id = e.user_id;-- Разрешить оптимизации
SET optimize_read_in_order = 1;
SET optimize_aggregation_in_order = 1;
SET optimize_or_like_chain = 1;
-- Оптимизация JOIN
SET join_algorithm = 'auto'; -- auto, hash, partial_merge
SET prefer_columnar_to_block_limit = 1024;
-- Оптимизация агрегации
SET optimize_aggregators_order_key = 1;
SET force_aggregation_memory_efficient = 0;
-- Оптимизация сортировки
SET max_bytes_before_external_sort = 0; -- 0 = без дискаSELECT name, value, changed
FROM system.settings
WHERE changed = 1;SELECT
query,
read_rows,
read_bytes,
result_rows,
result_bytes,
elapsed,
memory_usage
FROM system.query_log
WHERE query_date = today()
AND query LIKE '%SELECT%'
ORDER BY elapsed DESC
LIMIT 20;-- Текущие запросы
SELECT
query_id,
query,
elapsed,
read_rows,
read_bytes,
memory_usage
FROM system.processes
ORDER BY elapsed DESC;Изучим материализованные представления для предварительной агрегации данных.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.