Learning Platform
Глоссарий Troubleshooting
Урок 06.06 · 24 мин
Средний
vectorized-enginequery-pipelinepush-based

Push-based исполнение: от SQL к физическому плану

До сих пор в модуле мы разбирали структуры данных — Vector, DataChunk, физические типы, selection vector. Но эти структуры сами по себе не выполняют запрос. Нужно две вещи: превратить текст SQL в дерево операторов и прогнать через это дерево DataChunk-и. Этот урок — про обе. Сначала путь запроса от строки SQL до исполняемого физического плана, потом — модель исполнения, которой DuckDB прогоняет данные через план.

И здесь у DuckDB есть второе важное архитектурное решение (первое — векторизация). Классические движки тянут данные сверху вниз — pull-модель, итераторы Volcano. DuckDB толкает данные снизу вверх — push-модель.

Trino: логическое планирование и PlanNode

DataFusion: Query Pipeline от SQL до RecordBatch Разберём весь конвейер: четыре стадии компиляции запроса и push-исполнение.


Путь запроса: четыре стадии

Текст SQL не исполняется напрямую. Между строкой запроса и работающими операторами — четыре стадии преобразования. Каждая берёт результат предыдущей и обогащает его.

Конвейер обработки запроса: SQL в физический план
SQL-текстИсходная строка запроса от пользователя или приложения.
Parser
Parser: текст -> синтаксическое деревоРазбирает SQL по грамматике в дерево. Проверяет только синтаксис. Не знает про существование таблиц и колонок.
Binder
Binder: разрешение имён и типовСверяет имена таблиц и колонок с каталогом, определяет типы выражений. Здесь ловятся ошибки 'нет такой таблицы'.
Logical Planner
Logical plan: дерево логических операторовДерево LogicalOperator: ЧТО надо сделать (скан, фильтр, join, агрегация), без деталей КАК.
Optimizer
Optimizer: переписывание планаНесколько проходов: pushdown фильтров, выбор порядка join, упрощение выражений. План остаётся логическим, но эффективнее.
Physical Planner
Physical plan: дерево физических операторовДерево PhysicalOperator: конкретные алгоритмы (hash join, hash aggregate). Это дерево и исполняется.

Parser

Parser (парсер) превращает текст SQL в синтаксическое дерево по грамматике языка. Он отвечает на вопрос «синтаксически ли корректен запрос»: расставлены ли ключевые слова, сбалансированы ли скобки, на месте ли запятые.

Что важно — парсер не знает ничего о содержимом базы. Он не в курсе, существует ли таблица orders и есть ли в ней колонка amount. Парсер работает только с текстом и грамматикой. Запрос SELECT foo FROM bar пройдёт парсер успешно, даже если ни bar, ни foo не существуют — синтаксически он безупречен. Парсер DuckDB основан на грамматике PostgreSQL, поэтому DuckDB понимает Postgres-совместимый SQL.

Binder

Binder (биндер) — стадия, которая сводит запрос с реальностью базы. Биндер обращается к каталогу (catalog) — внутреннему реестру всех таблиц, колонок, типов, функций — и привязывает (binds) имена из запроса к реальным объектам.

Биндер делает две главные вещи. Первое — разрешение имён: проверяет, что таблица orders существует, что колонка amount в ней есть, что функция sum известна. Именно здесь возникают ошибки вида «table does not exist» или «column not found» — не в парсере, а в биндере. Второе — определение типов: биндер выясняет тип каждого выражения. amount это INTEGER, amount * 1.08 это DOUBLE, результат sum(amount) имеет такой-то тип. Без типов нельзя ни оптимизировать, ни исполнять запрос.

Граница «парсер не знает каталог, биндер знает» — принципиальная. Парсер — про форму, биндер — про смысл. Конструкции friendly SQL вроде COLUMNS() и GROUP BY ALL (модуль 03) разворачиваются именно на стадии биндинга: чтобы развернуть COLUMNS('regex') в список колонок, нужен каталог — а он есть только у биндера.

Logical planner

Logical Planner строит из привязанного дерева логический план — дерево LogicalOperator. Логический план описывает, ЧТО нужно сделать, в терминах реляционной алгебры: просканировать таблицу, отфильтровать, соединить, сгруппировать. Но он не говорит, КАК: «соединить две таблицы» — без указания, каким алгоритмом join.

Optimizer

Optimizer (оптимизатор) берёт логический план и переписывает его в эквивалентный, но более эффективный. Это несколько проходов (passes), каждый делает своё преобразование. Среди них: filter pushdown (продвинуть фильтры как можно ближе к сканированию, чтобы рано отсеять строки), projection pushdown (читать только нужные колонки), выбор порядка соединений (join order), упрощение выражений (например, свёртка констант). Оптимизатор подробно разбирается в отдельном модуле курса; здесь важно его место в конвейере — между логическим и физическим планом.

Physical planner

Physical Planner превращает оптимизированный логический план в физический — дерево PhysicalOperator. Здесь абстрактные операции получают конкретные алгоритмы: логический «join» становится физическим HASH JOIN, логическая «агрегация» — HASH AGGREGATE или другим конкретным алгоритмом. Физический план — это уже исполняемое дерево: именно его прогоняют DataChunk-и.


Pull против push: направление движения данных

Дерево физических операторов построено. Теперь — как через него текут данные. И здесь два принципиально разных подхода.

Классическая модель — pull (тянущая), она же итераторная модель Volcano. Управление идёт сверху вниз. Корневой оператор говорит дочернему «дай мне данные», тот говорит своему дочернему «дай мне данные», запрос-«дай» спускается до самого листа-сканирования. Затем данные поднимаются обратно вверх как ответы на эти запросы. Каждый оператор — итератор с методом «верни следующее». Инициатива у верхнего оператора, он вытягивает данные.

DuckDB использует push (толкающую) модель. Управление идёт снизу вверх. Источник данных (сканирование таблицы) сам берёт инициативу: он читает DataChunk и толкает (pushes) его вверх — передаёт следующему оператору. Тот обрабатывает чанк и толкает результат ещё выше. DataChunk-и движутся снизу вверх по дереву, проталкиваемые источником, а не вытягиваемые корнем.

Pull против push: кто двигает данные
PULL (Volcano): корень тянетУправление сверху вниз: корневой оператор запрашивает данные у дочернего, запрос-'дай' спускается до листа, данные поднимаются как ответы. Инициатива у верхнего оператора.
DuckDB выбирает другое
PUSH (DuckDB): источник толкаетУправление снизу вверх: источник сам читает DataChunk и проталкивает его вверх по дереву операторов. Инициатива у источника данных.

Почему push лучше для DuckDB

Push-модель выбрана не случайно — у неё есть конкретные преимущества для аналитического векторизованного движка.

Первое — параллелизм. В pull-модели управление переплетено по всему дереву: каждый оператор активно дёргает дочерние. Распараллелить такое дерево трудно — синхронизация нужна повсюду. В push-модели поток данных однонаправленный — снизу вверх. Это сильно упрощает параллельное исполнение: разные DataChunk-и можно толкать через дерево независимо, разными потоками. Параллельной осведомлённости требуют в основном только концы конвейера — источник и приёмник, — а промежуточные операторы просто обрабатывают то, что в них толкнули. На этой модели строится morsel-driven параллелизм DuckDB (отдельный модуль курса).

Второе — естественность для конвейера операторов. Push-модель ложится на понятие pipeline — линейной цепочки операторов от источника до приёмника. Чанк проталкивается через всю цепочку, потом следующий. Меньше накладных расходов на «протокол запросов» между операторами: нет постоянного обмена «дай — на» вверх-вниз, есть однонаправленный поток.

Третье — управление потоком и буферизация. Когда данные толкаются вверх, оператору-приёмнику проще контролировать темп и буферизовать; конвейер естественно делится на стадии в местах, где данные нужно накопить (например, построить хеш-таблицу для join).

NOTE

Pull-модель Volcano — это не «плохая» модель: она проста, элегантна и десятилетиями была стандартом, она и сейчас работает в множестве СУБД. Но она проектировалась для построчной обработки и однопоточного исполнения. Современный аналитический движок — векторизованный и многопоточный, и для него push-модель подходит лучше: однонаправленный поток DataChunk-ов проще параллелить и дешевле в накладных расходах. DuckDB выбрал push именно поэтому.

Push-модель и векторизация работают в паре. Векторизация определяет, ЧТО движется по дереву — DataChunk-и примерно по 2048 строк. Push-модель определяет, КАК они движутся — проталкиваются снизу вверх источником. Вместе они дают аналитический движок, который и эффективно использует процессор (плотные векторные циклы), и легко масштабируется на много ядер (однонаправленный параллелизуемый поток).

Итог урока. Запрос проходит четыре стадии: Parser (текст в синтаксическое дерево, только синтаксис, без каталога), Binder (разрешение имён по каталогу и определение типов, здесь ловятся ошибки несуществующих таблиц), Logical Planner плюс Optimizer (дерево логических операторов и его переписывание — pushdown, порядок join), Physical Planner (конкретные алгоритмы, исполняемое дерево). По готовому физическому плану DataChunk-и движутся в push-модели — источник толкает их снизу вверх, в отличие от pull-модели Volcano, где корень тянет данные сверху. Push выбран ради параллелизма, естественности конвейера и низких накладных расходов.


Попробуй сам

Задания на наблюдение через EXPLAIN.

  1. Выполните EXPLAIN SELECT region, sum(amount) FROM sales GROUP BY region (создав сначала таблицу sales). В выводе вы увидите физический план — дерево физических операторов. Найдите оператор сканирования и оператор агрегации.
  2. Сравните EXPLAIN (логический и физический план) и EXPLAIN ANALYZE (с реальными счётчиками выполнения) на одном запросе. Какой из них показывает, сколько строк реально прошло через каждый оператор?
  3. Намеренно напишите запрос с несуществующей таблицей (SELECT * FROM no_such_table). На какой из четырёх стадий возникнет ошибка — на парсере или на биндере? Объясните почему.
  4. Намеренно напишите синтаксически сломанный запрос (SELECT FROM WHERE). На какой стадии возникнет ошибка? Чем эта ошибка отличается от ошибки из задания 3?
  5. Объясните своими словами, почему push-модель проще распараллелить, чем pull-модель Volcano.

Проверка знанийKnowledge check
Какие четыре стадии проходит запрос от SQL-текста до физического плана, и чем push-модель исполнения отличается от pull-модели Volcano?
ОтветAnswer
Запрос проходит четыре стадии преобразования. Parser превращает текст SQL в синтаксическое дерево по грамматике (DuckDB использует грамматику PostgreSQL); он проверяет только синтаксис и НЕ знает ничего о содержимом базы — запрос с несуществующей таблицей пройдёт парсер, если синтаксис верен. Binder сводит запрос с реальностью: обращается к каталогу (реестру таблиц, колонок, типов, функций) и привязывает имена из запроса к реальным объектам — он делает разрешение имён (здесь возникают ошибки 'нет такой таблицы' или 'нет такой колонки', не в парсере) и определение типов каждого выражения. Конструкции friendly SQL вроде COLUMNS() и GROUP BY ALL разворачиваются именно на биндинге, потому что им нужен каталог. Logical Planner строит дерево логических операторов (LogicalOperator) — ЧТО надо сделать в терминах реляционной алгебры, без указания КАК; затем Optimizer несколькими проходами переписывает логический план в эквивалентный, но эффективнее (filter pushdown, projection pushdown, порядок join, упрощение выражений). Physical Planner превращает оптимизированный план в физический — дерево PhysicalOperator, где абстрактные операции получают конкретные алгоритмы (логический join становится HASH JOIN); это исполняемое дерево. По физическому плану данные движутся, и здесь push-модель отличается от pull. В классической pull-модели (итераторы Volcano) управление идёт сверху вниз: корневой оператор запрашивает данные у дочернего, запрос-'дай' спускается до листа-сканирования, данные поднимаются обратно как ответы; инициатива у верхнего оператора. В push-модели DuckDB управление идёт снизу вверх: источник (сканирование) сам читает DataChunk и проталкивает его вверх по дереву, следующий оператор обрабатывает и толкает результат ещё выше; DataChunk-и движутся снизу вверх, проталкиваемые источником. Push выбран ради трёх вещей: параллелизма (однонаправленный поток данных легко параллелить, разные чанки толкаются разными потоками независимо — на этом строится morsel-driven параллелизм, а параллельной осведомлённости требуют в основном концы конвейера), естественности для конвейера-pipeline (меньше накладных расходов, нет постоянного обмена 'дай-на'), и удобства управления потоком и буферизацией. Push-модель и векторизация работают в паре: векторизация определяет ЧТО движется (DataChunk-и), push — КАК (толкаются снизу вверх).

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Чем стадия Parser отличается от стадии Binder в обработке запроса?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 7