Files
clickstream-data-platform/docs/research/2026-08-22-view-predicate-pushdown.md
ddadminandClaude Fable 5 86ee901dd0 docs(research): проверено проталкивание условий сквозь представления над FINAL
Зачем: контракт будущего dds.event_v зависит от того, доезжают ли условия
запроса сквозь цепочку обычных представлений до индексов таблицы с FINAL,
или представление разворачивается целиком и фильтруется потом (#128).

Что: research-заметка с опытами на живом стенде (EXPLAIN indexes/actions,
сверка read_rows по system.query_log) и сверкой с документацией ClickHouse
через MCP Context7. Ответ: условия доезжают сквозь два уровня, включая
переименования и анализируемые выражения; FINAL отключает только
автоматический PREWHERE. Цепочка представлений ODS -> DDS -> DM годится.

Проверка: только читающие запросы, состояние стенда не менялось; вложенное
представление эмулировано подзапросом.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 03:33:55 -04:00

170 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Проталкивание условий сквозь представления над 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 переименованием, а не пересчётом.