Learning Platform
Урок 07.02 · 23 мин
Продвинутый
PlannerLogical planPhysical planEquivalent classesPlan tree

В прошлом уроке мы видели pipeline. Сейчас увеличим масштаб на самой интересной части — planner — и разделим его работу на два уровня: логический и физический. Это не академическая блажь; это та граница, на которой Postgres решает, что план корректен (выдаст правильный результат), и отдельно — какой дешевле.

Реляционная алгебра как промежуточный язык

Любой SQL-запрос можно представить как дерево операций реляционной алгебры: проекции, селекции, joins, агрегации. Это и есть логический план.

Например, запрос:

SELECT c.full_name, sum(o.total_cents) AS total
FROM customers c
JOIN orders o ON o.customer_id = c.id
WHERE c.country = 'RU'
GROUP BY c.full_name;

В логике это:

Логический план как дерево операций

Сверху вниз: aggregate группирует, join соединяет, selection фильтрует, scan читает таблицы. Никакой информации о методе выполнения — только что делается.

Aggregate (sum, group by full_name)Π агрегат по группам
Join (c.id = o.customer_id)⋈ — какие пары строк
Selection (country = 'RU')σ — фильтр
Scan customersΠ — какие колонки
Scan ordersΠ — какие колонки

В реляционной алгебре никакой информации о как выполнять нет. Нет понятий «индекс», «hash table», «sort». Есть только что считать.

Эквивалентные преобразования

Логический план можно переписать другим, дающим тот же результат. Это — эквивалентные преобразования, и planner активно их применяет:

  • Предикат «спустить вниз» (predicate pushdown): σ(country='RU')(c ⋈ o) эквивалентно σ(country='RU')(c) ⋈ o. Лучше: фильтруем customers до join, объём данных в join меньше.
  • Перестановка join’ов (associativity, commutativity): (A ⋈ B) ⋈ CA ⋈ (B ⋈ C). Это даёт planner свободу выбрать порядок.
  • Inline subquery → join: SELECT ... WHERE id IN (SELECT ...) часто переписывается в semi-join.
  • Projection pushdown: ненужные колонки выкидываются как можно раньше.

После преобразований остаётся

equivalent class
— все логические деревья, дающие одинаковый результат. Это пространство поиска.

Физический план: один логический — много физических

Для одного логического плана есть много физических. Различия:

  • Какой scan: Seq Scan, Index Scan, Bitmap Scan, Index Only Scan.
  • Какой join: Nested Loop, Hash Join, Merge Join (модуль 8 — целый посвящён этому).
  • Какой aggregate: Hash Aggregate, Group Aggregate (требует sorted input), Plain Aggregate (без group by).
  • Нужны ли Sort/Materialize узлы и где.
Один логический → три физических плана

Тот же логический join customers ⋈ orders → три разных executable plan. У каждого свой cost — planner выберет минимальный.

Логический план: customers ⋈ orders, σ country='RU', γ sum group by nameчто считать
Физ. план AHashAgg
Hash Joinна customer_id
Seq Scan cfilter country
Seq Scan o
cost ≈ 6000
Физ. план BGroupAgg
Merge Joinна customer_id
Sort c
Sort o
cost ≈ 12000
Физ. план CHashAgg
Nested Loopвнешний c
Seq Scan c (RU)
Index Scan oby customer_id
cost ≈ 4500

Planner перебирает (часть) физических планов в equivalent class, оценивает cost каждого, выбирает минимальный. Это и есть «cost-based optimization».

Посмотрим на разные физические планы

Базовый запрос. По умолчанию planner выберет — что-то одно. Запиши план.

PostgreSQL

Теперь принудительно отключим Hash Join — посмотрим, что planner выберет вторым.

Запрет Hash Join. Planner вынужден выбрать другой физический алгоритм для того же логического плана.

PostgreSQL

Та же выборка, тот же результат — но другое физическое дерево. Это и есть демонстрация: один логический план реализуется несколькими физическими.

Можно идти дальше — отключать поочерёдно:

Запрет hashjoin и mergejoin → остаётся только nested loop. Cost вырастет — но план корректен.

PostgreSQL

Сравни итоговый cost= — он гораздо выше. Именно поэтому planner и не выбрал nested loop сам.

Equivalent classes и pg_eclass

Внутри Postgres логические преобразования живут в структуре

EquivalenceClass
. Если в запросе a.x = b.y AND b.y = c.z, planner знает, что a.x = c.z — без явного указания. Это даёт ему свободу выбирать join order. Пример:

SELECT * FROM a, b, c
WHERE a.x = b.y AND b.y = c.z;

Logically {a.x, b.y, c.z} — одна equivalence class. Planner может выполнить a ⋈ c напрямую через выведенное условие a.x = c.z, даже если ты не написал это в WHERE.

Почему планы меняются между запусками

Не должны меняться без причины — но могут. Триггеры:

  • ANALYZE обновил статистику; раньше думали 1000 строк, теперь 1M — план перестроился.
  • Изменён GUC: random_page_cost, work_mem, enable_hashjoin.
  • Новый индекс создан или удалён.
  • В prepared statement переключение custom → generic plan (см. урок 1).
  • Версия Postgres обновлена с новыми эвристиками.

Если в продакшене план «вдруг» поменялся, в первую очередь проверь pg_stat_user_tables / pg_class.reltuples — есть ли свежий ANALYZE.

Зачем нам это разделение

В практике различение логический/физический даёт несколько вещей:

  • EXPLAIN читать стало понятно: видишь Hash Join — знаешь, что это физический выбор. Хочешь другой — отключай флаг или меняй статистику.
  • Hint’ы (через pg_hint_plan) работают именно на уровне физического плана: можно запретить определённый join algorithm для конкретного запроса.
  • Понимание границ оптимизатора: если логически невозможно (SELECT count(*) FROM t обязан прочитать каждую строку), никакой физический план не спасёт.
Проверка знанийKnowledge check
Запрос с тремя JOIN'ами выполняется 2 секунды. EXPLAIN показывает Nested Loop с большим количеством строк во внешнем входе. Ты подозреваешь, что Hash Join был бы лучше. Какие два независимых способа проверить эту гипотезу, не меняя сам запрос?
ОтветAnswer
Первый: SET enable_nestloop = off; EXPLAIN ANALYZE того же запроса. Planner вынужден выбрать другой план; смотришь, стало ли быстрее. Второй: SET enable_hashjoin = on (если был выключен — но он обычно включён) и проверь, не «ушёл» ли в Nested Loop из-за низкого work_mem. Увеличь work_mem (SET work_mem = '256MB'), потому что Hash Join строит hash table в памяти и при недостатке падает в дисковый partitioning, что делает его дорогим — и planner избегает. Дополнительно: ANALYZE затронутые таблицы — мб planner считает по устаревшей статистике, что во внешнем входе мало строк.

Чек-лист

  • Логический план — реляционная алгебра, описывает «что считать».
  • Физический план — конкретные узлы (Seq Scan / Hash Join / Sort), описывает «как».
  • Один логический → много физических; planner перебирает их в equivalent class.
  • Эквивалентные преобразования: predicate pushdown, join reordering, projection pushdown, semi-join из IN.
  • enable_* GUC выключают физические узлы — отладочный инструмент, не для прода.
  • EquivalenceClass позволяет planner выводить транзитивные равенства между relations.
  • Когда план «вдруг» изменился — ищи свежий ANALYZE, изменение GUC или новый индекс.
Селекция и проекция: два базовых оператора реляционной алгебры

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Что такое equivalence class в контексте PostgreSQL planner?

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

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

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

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