Trino: как ускорить SQL-запросы — архитектура, оптимизация JOIN, PlanNode и примеры

Trino — распределённый SQL-движок для аналитической обработки данных, который позволяет выполнять запросы сразу по нескольким источникам и параллельно обрабатывать большие объёмы информации. Его используют как единый SQL-слой над озёрами данных, реляционными СУБД, объектными хранилищами и другими системами. Но сам факт использования Trino ещё не гарантирует быстрые запросы: итоговая производительность зависит от того, какой план построил оптимизатор, сколько данных пришлось прочитать, сколько информации передать между узлами и где именно выполняются операции.

Оптимизация — это не только переписывание SQL. Trino изучает разные пути выполнения запроса и выбирает оптимальный. Для многих сценариев уже есть готовые шаблоны

Поэтому оптимизация Trino — это не только переписывание SQL. Необходимо понимать, как запрос превращается в план выполнения, какие операции выполняются на координаторе и рабочих узлах, каким образом Trino выбирает порядок JOIN, когда фильтр передаётся непосредственно источнику данных и почему один и тот же SQL может выполняться с совершенно разной эффективностью в зависимости от статистики таблиц.

В этой статье разберём внутреннюю логику Trino: от архитектуры и PlanNode до правил оптимизатора, cost-based optimization, распределения JOIN и pushdown. Отдельно рассмотрим практические ситуации — параллельную обработку, кэширование промежуточных результатов, анализ продаж в самый нагруженный день, поиск проблемного JOIN и перенос условий непосредственно в источник данных.

Что такое Trino

Trino — распределённый SQL query engine, предназначенный для выполнения аналитических запросов над данными, которые могут находиться в разных системах. В отличие от классической СУБД, Trino не обязан владеть физическим хранилищем данных: он подключается к источникам через коннекторы и выполняет над ними единый SQL-запрос.

Архитектурно Trino разделяет планирование и значительную часть исполнения между coordinator и worker nodes. Координатор принимает SQL, анализирует его, строит и оптимизирует распределённый план, а рабочие узлы выполняют задачи, читают данные из источников и обрабатывают промежуточные результаты.

Ключевая идея Trino: SQL-запрос является только исходным описанием задачи. Реальная производительность определяется тем, во что оптимизатор превратит этот SQL перед выполнением.

Как появился

История Trino начинается с проекта Presto, который в 2012 году создала команда Facebook. Целью было построить высокопроизводительный SQL-движок для интерактивной аналитики по большим объёмам данных, в том числе поверх существующей Hadoop-инфраструктуры. В 2013 году Presto был опубликован как open-source проект.

Изначальная задача хорошо объясняет современную архитектуру Trino. Команде был нужен не очередной универсальный storage engine, а отдельный слой выполнения SQL, который мог бы быстро работать с уже существующими хранилищами. Поэтому принцип отделения вычислений от хранения оказался одним из фундаментальных для проекта.

В 2019 году вокруг проекта была создана независимая Presto Software Foundation. Позднее разработчики, продолжившие развитие этой ветки, использовали название PrestoSQL, а в декабре 2020 года проект был переименован в Trino.

Зачем нужен

Главная практическая ценность Trino для корпоративной ИТ-архитектуры заключается в возможности предоставить пользователям единый SQL-интерфейс к разнородным данным. При этом не обязательно сначала физически собрать все данные в одну базу.

Например, в одном запросе можно работать с историческими данными в Data Lake, справочниками в реляционной СУБД и отдельным аналитическим источником. Trino предоставляет слой, который связывает эти системы на уровне SQL и выполняет распределённую обработку.

Для CIO и ИТ-руководителя это означает возможность рассматривать Trino как часть аналитической платформы:

  • Data Lake / Data Lakehouse — запросы к большим массивам файловых данных;
  • реляционные СУБД — подключение операционных и справочных систем;
  • объектные хранилища — работа с данными в S3-подобных системах;
  • BI — предоставление SQL-доступа к данным для отчётности;
  • Data Science — подготовка выборок для аналитики и машинного обучения;
  • федеративная аналитика — объединение информации из нескольких источников в одном запросе.

Однако федеративность одновременно является источником потенциальных проблем с производительностью. Если запрос заставляет Trino читать сотни гигабайт из удалённой СУБД, пересылать большие объёмы между worker nodes или строить огромную hash-таблицу для JOIN, SQL может оказаться дорогим независимо от того, насколько хорошо написана его синтаксическая часть.

Архитектура

Чтобы понимать оптимизацию Trino, необходимо сначала разделить три уровня: SQL-запрос, план выполнения и распределённое исполнение. Trino принимает SQL, анализирует его, формирует план, разбивает выполнение на стадии и распределяет работу между узлами кластера.

Распределённый план показывает не только последовательность логических операций, но и места, где данные должны перемещаться между узлами. В документации Trino такие участки представлены через fragments и exchanges, например HASH, BROADCAST, ROUND_ROBIN и SINGLE.

Координатор

Coordinator отвечает за управление запросом. Он принимает SQL от клиента, запускает parsing и analysis, обращается к metadata, строит логический и распределённый планы, применяет оптимизации и организует выполнение задач на worker nodes.

На этапе планирования Trino получает информацию о структуре таблиц, типах данных, доступных источниках и статистике. Затем оптимизатор пытается построить такой план, который позволит уменьшить объём вычислений, памяти и сетевого обмена.

Координатор не следует понимать как «сервер, на котором выполняется весь SQL». Это принципиальное отличие. Основная обработка больших объёмов данных распределяется между worker nodes, а coordinator управляет построением и выполнением распределённого плана.

При этом coordinator остаётся критическим компонентом инфраструктуры. Слишком большое количество одновременно планируемых запросов, сложные планы, большое число JOIN или высокая нагрузка на управление split/task могут создавать давление на координатор даже тогда, когда worker nodes ещё имеют свободные ресурсы.

Рабочие узлы

Worker nodes выполняют основную вычислительную работу. Они получают задачи, читают данные через соответствующие коннекторы, применяют фильтры, выполняют агрегации, JOIN, сортировки и другие операции, а затем передают промежуточные результаты следующим стадиям плана.

Trino делит распределённое выполнение на stages, а внутри них использует tasks и splits. Split можно рассматривать как адресуемую часть данных, которую необходимо обработать конкретной задачей. Такая модель позволяет распределять чтение и вычисления между множеством worker nodes.

Для производительности это означает, что нужно смотреть не только на объём данных, но и на характер их распределения. Если одна часть данных значительно больше остальных, отдельные workers могут оказаться перегружены, пока другие уже завершили свою работу. Поэтому равномерность распределения, количество splits и объём shuffle имеют не меньшее значение, чем число CPU.

Компонент Основная функция Что важно для производительности
Coordinator Анализ, планирование, оптимизация, управление запросом Сложность планов, количество запросов, metadata, управление tasks
Worker Чтение и обработка данных CPU, RAM, I/O, количество splits, skew
Connector Связь Trino с источником данных Pushdown, статистика, скорость источника, сетевые задержки
Exchange Передача данных между стадиями и узлами Объём network traffic и способ распределения данных

Как работает оптимизация SQL-запросов в Trino

Оптимизация в Trino начинается ещё до непосредственного чтения данных. SQL проходит через parsing и analysis, после чего строится план, который затем последовательно преобразуется различными оптимизаторами. В результате исходная конструкция SQL может превратиться в совершенно другую последовательность операций, сохраняя при этом логический результат.

В современном Trino оптимизация включает как rule-based transformations, так и cost-based optimization. Последняя использует статистику таблиц и колонок, чтобы оценивать предполагаемую стоимость разных вариантов выполнения.

Оптимизация SQL в Trino — это поиск не «красивого» SQL, а более дешёвого плана выполнения.

PlanNode — это дерево реляционных операторов

Внутреннее представление запроса можно рассматривать как дерево PlanNode, где каждый узел представляет определённую операцию над данными. Один узел может получать данные от одного или нескольких дочерних узлов и передавать результат следующей операции.

Например, простой запрос может концептуально выглядеть так:

Output
  └── Project
       └── Filter
            └── TableScan

Более сложный запрос с JOIN и агрегацией будет представлен уже значительно более глубоким деревом:

Output
  └── Aggregate
       └── Join
          ├── Filter
          │    └── TableScan orders
          └── Filter
               └── TableScan customers

Оптимизатор применяет преобразования к таким поддеревьям. В исходном коде Trino набор планировщиков содержит большое количество правил для pruning колонок, pushdown фильтров и проекций, преобразования JOIN, агрегаций, LIMIT, TopN и других операций.

Основные операторы

В плане Trino можно встретить несколько базовых типов операций:

  • TableScan — чтение данных из источника;
  • Filter — фильтрация строк;
  • Project — формирование и вычисление выражений и колонок;
  • Join — объединение наборов данных;
  • Aggregate — группировка и вычисление агрегатов;
  • Sort / TopN — сортировка и выбор верхней части результата;
  • Exchange — распределение данных между стадиями;
  • Window — оконные вычисления.

Особенно важны TableScan, Join и Exchange. TableScan показывает, сколько данных необходимо извлечь из источника. Join может потребовать значительного объёма памяти и сетевого обмена. Exchange показывает, где данные должны перейти между стадиями распределённого выполнения.

Именно поэтому при анализе медленного запроса недостаточно смотреть только на SQL-текст. Намного полезнее посмотреть, какие PlanNode фактически получил оптимизатор и сколько данных проходит через каждый из них.

Встроенные правила Rule и паттерны

Trino использует систему правил, которые сопоставляются с определёнными структурами плана. Упрощённо можно представить Rule как преобразование вида: «если в дереве найден такой тип поддерева и выполнены определённые условия, его можно заменить эквивалентным, но более эффективным вариантом».

Каждое правило работает с определённым pattern. Это позволяет оптимизатору не переписывать весь план целиком, а находить конкретные участки, подходящие под условие правила.

Например, если план содержит Filter поверх Project, оптимизатор может изменить структуру дерева так, чтобы фильтр оказался ниже проекции. Если определённые колонки больше не нужны, они могут быть удалены из плана. Если LIMIT можно передвинуть ближе к источнику, количество обрабатываемых строк может уменьшиться.

В исходном коде Trino присутствуют отдельные правила для predicate pushdown, projection pushdown, column pruning, limit pushdown, simplification expressions, join reordering и множества других преобразований.

Основные правила

Тип оптимизации Что меняется Практический эффект
Predicate pushdown Фильтр перемещается ближе к источнику Меньше строк читается и передаётся
Projection pushdown В источник передаются только необходимые колонки Снижается объём I/O и сети
Column pruning Удаляются ненужные поля из плана Снижается объём обрабатываемых данных
Limit / TopN pushdown Ограничение результата приближается к источнику Можно существенно сократить чтение
Join reordering Изменяется порядок соединения таблиц Уменьшается промежуточный объём данных
Join distribution Выбирается способ распределения JOIN Оптимизируется network и memory

При этом правило не означает безусловную оптимизацию. Например, перенос JOIN в источник имеет смысл только при наличии соответствующей поддержки коннектора. Pushdown зависит от конкретного источника данных и возможностей его Trino connector.

Оптимизация JOIN

JOIN — одна из наиболее важных операций с точки зрения производительности Trino. Причина проста: соединение больших наборов данных может потребовать не только вычислительных ресурсов, но и существенного объёма памяти и передачи данных между worker nodes.

Если один JOIN создаёт промежуточный результат значительно большего размера, все следующие стадии могут быть вынуждены обрабатывать этот объём. Поэтому изменение порядка JOIN способно принципиально изменить время выполнения одного и того же SQL-запроса.

Trino использует статистику таблиц для cost-based выбора порядка соединения. Оптимизатор оценивает различные варианты и выбирает порядок с меньшей расчётной стоимостью.

Пример 1. Маленькая таблица и большая таблица

Предположим, есть таблица фактов на несколько миллиардов строк и небольшая таблица клиентов. Если небольшую таблицу можно эффективно использовать как build side, JOIN может быть значительно дешевле, чем вариант, в котором огромный набор данных становится основой промежуточного результата.

SELECT
    o.order_id,
    o.amount,
    c.segment
FROM orders o
JOIN customers c
    ON o.customer_id = c.customer_id
WHERE c.segment = 'VIP';

Важна не только запись JOIN, но и то, какой объём таблицы customers реально попадёт в соединение. Если фильтр по VIP можно применить заранее, размер build side уменьшается.

Пример 2. Порядок нескольких JOIN

Рассмотрим цепочку из четырёх таблиц:

orders
  JOIN customers
  JOIN products
  JOIN stores

Если первым соединить две огромные таблицы, можно получить очень большой промежуточный набор. Если же сначала применить селективные фильтры и соединить небольшие результаты, последующие JOIN будут работать с меньшим объёмом данных.

Именно поэтому для Trino критичны статистики таблиц и колонок. Они позволяют оценивать количество строк, размер данных, долю NULL, количество уникальных значений и диапазон значений колонок.

Пример 3. Broadcast и распределённый JOIN

В распределённой архитектуре JOIN может выполняться с разными способами распределения данных. При broadcast небольшая сторона соединения может быть отправлена на worker nodes, чтобы избежать большого перераспределения обеих таблиц.

При больших таблицах может использоваться partitioned join, когда данные распределяются между worker nodes по ключу соединения. Выбор зависит от размеров сторон JOIN, статистики, доступной памяти и других характеристик плана.

Если данные распределены неравномерно, появляется проблема data skew: отдельный ключ может соответствовать огромному количеству строк. Тогда один или несколько workers получают непропорционально большую нагрузку, и увеличение числа узлов не обязательно решает проблему.

Пример 4. Dynamic filtering

Для JOIN Trino может использовать dynamic filtering: значения, полученные на build side соединения, используются для сокращения чтения probe side. В некоторых сценариях это позволяет не читать части данных, которые заведомо не смогут попасть в результат.

Dynamic filtering развивается вместе с динамическим partition pruning: собранные значения могут использоваться для пропуска ненужных партиций источника. Это особенно важно для больших таблиц, где небольшая таблица задаёт очень ограниченное множество ключей.

Примеры

Рассмотрим несколько типовых ситуаций, с которыми сталкивается команда, эксплуатирующая Trino в корпоративной аналитической платформе. Во всех случаях принцип один: сначала определить, где возникает основной объём работы, затем изменить план или архитектуру обработки так, чтобы этот объём уменьшился.

Для диагностики полезно начинать с EXPLAIN и EXPLAIN ANALYZE. Первый позволяет посмотреть план, а второй показывает фактическое распределённое выполнение и статистику операций, включая CPU, input/output и характеристики отдельных plan nodes.

Практическое правило: прежде чем оптимизировать SQL, посмотрите план. Иначе есть риск оптимизировать текст запроса, а не реальную причину задержки.

Параллельная обработка

Предположим, необходимо обработать несколько миллиардов событий за сутки. Наивный подход — запустить один тяжёлый запрос и ожидать, что Trino автоматически решит проблему за счёт количества worker nodes.

На практике важно, чтобы источник данных позволял достаточно эффективно разделить работу на splits. Если данные физически представлены небольшим числом крупных объектов или распределены неравномерно, добавление worker nodes может дать значительно меньший эффект, чем ожидается.

Пример аналитического запроса:

SELECT
    event_type,
    count(*) AS events,
    sum(amount) AS total_amount
FROM events
WHERE event_date >= DATE '2026-09-01'
  AND event_date < DATE '2026-09-02'
GROUP BY event_type;

Если таблица физически организована по дате, фильтр может существенно уменьшить количество читаемых данных. Если же дата является только логическим полем внутри огромных файлов без эффективной организации хранения, Trino всё равно может быть вынужден просмотреть значительную часть источника.

Поэтому при проектировании Data Lake необходимо учитывать не только SQL-модель, но и физическую организацию данных: партиционирование, размер файлов, формат хранения, статистику и возможности predicate pushdown.

Кэширование промежуточных итогов

Не каждый запрос необходимо выполнять «с нуля». Если одни и те же тяжёлые вычисления повторяются десятки или сотни раз, имеет смысл вынести их в предварительно рассчитанный слой — например, материализованное представление, агрегированную таблицу или отдельный набор данных.

Предположим, BI-система постоянно рассчитывает продажи по дням, регионам и товарным категориям из многолетнего факта. Каждый новый запрос повторно читает огромный объём исторических данных.

SELECT
    sale_date,
    region,
    category,
    SUM(amount) AS revenue
FROM sales
GROUP BY
    sale_date,
    region,
    category;

Если этот расчёт нужен постоянно, архитектурно разумнее рассмотреть предварительную агрегацию:

daily_sales
----------------------------
sale_date
region
category
revenue
orders_count

После этого BI-запрос работает уже не с миллиардами строк исходного факта, а с существенно меньшим агрегированным набором.

Важно различать кэширование результата и оптимизацию плана. Кэш не делает конкретный запрос быстрее сам по себе — он позволяет избежать повторного выполнения дорогой работы. Для часто повторяющихся аналитических запросов это может дать больший эффект, чем локальное изменение SQL.

Продажи в самый нагруженный день

Рассмотрим типичный запрос: аналитик хочет найти день с максимальным количеством заказов, а затем посмотреть детальную статистику по нему.

SELECT
    order_date,
    COUNT(*) AS orders_count,
    SUM(amount) AS revenue
FROM orders
GROUP BY order_date
ORDER BY orders_count DESC
LIMIT 1;

Здесь потенциально интересна оптимизация TopN. Вместо полноценной сортировки огромного результата агрегирования движок может использовать специализированную обработку TopN. Возможность протолкнуть TopN ближе к источнику зависит от конкретного плана и connector capabilities.

Если после определения дня необходимо получить детальные данные, имеет смысл разделить задачу на две стадии: сначала получить небольшой набор ключей, а затем выполнить запрос по конкретному дню. Такой подход особенно полезен, если первая стадия резко сокращает объём данных для второй.

Но важно не превращать это в универсальное правило. Если источник уже предоставляет эффективную агрегацию или статистику, а optimizer способен построить более дешёвый план автоматически, ручное разделение запроса может оказаться избыточным.

Анализ JOIN

Допустим, запрос неожиданно стал выполняться 15 минут вместо обычных двух. Первое действие — не переписывать JOIN вслепую, а выполнить:

EXPLAIN ANALYZE
SELECT
    o.order_id,
    c.customer_name,
    o.amount
FROM orders o
JOIN customers c
    ON o.customer_id = c.customer_id
WHERE o.order_date >= DATE '2026-09-01';

EXPLAIN ANALYZE показывает фактические характеристики выполнения plan nodes. В частности, можно увидеть объём входных и выходных данных, CPU time, scheduled time и признаки неравномерности распределения.

Для JOIN стоит проверить несколько показателей:

  • какая сторона является build side;
  • сколько строк реально приходит на JOIN;
  • какой объём данных передаётся через Exchange;
  • не отличается ли фактический объём данных от оценок optimizer;
  • нет ли data skew;
  • достаточны ли статистики таблиц;
  • можно ли применить dynamic filtering;
  • может ли JOIN быть выполнен непосредственно источником.

Если оценки сильно расходятся с реальностью, одной из первых проверок должны стать статистики. Trino поддерживает сбор статистики через ANALYZE, хотя конкретные возможности зависят от connector и источника.

Перенос условия в источник

Одна из наиболее эффективных оптимизаций в федеративной архитектуре — pushdown. Его смысл заключается в том, чтобы выполнить часть операции не внутри Trino, а непосредственно в подключённой системе.

Например, вместо ситуации:

Источник
   ↓
миллионы строк
   ↓
Trino
   ↓
WHERE status = 'ACTIVE'
   ↓
результат

желательно получить:

Источник
   ↓
WHERE status = 'ACTIVE'
   ↓
только нужные строки
   ↓
Trino

При predicate pushdown источник сам выполняет фильтрацию, благодаря чему в Trino передаётся меньше данных. Это сокращает I/O, сетевой обмен и нагрузку на сам движок.

Аналогичный принцип может работать для projection pushdown: если запросу нужны только три поля из таблицы со ста колонками, нет смысла читать все сто. Ещё интереснее ситуация с JOIN pushdown: если connector способен выполнить соединение непосредственно в исходной СУБД, Trino может получить уже уменьшенный результат вместо самостоятельного чтения обеих таблиц.

Однако pushdown не является магическим механизмом. Его возможности зависят от конкретного connector. Например, для разных СУБД набор поддерживаемых операций различается. Проверять фактический pushdown необходимо через EXPLAIN: если операция действительно ушла в источник, структура плана будет это отражать.

Как системно ускорять Trino в корпоративной среде

Оптимизация Trino наиболее эффективна тогда, когда рассматривается не как набор отдельных SQL-хаков, а как процесс управления производительностью аналитической платформы. В нём должны участвовать разработчики SQL, специалисты по Data Platform, владельцы источников и команда эксплуатации.

Для каждого проблемного запроса полезно проходить один и тот же диагностический цикл: определить фактическое время выполнения, посмотреть план, сравнить оценки с реальными объёмами, найти самый дорогой участок, проверить pushdown и статистику, а затем повторить измерение.

Этап Что проверить Типичная проблема
1. SQL Фильтры, JOIN, SELECT, GROUP BY Читается слишком большой объём данных
2. EXPLAIN PlanNode и Exchanges Неэффективная структура плана
3. EXPLAIN ANALYZE Фактические rows, CPU, memory, network План отличается от ожиданий
4. Statistics Размеры, cardinality, NULL, диапазоны Оптимизатор неправильно оценивает стоимость
5. Pushdown Что выполняется в источнике Trino получает слишком много данных
6. JOIN Порядок, distribution, skew Большой shuffle или memory pressure
7. Storage Формат, partitioning, размер файлов Слишком дорогой TableScan

Отдельно стоит контролировать качество статистики. Cost-based optimizer может принимать решения только на основании доступной ему информации. Если статистики устарели или отсутствуют, оптимизатору сложнее правильно оценить cardinality и стоимость разных вариантов плана.

Для больших аналитических платформ полезно также разделять классы нагрузки: интерактивные запросы BI, тяжёлые ETL/ELT, ad-hoc аналитика, регулярные отчёты. Trino поддерживает resource groups, которые позволяют организовывать управление очередями и ресурсами разных категорий запросов.

И наконец, не следует считать количество worker nodes универсальным способом ускорения. Если узким местом является удалённая СУБД, сетевой обмен, неправильный JOIN, плохие статистики или чтение огромного числа мелких файлов, добавление CPU может почти не изменить ситуацию.

Что в итоге определяет скорость SQL в Trino

Trino способен выполнять очень сложные распределённые SQL-запросы, но его производительность определяется взаимодействием сразу нескольких уровней: SQL → PlanNode → optimizer rules → cost model → distributed plan → connector → storage. Поэтому оптимизировать только текст запроса недостаточно.

На уровне SQL необходимо минимизировать объём обрабатываемых данных. На уровне оптимизатора — обеспечить доступность корректных статистик и возможность применения подходящих правил. На уровне connector — использовать pushdown. На уровне хранения — правильно организовать данные, партиционирование и физическую структуру. На уровне кластера — контролировать память, сеть, CPU и распределение нагрузки между worker nodes.

Наиболее важные практические принципы можно свести к нескольким правилам:

  • сначала смотрите EXPLAIN ANALYZE, потом переписывайте SQL;
  • уменьшайте объём данных как можно раньше;
  • используйте predicate и projection pushdown, когда их поддерживает источник;
  • следите за статистиками таблиц и колонок;
  • особенно внимательно анализируйте JOIN и Exchange;
  • учитывайте data skew;
  • для повторяющихся тяжёлых расчётов рассматривайте предварительную агрегацию и материализацию;
  • не рассчитывайте, что увеличение количества worker nodes автоматически устранит проблему;
  • проверяйте фактический план после каждого существенного изменения.

Таким образом, главный подход к ускорению Trino заключается не в поиске одной «волшебной» настройки. Быстрый Trino — это результат правильно построенной цепочки от физического хранения данных до стоимости операций в распределённом плане. Чем раньше система отбрасывает ненужные строки и колонки, чем меньше данных передаётся между узлами и чем точнее optimizer понимает размеры наборов данных, тем меньше ресурсов требуется для выполнения того же SQL.

CIO-NAVIGATOR