From 86ee901dd08e8491e35f1f9443b6fe51d9658421 Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Sat, 22 Aug 2026 03:33:55 -0400 Subject: [PATCH] =?UTF-8?q?docs(research):=20=D0=BF=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=BD=D0=BE=20=D0=BF=D1=80=D0=BE=D1=82=D0=B0?= =?UTF-8?q?=D0=BB=D0=BA=D0=B8=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D1=83=D1=81?= =?UTF-8?q?=D0=BB=D0=BE=D0=B2=D0=B8=D0=B9=20=D1=81=D0=BA=D0=B2=D0=BE=D0=B7?= =?UTF-8?q?=D1=8C=20=D0=BF=D1=80=D0=B5=D0=B4=D1=81=D1=82=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BD=D0=B0=D0=B4=20FINAL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Зачем: контракт будущего 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 --- .../2026-08-22-view-predicate-pushdown.md | 169 ++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 docs/research/2026-08-22-view-predicate-pushdown.md diff --git a/docs/research/2026-08-22-view-predicate-pushdown.md b/docs/research/2026-08-22-view-predicate-pushdown.md new file mode 100644 index 0000000..946de83 --- /dev/null +++ b/docs/research/2026-08-22-view-predicate-pushdown.md @@ -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 переименованием, а не пересчётом. -- 2.54.0