Merge pull request 'docs(research): проталкивание условий сквозь представления над FINAL' (#139) from research/view-predicate-pushdown into main
This commit was merged in pull request #139.
This commit is contained in:
commit
d359882e1d
1 file changed
+169
@@ -0,0 +1,169 @@
|
|||||||
|
# Проталкивание условий сквозь представления над FINAL
|
||||||
|
|
||||||
|
Дата исследования: 2026-08-22. Тикет #128, карта #127.
|
||||||
|
|
||||||
|
Вопрос: пробрасывает ли ClickHouse условия запроса внутрь обычного
|
||||||
|
представления над таблицей с `FINAL` (`ods.event_v`) и внутрь вложенного
|
||||||
|
представления над ним (будущее `dds.event_v`) — или разворачивает
|
||||||
|
представление целиком и фильтрует потом.
|
||||||
|
|
||||||
|
## Короткий вывод
|
||||||
|
|
||||||
|
- Пробрасывает. Обычное представление — это подзапрос в `FROM`, и условие
|
||||||
|
внешнего запроса доезжает сквозь него до партиций и первичного ключа
|
||||||
|
таблицы. Проверено планом и счётчиком прочитанных строк.
|
||||||
|
- Второй уровень вложенности ничего не ломает: сквозь подзапрос над
|
||||||
|
`ods.event_v` с переименованными колонками условие доезжает так же —
|
||||||
|
срезание партиций совпадает с прямым запросом к `ods.event_dist FINAL`
|
||||||
|
один в один.
|
||||||
|
- Условие доезжает даже сквозь вычисленную колонку: фильтр по
|
||||||
|
`toStartOfWeek(EventDate)` анализируется индексом MinMax.
|
||||||
|
- Единственное, что `FINAL` отнимает, — автоматический перенос условия в
|
||||||
|
`PREWHERE`: настройка `optimize_move_to_prewhere_if_final` по умолчанию
|
||||||
|
выключена. Срезание по индексам при этом работает полностью; теряется
|
||||||
|
только оптимизация порядка чтения колонок внутри уже выбранных гранул.
|
||||||
|
- Для контракта `dds.event_v` это значит: цепочка обычных представлений
|
||||||
|
ODS → DDS годится, представление до фильтра не материализуется.
|
||||||
|
|
||||||
|
## Как проверяли
|
||||||
|
|
||||||
|
Живой стенд, ClickHouse 26.3.17.56, `enable_analyzer = 1` (умолчание).
|
||||||
|
Только читающие запросы с ноды 1: `EXPLAIN indexes = 1`,
|
||||||
|
`EXPLAIN actions = 1`, `SELECT` со сверкой по `system.query_log`.
|
||||||
|
Вложенное представление не создавали — состояние стенда не менялось;
|
||||||
|
второй уровень эмулирован эквивалентным подзапросом.
|
||||||
|
|
||||||
|
Устройство пары: `ods.event_rep` — `ReplicatedReplacingMergeTree(_load_ts)`,
|
||||||
|
`PARTITION BY EventDate`, `ORDER BY (CounterID, EventDate,
|
||||||
|
intHash32(ClientID), WatchID)`; на момент опыта 16 активных кусков в
|
||||||
|
8 партициях, 24 гранулы. `ods.event_v` — `SELECT * FROM ods.event_dist FINAL`.
|
||||||
|
|
||||||
|
## Опыт 1. Фильтр по ключу партиции сквозь представление
|
||||||
|
|
||||||
|
```sql
|
||||||
|
EXPLAIN indexes = 1
|
||||||
|
SELECT count() FROM ods.event_v WHERE EventDate = '2026-06-01';
|
||||||
|
```
|
||||||
|
|
||||||
|
План читает `ods.event_rep` с условием `EventDate in [20605, 20605]` во всех
|
||||||
|
трёх индексах — MinMax, Partition, PrimaryKey: `Parts: 2/16`,
|
||||||
|
`Granules: 3/24`. Прямой запрос `SELECT count() FROM ods.event_dist FINAL
|
||||||
|
WHERE EventDate = '2026-06-01'` даёт те же цифры. Представление ничего не
|
||||||
|
прячет от анализа индексов.
|
||||||
|
|
||||||
|
## Опыт 2. Второй уровень: подзапрос над представлением
|
||||||
|
|
||||||
|
Будущее `dds.event_v` — представление над `ods.event_v` с переименованием в
|
||||||
|
язык бизнес-модели. Эмуляция:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
EXPLAIN indexes = 1
|
||||||
|
SELECT count()
|
||||||
|
FROM (
|
||||||
|
SELECT WatchID AS watch_id, EventDate AS event_date,
|
||||||
|
lower(EventType) AS event_type
|
||||||
|
FROM ods.event_v
|
||||||
|
) WHERE event_date = '2026-06-01';
|
||||||
|
```
|
||||||
|
|
||||||
|
Условие по переименованной колонке доехало до таблицы: `Parts: 2/16`,
|
||||||
|
`Granules: 3/24` — цифры совпали с опытом 1. Два уровня разворачивания и
|
||||||
|
переименование колонок проталкиванию не мешают.
|
||||||
|
|
||||||
|
Отдельно проверена вычисленная колонка:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT count()
|
||||||
|
FROM (SELECT toStartOfWeek(EventDate) AS event_week FROM ods.event_v)
|
||||||
|
WHERE event_week = '2026-06-01';
|
||||||
|
```
|
||||||
|
|
||||||
|
В плане условие `toStartOfWeek(EventDate) in [20605, 20605]` стоит в индексе
|
||||||
|
MinMax и срезает `Parts: 0/16` — ноль корректен: при умолчании функции ни
|
||||||
|
одна неделя не начинается в этот понедельник. То есть даже фильтр по
|
||||||
|
выражению от ключевой колонки анализируется индексом, пока функция ему
|
||||||
|
понятна.
|
||||||
|
|
||||||
|
## Опыт 3. Фильтр по ключу сортировки
|
||||||
|
|
||||||
|
```sql
|
||||||
|
EXPLAIN indexes = 1
|
||||||
|
SELECT count() FROM ods.event_v WHERE CounterID = 42150607;
|
||||||
|
```
|
||||||
|
|
||||||
|
Условие `CounterID in [...]` дошло до PrimaryKey (`Search Algorithm: binary
|
||||||
|
search`). Гранулы не срезались (`24/24`) — но это свойство данных, а не
|
||||||
|
представления: счётчик один, он есть в каждой грануле.
|
||||||
|
|
||||||
|
## Опыт 4. PREWHERE и FINAL
|
||||||
|
|
||||||
|
По колонке вне ключа (`EventType = 'pageview'`, `EXPLAIN actions = 1`,
|
||||||
|
запрос с широким списком колонок):
|
||||||
|
|
||||||
|
- `ods.event_rep` без `FINAL` — условие уехало в `Prewhere filter`;
|
||||||
|
- `ods.event_rep FINAL` и `ods.event_v` — условие осталось обычным `Filter`
|
||||||
|
над шагом чтения.
|
||||||
|
|
||||||
|
Причина — настройка `optimize_move_to_prewhere_if_final`, на стенде `0`
|
||||||
|
(умолчание). Это цена самого `FINAL`, а не представления: прямой запрос с
|
||||||
|
`FINAL` ведёт себя так же.
|
||||||
|
|
||||||
|
## Опыт 5. Счётчик прочитанных строк
|
||||||
|
|
||||||
|
План — обещание; исполнение сверено по `system.query_log`
|
||||||
|
(`read_rows` начального запроса, сумма по кластеру):
|
||||||
|
|
||||||
|
| Запрос | read_rows |
|
||||||
|
|---|---|
|
||||||
|
| `ods.event_v` без фильтра | 401 185 |
|
||||||
|
| `ods.event_v WHERE EventDate = '2026-06-01'` | 50 626 |
|
||||||
|
| подзапрос над `ods.event_v`, фильтр по `event_date` | 50 626 |
|
||||||
|
|
||||||
|
Фильтр сквозь представление читает только партиции своего дня, вложенность
|
||||||
|
не добавляет ни строки. 401 185 — все физические строки обеих нод.
|
||||||
|
|
||||||
|
## Что говорит документация
|
||||||
|
|
||||||
|
Сверено через MCP Context7 22 августа 2026 года, библиотека
|
||||||
|
`/websites/clickhouse`.
|
||||||
|
|
||||||
|
- Обычное представление не хранит данных: при обращении его запрос
|
||||||
|
исполняется как подзапрос в `FROM` — `SELECT a, b, c FROM view`
|
||||||
|
эквивалентен `SELECT a, b, c FROM (SELECT ...)`
|
||||||
|
(`sql-reference/statements/create/view`).
|
||||||
|
- Проталкивание предикатов в подзапросы — штатная работа оптимизатора;
|
||||||
|
анализатор доводит фильтры до хранилища, включая срезание партиций и
|
||||||
|
первичного ключа (changelog 2026, настройки семейства
|
||||||
|
`allow_push_predicate_*`).
|
||||||
|
- `optimize_move_to_prewhere_if_final` управляет автоматическим `PREWHERE`
|
||||||
|
для запросов с `FINAL` (`settings/session-settings`); на стенде она в
|
||||||
|
умолчании `0`, что и видно в опыте 4.
|
||||||
|
- `do_not_merge_across_partitions_select_final = 1` заставляет `FINAL`
|
||||||
|
обрабатывать партиции независимо; с 2026 года значение `1` включает режим
|
||||||
|
безусловно, а автоматику решает отдельная настройка
|
||||||
|
`enable_automatic_decision_for_merging_across_partitions_for_final`
|
||||||
|
(changelog 2026). К `ods.event` режим применим: партиция — `EventDate`,
|
||||||
|
версии одного события всегда лежат в одном дне. На стенде настройка `0`,
|
||||||
|
и трогать её не понадобилось — вопрос тикета решился без неё.
|
||||||
|
- `FINAL` поверх подзапроса не работает — модификатор обязан стоять у самой
|
||||||
|
таблицы (`guides/.../analyzer`, «Fixing Invalid FINAL Usage»). Наша
|
||||||
|
цепочка это соблюдает: `FINAL` живёт в один слой, внутри `ods.event_v`.
|
||||||
|
|
||||||
|
## Вывод для контракта dds.event_v
|
||||||
|
|
||||||
|
Сомнение владельца — «представление материализуется до фильтра» — не
|
||||||
|
подтвердилось. Условия по ключу партиции и ключу сортировки доезжают до
|
||||||
|
индексов сквозь два уровня обычных представлений, переименование колонок и
|
||||||
|
анализируемые выражения; исполняется это, а не только планируется.
|
||||||
|
|
||||||
|
Цепочка обычных представлений ODS → DDS → DM годится как форма контракта.
|
||||||
|
Две оговорки в контракт:
|
||||||
|
|
||||||
|
1. `FINAL` внутри `ods.event_v` отключает автоматический `PREWHERE` — на
|
||||||
|
объёмах стенда это незаметно, но слабый фильтр по неключевой колонке
|
||||||
|
при широком `SELECT` читает больше, чем без `FINAL`. Это свойство
|
||||||
|
`FINAL`, представления его не усугубляют.
|
||||||
|
2. Индекс срезает только то, что умеет анализировать: фильтр по колонке,
|
||||||
|
которую `dds.event_v` вычислит нетривиальным выражением, до индекса
|
||||||
|
может не доехать. Ключевые для срезов колонки — прежде всего
|
||||||
|
`EventDate` — стоит отдавать в DDS переименованием, а не пересчётом.
|
||||||
Reference in new issue
Block a user