Learning Platform
Глоссарий Troubleshooting
Урок 17.04 · 25 мин
Продвинутый
federationpostgresqlpushdowndata-marts

Федеративный слой: подключение PostgreSQL и витрины

В уроке постановки мы приняли решение: справочники RetailScope остаются в PostgreSQL и не копируются в lakehouse. Теперь это решение реализуем. В этом уроке мы подключаем PostgreSQL к Trino как каталог, соединяем Iceberg-факты со справочниками одним SQL-запросом и строим полноценные денормализованные витрины. Это и есть федеративный слой — то, ради чего в архитектуру взяли именно Trino.

SQL: EXPLAIN в PostgreSQL — смотрим, что Trino протолкнул в базу

Главное, что покажет урок: один SELECT обращается к двум совершенно разным хранилищам — object storage и реляционной базе — а Trino сам решает, какую часть работы отдать PostgreSQL, а какую посчитать сам.


Подключение PostgreSQL

В docker-compose.yml из урока развёртывания добавляем сервис базы:

  postgres:
    image: postgres:16
    environment:
      POSTGRES_USER: retail
      POSTGRES_PASSWORD: retail_pw
      POSTGRES_DB: refdata
    ports:
      - "5432:5432"
    volumes:
      - pg-data:/var/lib/postgresql/data

Чтобы Trino увидел базу, добавляем каталог. Файл etc/catalog/postgresql.properties — у координатора и у каждого воркера:

connector.name=postgresql
connection-url=jdbc:postgresql://postgres:5432/refdata
connection-user=retail
connection-password=retail_pw

Всё. После перезапуска SHOW CATALOGS покажет postgresql рядом с iceberg. Каталог — это всего лишь конфигурация подключения; коннектор postgresql адаптирует базу к модели Trino. Naming-маппинг прямой: схема PostgreSQL становится Trino-схемой, таблица — Trino-таблицей. Запрос SELECT * FROM postgresql.public.dim_product читает таблицу напрямую из PostgreSQL.

Для проекта наполним PostgreSQL справочниками — товары, магазины, категории:

-- Выполняется в PostgreSQL
CREATE TABLE dim_product (
  product_id   INTEGER PRIMARY KEY,
  product_name VARCHAR(120),
  category     VARCHAR(60),
  brand        VARCHAR(60)
);

CREATE TABLE dim_store (
  store_id   INTEGER PRIMARY KEY,
  store_name VARCHAR(120),
  region     VARCHAR(60),
  city       VARCHAR(60)
);

Федеративный запрос: один SQL, два хранилища

Теперь — суть. Соединяем raw_sales (Iceberg, факты в object storage) со справочниками (PostgreSQL) в одном запросе:

SELECT
  st.region,
  p.category,
  count(*)                                   AS sale_lines,
  sum(s.quantity * s.unit_price - s.discount) AS net_revenue
FROM iceberg.retail.raw_sales s
JOIN postgresql.public.dim_product p ON s.product_id = p.product_id
JOIN postgresql.public.dim_store   st ON s.store_id   = st.store_id
WHERE s.sale_ts >= TIMESTAMP '2026-01-01 00:00:00'
GROUP BY st.region, p.category
ORDER BY net_revenue DESC;

Один запрос, три таблицы из двух разных систем. Аналитику не нужно знать, что факты лежат в MinIO, а справочники в PostgreSQL, — для него это единое SQL-пространство. Это и есть query federation: Trino выступает единой точкой доступа поверх разнородных источников.

Федеративный запрос: что считается где
Скан raw_salesIceberg-таблица фактов в MinIO. Воркеры читают Parquet-файлы; фильтр по sale_ts даёт partition pruning.
Чтение справочниковdim_product и dim_store в PostgreSQL. Trino отправляет в базу SQL — только нужные колонки, по возможности с фильтром.
данные сходятся на воркерах Trino
Join в TrinoСоединение фактов и справочников исполняется на воркерах Trino: join разных источников движок всегда делает у себя.
агрегация и сортировка
РезультатАгрегированная выручка по региону и категории. Отдаётся клиенту через protocol API.

Pushdown: что Trino отдаёт PostgreSQL

Федерация была бы бесполезна, если бы Trino тащил справочники целиком и фильтровал сам. Поэтому работает pushdown — Trino отдаёт часть работы источнику, если коннектор это умеет. Коннектор postgresql поддерживает pushdown хорошо, потому что PostgreSQL — полноценная SQL-база.

Что Trino проталкивает в PostgreSQL:

  • Predicate pushdownWHERE уезжает в базу: PostgreSQL отдаёт уже отфильтрованные строки.
  • Projection pushdown — в базу уходит только список нужных колонок, лишние не читаются.
  • Aggregation pushdown — если запрос агрегирует только данные PostgreSQL, count/sum/GROUP BY могут посчитаться в самой базе.

Что Trino всегда делает сам: join данных из разных каталогов. Соединить Iceberg-факты с PostgreSQL-справочником в PostgreSQL невозможно — база не видит Iceberg. Такой join всегда исполняется на воркерах Trino.

Увидеть распределение работы помогает EXPLAIN. В плане федеративного запроса у скана PostgreSQL-таблицы виден проброшенный в источник запрос:

Fragment 3 [SOURCE]
    TableScan[table = postgresql:public.dim_store]
        region := region:varchar
        store_id := store_id:integer
        -- запрос, реально отправленный в PostgreSQL:
        -- SELECT store_id, region FROM public.dim_store

В скане только store_id и region — projection pushdown сработал, остальные колонки dim_store не читаются. Если бы в запросе был фильтр только по справочнику (например, WHERE st.region = 'North'), он бы тоже ушёл в PostgreSQL отдельным WHERE.

TIP

Перед запуском незнакомого федеративного запроса снимите EXPLAIN и посмотрите, что уходит в источник. Если в PostgreSQL уходит SELECT без фильтров и со всеми колонками, а должен был с фильтром, — pushdown не сработал, и Trino вытянет всю таблицу по сети. Для маленького справочника это терпимо, для большой таблицы из federated-источника — серьёзная проблема производительности.


Что коннектор делает под капотом

Полезно понимать, что postgresql — это JDBC-коннектор, и через что он проходит, обслуживая федеративный запрос.

Коннектор реализует Connector SPI Trino: он переводит запросы движка в обращения к источнику и обратно. Для PostgreSQL это означает три рода работы. Первое — метаданные: когда Trino нужно узнать, какие в refdata есть схемы и таблицы и какого типа их колонки, коннектор спрашивает это у системного каталога PostgreSQL. Второе — маппинг типов: типы PostgreSQL не совпадают с типами Trino один в один, и коннектор сопоставляет их — integer PostgreSQL становится integer Trino, varchar остаётся varchar, numeric отображается в decimal, timestamp — в timestamp. Если у источника есть тип без точного аналога в Trino, коннектор подбирает ближайший или помечает колонку как непереносимую. Третье — трансляция запроса: ту часть работы, которую Trino решил протолкнуть (фильтр, проекцию, агрегацию), коннектор оформляет как валидный SQL-запрос на диалекте PostgreSQL и отправляет в базу через JDBC.

postgresql-коннектор как переводчик между Trino и базой
Движок TrinoРаботает в терминах своей модели: catalog, schema, table, типы Trino, план запроса.
Connector SPI
postgresql-коннекторПереводит: метаданные из системного каталога PostgreSQL, маппинг типов, трансляцию протолкнутой части в SQL диалекта PostgreSQL.
JDBC
PostgreSQLОбычная реляционная база. Получает валидный SQL-запрос и отдаёт строки, не зная, что обращается к ней Trino.

Для PostgreSQL сама база видит обычные SQL-запросы и даже не знает, что обращается к ней Trino — она просто отдаёт строки. Именно поэтому коннектор postgresql хорошо поддерживает pushdown: PostgreSQL — полноценная SQL-СУБД, и почти любую протолкнутую операцию она исполнит сама. У коннекторов к менее «умным» источникам pushdown беднее — это всегда зависит от того, что источник умеет.


Антипаттерн федерации

Федерация мощна, но её легко применить неправильно. Главный антипаттерн — join двух больших таблиц из разных federated-источников.

Представьте, что фактов было бы две гигантские таблицы в двух разных PostgreSQL-базах, и мы соединяем их в Trino. Pushdown тут не спасает: join разных источников всегда делается в Trino, а значит обе огромные таблицы целиком поедут по сети на воркеры. Сеть станет узким местом, и запрос будет медленным независимо от мощности кластера.

Почему RetailScope этого избегает: федерация в проекте — это всегда большие факты в Iceberg плюс маленькие справочники из PostgreSQL. Справочник мал — протащить его по сети дёшево. Факты велики, но они в Iceberg, который Trino читает эффективно: partition pruning, колоночное чтение, dynamic filtering. Перекос размеров — то, что делает федерацию RetailScope быстрой.

Сценарий федерацииОценка
Большие факты в Iceberg + маленький справочник из PostgreSQLХорошо: справочник дёшево протащить, факты Trino читает эффективно
Маленькая таблица из PostgreSQL + маленькая из MySQLНормально: объёмы малы, сеть не узкое место
Большая таблица из PostgreSQL + большая из другой базыАнтипаттерн: обе целиком едут по сети, сеть — bottleneck
Регулярный отчёт по большой federated-таблицеПлохо: лучше один раз загрузить её в Iceberg

Денормализованная витрина с федерацией

Теперь достроим витрину mart_sales_daily из прошлого урока — добавим в неё справочные атрибуты из PostgreSQL. Это полноценная денормализованная витрина:

CREATE OR REPLACE TABLE iceberg.retail.mart_sales_enriched
WITH (
  format = 'PARQUET',
  partitioning = ARRAY['month(sale_date)']
)
AS
SELECT
  CAST(s.sale_ts AS DATE)                      AS sale_date,
  st.region,
  st.store_name,
  p.category,
  p.brand,
  count(*)                                     AS sale_lines,
  sum(s.quantity)                              AS units_sold,
  sum(s.quantity * s.unit_price - s.discount)  AS net_revenue
FROM iceberg.retail.raw_sales s
JOIN postgresql.public.dim_product p ON s.product_id = p.product_id
JOIN postgresql.public.dim_store   st ON s.store_id   = st.store_id
GROUP BY 1, 2, 3, 4, 5;

Что здесь произошло. Витрина денормализована — region, store_name, category, brand «вшиты» прямо в таблицу. Она целиком в Iceberg: справочники из PostgreSQL прочитаны один раз в момент построения и зафиксированы в витрине. Дашборды теперь бьют по mart_sales_enriched — компактной, агрегированной, полностью в lakehouse — и не нагружают PostgreSQL на каждый запрос.

NOTE

Здесь видна роль федерации в проекте. Федерация работает в момент построения витрины: запрос денормализации соединяет lakehouse и PostgreSQL. Дашборды же читают уже готовую Iceberg-витрину — для них федерации нет, есть быстрая локальная таблица. Витрина пересобирается по расписанию оркестратором, и при пересборке справочные значения обновляются. Это сознательный компромисс: дашборды быстрые, а свежесть справочников в витрине определяется частотой пересборки.

Airflow: запуск денормализации витрины по расписанию через TrinoOperator dbt: денормализованная витрина как incremental-модель поверх Trino

Попробуй сам

Добавьте к платформе RetailScope федеративный слой.

  1. Добавьте сервис postgres в docker-compose.yml и каталог etc/catalog/postgresql.properties. Перезапустите и проверьте SHOW CATALOGS — виден ли postgresql.
  2. В PostgreSQL создайте dim_product и dim_store и наполните их (можно сгенерировать строки запросом или взять tpch.tiny.part и tpch.tiny.supplier как основу). Из Trino выполните SELECT count(*) FROM postgresql.public.dim_product.
  3. Запустите федеративный запрос выручки по региону и категории из урока. Он соединяет Iceberg и PostgreSQL в одном SQL.
  4. Снимите EXPLAIN этого запроса. Найдите фрагменты со сканом PostgreSQL-таблиц: какие колонки в них и какой SQL уходит в базу? Сработал ли projection pushdown?
  5. Добавьте в запрос WHERE st.region = 'North' и снова посмотрите EXPLAIN — ушёл ли этот фильтр в PostgreSQL отдельным условием?
  6. Постройте денормализованную витрину mart_sales_enriched. Сравните время запроса дашборда к ней с временем исходного федеративного запроса.

Цель — на практике увидеть, как один SQL работает поверх двух хранилищ, и как федерация при построении витрины уступает место быстрому локальному чтению при работе дашбордов.


Проверка знанийKnowledge check
В RetailScope федеративные запросы соединяют большие Iceberg-факты с маленькими справочниками из PostgreSQL и работают быстро. Почему такая федерация эффективна, но при этом join двух больших таблиц из двух разных federated-баз был бы антипаттерном — несмотря на то, что Trino поддерживает pushdown?
ОтветAnswer
Разница в том, что именно вынужден делать Trino, и pushdown эту разницу не отменяет. Pushdown позволяет Trino отдать источнику часть работы — фильтрацию (predicate pushdown), выбор колонок (projection pushdown), иногда агрегацию, — если коннектор это умеет; коннектор postgresql поддерживает это хорошо. Но есть операция, которую Trino всегда делает сам и которую невозможно протолкнуть: join данных из разных каталогов. PostgreSQL не видит Iceberg, другая база не видит первую — поэтому соединение разнородных источников всегда исполняется на воркерах Trino, а значит данные обеих соединяемых сторон должны физически прийти на воркеры по сети. Вот здесь и решает размер. В федерации RetailScope соединяются большие факты в Iceberg и маленькие справочники из PostgreSQL. Справочник мал, поэтому протащить его по сети на воркеры дёшево. Факты велики, но они лежат в Iceberg, который Trino читает очень эффективно: partition pruning отсекает ненужные дни, колоночное чтение берёт только нужные колонки, dynamic filtering сужает скан по join-ключу — по сети едет лишь действительно необходимая часть фактов. Перекос размеров — большое в эффективном Iceberg, маленькое из federated-источника — это то, что делает федерацию быстрой. Join же двух больших таблиц из двух разных federated-баз — антипаттерн именно потому, что pushdown не может протолкнуть сам join: обе огромные таблицы целиком поедут по сети на воркеры Trino, сеть станет узким местом, и запрос будет медленным независимо от мощности кластера. Pushdown сократит колонки и строки на каждой стороне, но не устранит необходимость свести обе большие стороны в Trino. Поэтому правило проекта такое: федерация хороша, когда хотя бы одна сторона join маленькая или когда большая сторона лежит в формате, который Trino читает эффективно; если же регулярно нужен join большой federated-таблицы, правильное решение — один раз загрузить её в Iceberg, а не федерировать на каждый запрос.

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

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

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

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

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

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