EXPLAIN и EXPLAIN ANALYZE
Пять предыдущих уроков прошли жизненный цикл запроса изнутри: текст, AST, Analysis, логический план, оптимизация, распределённый план. Всё это — внутренние представления. Но Trino даёт инструмент, который показывает эти представления наружу, — команду EXPLAIN. Это главный инструмент диагностики: умение читать план отличает того, кто пишет SQL, от того, кто понимает, что движок с этим SQL делает.
Этот урок — про EXPLAIN и EXPLAIN ANALYZE: что показывает каждая команда, как читать вывод и в чём принципиальная разница между ними.
EXPLAIN: план без выполнения
EXPLAIN показывает план запроса, не выполняя сам запрос. Trino проходит этапы планирования — анализ, логический план, оптимизация, распределённый план — и выдаёт результат, но до исполнения дело не доходит. Никакие данные не читаются, воркеры не нагружаются.
У EXPLAIN есть параметр TYPE, выбирающий, какое представление показать:
| Команда | Что показывает |
|---|---|
EXPLAIN (TYPE LOGICAL) | Логический план — дерево PlanNode после оптимизации |
EXPLAIN (TYPE DISTRIBUTED) | Распределённый план — фрагменты-stages с типами распределения |
EXPLAIN (TYPE VALIDATE) | Только проверка: корректен ли запрос (true/false), без построения плана |
EXPLAIN (TYPE IO) | Какие таблицы и столбцы запрос будет читать и писать |
EXPLAIN без указания TYPE показывает распределённый план — это поведение по умолчанию.
Главное про EXPLAIN: цифры в его выводе — это оценки (estimates). Когда EXPLAIN пишет рядом с узлом число строк, это прогноз планировщика, основанный на статистике таблиц. Прогноз может быть точным, а может сильно ошибаться, если статистики нет или она устарела. EXPLAIN показывает намерение движка, а не факт.
Как читать логический план
Возьмём вывод EXPLAIN (TYPE LOGICAL):
EXPLAIN (TYPE LOGICAL)
SELECT name FROM tpch.sf1.customer WHERE acctbal > 5000;
Output[columnNames = [name]]
Project[]
Filter[filterPredicate = (acctbal > 5000)]
TableScan[table = tpch:customer:sf1]
name := name:varchar(25)
acctbal := acctbal:double
Читается это дерево по отступам — отступ показывает вложенность, родитель левее потомка. И читать его смысл удобнее снизу вверх, по потоку данных:
TableScan— лист. Читает таблицуcustomer, столбцыnameиacctbal.Filter— над ним. Применяет предикатacctbal > 5000, отбрасывает строки.Project— выбирает финальный набор столбцов.Output— корень. Отдаёт результат, столбецname.
Это ровно дерево PlanNode из урока про логическое планирование, выведенное текстом. Обратите внимание: фильтр стоит прямо над TableScan — это работа оптимизатора, опустившего предикат к источнику.
Как читать распределённый план
EXPLAIN (TYPE DISTRIBUTED) показывает план, разрезанный на фрагменты:
EXPLAIN (TYPE DISTRIBUTED)
SELECT nationkey, count(*) AS cnt
FROM tpch.sf1.customer
GROUP BY nationkey;
Fragment 0 [SINGLE]
Output[nationkey, cnt]
RemoteSource[sourceFragmentIds = [1]]
Fragment 1 [HASH]
Aggregate[type = FINAL, keys = [nationkey]]
cnt := count(partial_cnt)
RemoteSource[sourceFragmentIds = [2]]
Fragment 2 [SOURCE]
Aggregate[type = PARTIAL, keys = [nationkey]]
partial_cnt := count(*)
TableScan[table = tpch:customer:sf1]
Здесь три фрагмента, и читать тоже удобно снизу — от наибольшего номера:
Fragment 2 [SOURCE]— чтениеcustomerи частичная агрегация (PARTIAL) прямо у источника: каждый воркер считает count по своей части данных. ТипSOURCE— фрагмент идёт туда, где splits.Fragment 1 [HASH]— данные перераспределены по хэшуnationkey, считается финальная агрегация (FINAL): частичные counts суммируются по каждой группе.Fragment 0 [SINGLE]— собирает результат на одной ноде, отдаёт клиенту.
RemoteSource — это линия разреза между фрагментами, точка сетевого обмена. sourceFragmentIds = [2] означает «данные приходят от фрагмента 2».
Здесь же видна важная оптимизация — двухфазная агрегация. Агрегат считается дважды: PARTIAL локально у источника и FINAL после обмена. Смысл — резко уменьшить объём данных, идущих по сети: вместо миллионов строк между фрагментами летят уже частично свёрнутые счётчики по группам.
Правило нумерации фрагментов: меньший номер — последний шаг исполнения (ближе к результату), больший номер — первый шаг (ближе к источнику). Поэтому Fragment 0 всегда финал, а фрагмент с наибольшим номером — начало, чтение таблиц. Читайте распределённый план от наибольшего номера к нулю — это и есть порядок исполнения.
EXPLAIN ANALYZE: план плюс факт
EXPLAIN ANALYZE — принципиально другая команда. Она реально выполняет запрос и показывает распределённый план, заполненный фактической статистикой исполнения.
Разница с EXPLAIN фундаментальна:
EXPLAIN отвечает на вопрос «что движок СОБИРАЕТСЯ делать». EXPLAIN ANALYZE отвечает на вопрос «что на самом деле произошло». Поскольку EXPLAIN ANALYZE выполняет запрос, он занимает столько же ресурсов и времени, сколько обычный запуск, — и не годится для тяжёлых запросов, которые не хочется выполнять. Зато его цифры реальны.
Что показывает EXPLAIN ANALYZE сверх обычного плана:
- Фактическое время по стадиям и операторам — где запрос реально провёл время (wall time, CPU time).
- Фактическое число строк, прошедших через каждый оператор, — и его можно сравнить с оценкой из
EXPLAIN. - Объёмы данных: сколько прочитано, сколько передано между фрагментами по сети.
- Относительная стоимость стадий — какая стадия съела львиную долю.
EXPLAIN ANALYZE
SELECT nationkey, count(*) FROM tpch.sf1.customer GROUP BY nationkey;
Fragment 1 [HASH]
CPU: 2.34s, Scheduled: 4.10s, Input: 150000 rows
Aggregate[type = FINAL, keys = [nationkey]]
Input avg.: 25.00 rows, Input std.dev.: 12.5%
...
Fragment 2 [SOURCE]
CPU: 5.81s, Scheduled: 9.20s, Input: 1500000 rows
TableScan[table = tpch:customer:sf1]
Input: 1500000 rows
Здесь видно фактическое: Fragment 2 прочитал 1 500 000 строк и потратил 5.81s CPU, Fragment 1 после агрегации работает уже со 150 000 строк. Это не прогноз — это то, что случилось.
Зачем это нужно: связь оценки и факта
Главная диагностическая ценность — в сопоставлении EXPLAIN и EXPLAIN ANALYZE. EXPLAIN показывает оценку строк, EXPLAIN ANALYZE — фактическое число. Если они близки — статистика хорошая, планировщик видел реальность, план построен на верных данных. Если они расходятся на порядки — статистика врёт, и любые решения оптимизатора (порядок join, broadcast или hash) могли быть приняты на ложной основе.
Это типичный сценарий разбора медленного запроса. Снимаете EXPLAIN ANALYZE, находите стадию с наибольшим временем — bottleneck. Смотрите фактическое число строк на ней. Сравниваете с оценкой из EXPLAIN. Большое расхождение — сигнал собрать статистику командой ANALYZE и перепланировать. Это смыкается с модулем про cost-based optimizer: качество плана определяется качеством статистики, а EXPLAIN/EXPLAIN ANALYZE — инструмент, которым это качество проверяют.
Практический алгоритм: EXPLAIN — когда нужно быстро посмотреть структуру плана, не выполняя запрос (например, запрос тяжёлый). EXPLAIN ANALYZE — когда запрос уже медленный и надо понять, ГДЕ именно, по фактам. Есть и EXPLAIN ANALYZE VERBOSE — он добавляет низкоуровневую статистику операторов, но требует понимания внутренностей движка.
Место EXPLAIN в картине цикла
EXPLAIN — не отдельный этап жизненного цикла, а окно в него. Он показывает наружу те представления, которые движок строит внутри: TYPE LOGICAL — результат логического планирования и оптимизации, TYPE DISTRIBUTED — результат distributed planning. EXPLAIN ANALYZE идёт дальше и показывает результат самого исполнения.
Поэтому этот урок завершает модуль про жизненный цикл логично: пять уроков объясняли этапы изнутри, шестой дал инструмент видеть их снаружи. Дальше курс переходит к распределённому исполнению — к тому, что происходит после планирования, когда stages разворачиваются в task на воркерах. И EXPLAIN ANALYZE будет главным инструментом и там.
Попробуй сам
На кластере Trino отработайте оба инструмента:
- Снимите
EXPLAIN (TYPE LOGICAL)иEXPLAIN (TYPE DISTRIBUTED)для одного и того же запроса сGROUP BY. Сопоставьте: как узлы логического плана разложились по фрагментам распределённого. - Снимите
EXPLAIN ANALYZEдля того же запроса. Найдите стадию с наибольшим CPU-временем. Найдите фактическое число строк вTableScan. - Сравните оценку числа строк из
EXPLAINс фактом изEXPLAIN ANALYZEдляtpch.sf1.orders. У tpch статистика обычно хорошая — оценки должны быть близки к факту. - Возьмите запрос с join и по распределённому плану определите: какой фрагмент читает какую таблицу, где проходят
RemoteSource, какой тип у фрагмента join. - Сформулируйте письменно, в каких ситуациях вы выберете
EXPLAIN, а в каких —EXPLAIN ANALYZE, и почему.