Pushdown: predicate, projection, aggregation
Самая дешёвая работа — та, которую не пришлось делать. Если вы джойните таблицу из PostgreSQL и вам нужны только три столбца из тридцати и только строки за последний день, бессмысленно тянуть из PostgreSQL всю таблицу целиком, а потом отбрасывать 90% столбцов и 99% строк уже в Trino. Лучше попросить PostgreSQL отдать сразу только нужное.
Это и есть pushdown — проталкивание части работы из Trino вниз, в источник данных. Pushdown — один из главных рычагов производительности Trino, особенно в федеративных запросах. Этот урок — про три вида pushdown (predicate, projection, aggregation), про то, как они работают через SPI, и почему pushdown зависит от коннектора.
Идея pushdown: считать ближе к данным
Без pushdown модель работы Trino простая: коннектор отдаёт сырые данные таблицы, Trino применяет к ним фильтры, проекции, агрегации своими операторами на воркерах. Корректно, но расточительно: по сети из источника тащится всё, включая то, что тут же будет отброшено.
Pushdown переворачивает это там, где возможно: часть операций Trino передаёт источнику, и источник применяет их сам, до того как данные уйдут в Trino. Выигрыш двойной. Меньше данных едет по сети из источника в Trino. И часть вычислений делает сам источник — а он часто к этой работе приспособлен лучше: у реляционной СУБД есть индексы, у Parquet-файла — статистика по row group’ам.
Почему pushdown особенно важен для федерации. Когда источник — внешняя СУБД, канал между ней и Trino — это сеть, и она легко становится бутылочным горлышком. Протолкнуть в СУБД фильтр, отсекающий 99% строк, означает прогнать по сети в 100 раз меньше данных. Для федеративных запросов pushdown — часто разница между «секунды» и «минуты».
Predicate pushdown: проталкивание фильтра
Predicate pushdown — проталкивание условия WHERE в источник. Вместо «отдай все строки, Trino сам отфильтрует» Trino говорит источнику: «отдай только строки, удовлетворяющие этому условию».
SELECT name, email
FROM postgresql.public.users
WHERE country = 'DE' AND created_at >= DATE '2026-01-01';
С predicate pushdown условие country = 'DE' AND created_at >= ... уходит в PostgreSQL — он применит его сам, возможно используя индекс по country, и вернёт Trino уже отфильтрованный набор. Без pushdown PostgreSQL отдал бы всю таблицу users, а Trino выбросил бы всех не-немцев уже у себя, впустую прогнав их по сети.
Predicate pushdown работает не только в РСУБД. В файловых форматах он включает отсечение на уровне метаданных: для Parquet и ORC коннектор по статистике row group’ов (min/max столбцов) пропускает целые блоки, которые заведомо не содержат строк под условие. А для партиционированных таблиц predicate pushdown по партиционирующему столбцу даёт partition pruning — целые партиции не читаются вовсе.
Parquet: Metadata и Statistics ClickHouse: PREWHERE — фильтрация до чтенияProjection и dereference pushdown: проталкивание проекции
Projection pushdown — проталкивание выбора столбцов. Trino сообщает источнику, какие именно столбцы нужны, чтобы тот не отдавал остальные.
SELECT user_id, email
FROM postgresql.public.users;
Из тридцати столбцов таблицы users запросу нужны два. С projection pushdown Trino просит у PostgreSQL только user_id и email — остальные 28 столбцов по сети не едут. В колоночных форматах (Parquet, ORC) projection pushdown особенно силён: колоночное хранение позволяет физически прочитать с диска только нужные столбцы, не касаясь остальных, — это и есть главное преимущество колоночных форматов, и pushdown даёт Trino им воспользоваться.
Частный, более тонкий случай — dereference pushdown. Он касается вложенных структур ROW. Если столбец — структура с десятью полями, а запросу нужно одно вложенное поле, dereference pushdown проталкивает в источник обращение именно к этому полю:
SELECT event.user.country
FROM iceberg.analytics.events;
Здесь event — вложенная структура ROW. Dereference pushdown означает: коннектор прочитает из Parquet только путь event.user.country, не материализуя весь объект event. Мы упоминали эту оптимизацию во втором модуле, когда говорили про типизированный ROW — вот где она работает. Условие dereference pushdown — статически известная схема ROW: движок знает раскладку полей и может попросить ровно нужное.
Aggregation pushdown: проталкивание агрегации
Aggregation pushdown — самый продвинутый вид. Он проталкивает в источник саму агрегацию: count, sum, avg, GROUP BY.
SELECT country, count(*)
FROM postgresql.public.users
GROUP BY country;
Без aggregation pushdown Trino вытянул бы из PostgreSQL все строки users и сгруппировал их своими операторами. С aggregation pushdown в PostgreSQL уходит сам SELECT country, count(*) ... GROUP BY country — PostgreSQL выполняет группировку и возвращает Trino уже готовый компактный результат: по строке на страну. Если в таблице сто миллионов пользователей и двести стран, по сети едут двести строк вместо ста миллионов — выигрыш на порядки.
Aggregation pushdown потому и самый продвинутый, что агрегация — операция сложнее фильтра или проекции, и не каждый источник способен принять её в произвольном виде. Источник может поддерживать pushdown простых агрегатов и не поддерживать сложных; коннектор может реализовать этот вид pushdown частично или не реализовать совсем.
Pushdown работает через SPI и зависит от коннектора
Теперь принципиальное — почему pushdown не «всегда есть». Pushdown — это диалог между движком и коннектором, и ведётся он через Connector SPI.
Механика такая. Оптимизатор Trino, дойдя до операции, которую в принципе можно протолкнуть, обращается к коннектору через специальные методы SPI — концептуально это applyFilter, applyProjection, applyAggregation. Коннектор отвечает: «эту операцию я приму на себя» — целиком, частично или «не приму, считай сам». Если коннектор принял — операция уходит в источник; если нет — её выполняют операторы Trino, как обычно.
Отсюда два следствия. Первое: pushdown — свойство коннектора, а не движка. Один коннектор реализует все три вида pushdown, другой — только predicate, третий — никакого. Объём поддержки нужно знать для конкретного коннектора. Второе: pushdown может быть частичным. Коннектор вправе принять половину сложного WHERE (то, что умеет протолкнуть) и вернуть остальное Trino — тогда часть условия проверит источник, часть доделает Trino.
Как увидеть pushdown в плане
Сработал pushdown или нет — прямо видно в EXPLAIN. Главный признак: исчезновение отдельных операторов из плана. Когда фильтр протолкнут, в плане нет отдельного оператора Filter — условие «вшито» в узел TableScan источника. Когда фильтр не протолкнут — TableScan отдаёт сырые строки, а над ним стоит отдельный Filter.
EXPLAIN
SELECT name FROM postgresql.public.users WHERE country = 'DE';
Если predicate pushdown сработал, в плане у TableScan будет указано само условие (в выводе видна конструкция вроде constraint или подзапрос с уже включённым WHERE), а отдельного Filter-узла не будет. Если не сработал — увидите TableScan без условия плюс отдельный Filter[country = 'DE'] над ним. То же для агрегации: pushdown сработал — Aggregate-узел исчезает из плана Trino, агрегация ушла в источник; не сработал — Aggregate стоит в плане. Чтение EXPLAIN именно с этой стороны — «какие операторы пропали» — основной способ проверить, что pushdown работает.
Привыкайте читать EXPLAIN федеративных запросов через вопрос «что исчезло из плана». Если WHERE по внешней СУБД дал в плане отдельный Filter, а не вшился в TableScan, — predicate pushdown не сработал, и по сети, скорее всего, едут лишние строки. Если GROUP BY оставил в плане Aggregate-узел над сканом внешнего источника — агрегация считается в Trino, а не в источнике. Это сигнал: либо коннектор не поддерживает нужный вид pushdown, либо условие записано так, что протолкнуть его нельзя. Pushdown — то, что стоит проверять в первую очередь, когда федеративный запрос медленный.
Попробуй сам
На песочнице курса (Trino 481):
-
Возьмите
EXPLAIN SELECT orderkey FROM tpch.sf1.orders WHERE orderstatus = 'F';. Найдите узелTableScan/ScanFilterи определите, вшито ли условиеorderstatus = 'F'в скан или стоит отдельнымFilter-узлом. Сформулируйте, сработал ли predicate pushdown. -
Сравните два плана:
EXPLAIN SELECT * FROM tpch.sf1.orders;иEXPLAIN SELECT orderkey, custkey FROM tpch.sf1.orders;. Посмотрите, как в плане отражается, что во втором случае запрашиваются только два столбца. Объясните, какой вид pushdown тут работает и почему для колоночных данных он особенно ценен. -
Рассуждение в двух абзацах. Первый: почему aggregation pushdown в федеративном запросе
SELECT country, count(*) FROM external_db.users GROUP BY countryможет ускорить запрос на порядки — распишите, сколько строк едет по сети с pushdown и без. Второй: почему наличие этого ускорения не гарантировано и от чего оно зависит.