linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
Intro
Lessons
Footer
linuxlab-УчебникиЦеныО платформеКонфиденциальность и куки
Copyright © 2026 LinuxLab. Все права защищены.
linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
  • Введение
  • Главы
  • How it worksскоро
  • Уроки
  • База знаний
  • Собеседование
Часть VI — Планировщик и выполнение

$ глава 29 · 50 минут

Порядок соединений, GEQO и кеш плана

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

Вторая тема главы - кеш плана. Подготовленное выражение может запомнить не только разобранный запрос, но и сам план, чтобы не планировать заново на каждый вызов. Но кешированный план иногда хуже свежего. Разберём, как PostgreSQL балансирует между generic и custom планом и когда это бьёт по производительности.

29.1 Комбинаторный взрыв

Соединение трёх таблиц A, B, C можно собрать в разном порядке: (A⋈B)⋈C, (A⋈C)⋈B, (B⋈C)⋈A и так далее. Для каждого порядка ещё выбирается алгоритм (nested loop / hash / merge) и метод доступа к каждой таблице. Число комбинаций растёт быстрее факториала от числа таблиц.

таблиц:   2     3     4      5       ...    10
порядков: 1     3     15     105     ...    ~17 млн

На 4-5 таблицах перебрать всё ещё реально. На 10 - уже миллионы вариантов, и честный перебор каждого занял бы дольше, чем само выполнение запроса. Планировщик должен где-то остановиться.

29.2 Динамическое программирование и join_collapse_limit

Пока таблиц немного, планировщик ищет лучший порядок почти полным перебором, но умно - динамическим программированием: лучший план для пары таблиц переиспользуется при построении планов для троек, и так далее. Это резко сокращает работу по сравнению с наивным перебором.

Сколько таблиц «немного», задаёт join_collapse_limit (по умолчанию 8). Он управляет тем, насколько планировщик волен менять порядок явных JOIN. Если число таблиц в одном уровне FROM превышает лимит, планировщик перестаёт переставлять их и берёт порядок, как написано в запросе.

sql
SHOW join_collapse_limit;   -- 8
SHOW from_collapse_limit;   -- 8 (то же для подзапросов)

Практическое следствие: на запросе с десятком таблиц порядок, в котором ты написал JOIN, начинает влиять на план. Иногда это используют намеренно - выставляют join_collapse_limit = 1, чтобы зафиксировать порядок соединений вручную.

29.3 GEQO: генетический поиск

Когда таблиц очень много, даже динамическое программирование становится дорогим. На этот случай есть GEQO - genetic query optimization. Вместо перебора он ищет хороший порядок генетическим алгоритмом: берёт несколько случайных порядков, «скрещивает» лучшие, вносит мутации, отбирает по стоимости - несколько поколений, и достаточно хороший порядок найден.

Включается GEQO, когда число таблиц в соединении достигает geqo_threshold (по умолчанию 12). Важная особенность: GEQO не гарантирует оптимальный план и недетерминирован - на одном и том же запросе он может выдать чуть разные порядки. Это сознательный размен: на 15 таблицах лучше быстро найти хороший план, чем долго искать идеальный.

sql
SHOW geqo_threshold;   -- 12

29.4 Сколько стоит само планирование

Планирование - это работа, и на сложных запросах она заметна. Увидеть её можно двумя способами:

sql
EXPLAIN ANALYZE SELECT ...;   -- внизу строка Planning Time
EXPLAIN (MEMORY) SELECT ...;  -- сколько памяти ушло на планирование

Planning Time против Execution Time показывает баланс: если запрос выполняется за 2 мс, а планируется за 20 - планирование доминирует, и его стоит кешировать. Меняя join_collapse_limit и geqo_threshold, можно наблюдать, как растёт Planning Time с числом рассмотренных порядков. Это и есть мотивация кеша плана: запланировать один раз, выполнять много.

29.5 Кеш плана: prepared statements

Подготовленное выражение (PREPARE) разбирается и анализируется один раз, а выполняется много с разными параметрами. Сначала PostgreSQL для каждого вызова строит custom plan - план под конкретные значения параметров. Это точно, но каждый раз тратит время на планирование.

sql
PREPARE q(int) AS SELECT * FROM tickets WHERE flight_id = $1;
EXECUTE q(7);
EXECUTE q(8);

После нескольких выполнений (порядка пяти) планировщик прикидывает: а нельзя ли построить один generic plan - план без привязки к значению параметра, который годится для любого $1? Если средняя стоимость custom-планов не выгоднее generic, он переключается на generic и перестаёт планировать на каждый вызов.

29.6 Generic против custom plan

Разница принципиальная. Custom plan видит конкретное значение и оценивает селективность точно: flight_id = 7 - редкое значение, возьмём индекс. Generic plan значения не видит и берёт усреднённую селективность - что хорошо для «обычного» параметра, но плохо для перекошенных данных.

sql
SELECT name, generic_plans, custom_plans
FROM pg_prepared_statements;

Управлять выбором можно вручную:

sql
SET plan_cache_mode = force_custom_plan;   -- всегда планировать заново
SET plan_cache_mode = force_generic_plan;  -- всегда один план
SET plan_cache_mode = auto;                -- решает сам (по умолчанию)

29.6.1 Подводный камень: generic plan на перекошенных данных

Беда приходит, когда значения параметра сильно различаются по селективности. Пусть status = 'done' встречается у 99% строк, а status = 'error' - у 0.01%. Для 'error' нужен индекс, для 'done' - Seq Scan.

Generic plan усреднит и выберет что-то одно - и будет плохим для половины вызовов. Симптом: после нескольких выполнений подготовленное выражение «вдруг» замедлилось (переключилось на generic), хотя текст и параметры те же. Видно по росту generic_plans в pg_prepared_statements.

Лечение - plan_cache_mode = force_custom_plan для таких запросов: пусть планирует каждый раз, зато под реальное значение. Платишь временем планирования, выигрываешь правильный план. Подробнее про кеш и порядок - в join-order-geqo.

Уроки в sandbox

lab-29.1. Наблюдать GEQO и переключение generic/custom plan

Сначала посмотрим, как растёт время планирования с числом рассмотренных порядков, потом поймаем переключение подготовленного выражения с custom на generic plan. Предсказывай счётчики до проверки.

  1. Сделай EXPLAIN ANALYZE запроса с соединением tickets, flights, bookings и запиши Planning Time.

  2. Зафиксируй порядок: SET join_collapse_limit = 1; и повтори - сравни Planning Time и план (порядок берётся как в запросе).

  3. Подготовь выражение: PREPARE q(int) AS SELECT * FROM tickets WHERE flight_id = $1;.

  4. Выполни EXECUTE q(7); шесть раз подряд. Предскажи, на каком вызове появится generic plan.

  5. Проверь счётчики: SELECT name, generic_plans, custom_plans FROM pg_prepared_statements; - оба должны быть ненулевыми.

  6. Принудительно: SET plan_cache_mode = force_custom_plan;, выполни ещё раз и убедись, что растёт только custom_plans.

sandbox с автопроверкой - открыть в песочнице

Резюме

  • Число порядков соединения растёт быстрее факториала от числа таблиц; на 4-5 таблицах перебор реален, на 10 - миллионы вариантов.
  • Планировщик ищет порядок динамическим программированием, переиспользуя планы подмножеств; join_collapse_limit (8) ограничивает, сколько JOIN он волен переставлять.
  • При join_collapse_limit таблиц больше лимита порядок берётся как написан; join_collapse_limit = 1 фиксирует порядок соединений вручную.
  • При числе таблиц от geqo_threshold (12) включается GEQO - генетический поиск порядка: быстрый, но недетерминированный и не гарантирующий оптимум.
  • Planning Time и EXPLAIN (MEMORY) показывают стоимость самого планирования; когда оно доминирует над выполнением - план стоит кешировать.
  • Подготовленное выражение сначала строит custom plan под значения, после нескольких вызовов может перейти на generic plan без привязки к параметру.
  • Generic plan плох на перекошенных данных (значения с разной селективностью); лечится plan_cache_mode = force_custom_plan, видно по generic_plans в pg_prepared_statements.

Контрольные вопросы

  1. Почему планировщик не перебирает все порядки соединения на запросе с 15 таблицами?

    Показать ответ

    Потому что число порядков растёт быстрее факториала: на 15 таблицах это астрономическое количество вариантов, и честный перебор каждого занял бы дольше, чем само выполнение запроса. На малом числе таблиц планировщик использует динамическое программирование (почти полный, но умный перебор), а при достижении geqo_threshold (12) переключается на GEQO - генетический алгоритм, который находит достаточно хороший порядок за разумное время, жертвуя гарантией оптимальности.

  2. Что произойдёт с запросом, где число JOIN превышает `join_collapse_limit`?

    Показать ответ

    Планировщик перестанет свободно переставлять таблицы и возьмёт порядок соединений примерно так, как он записан в запросе. До лимита он сплющивает все JOIN в один уровень и ищет лучший порядок; сверх лимита - оставляет структуру как есть, чтобы не взорвать перебор. Поэтому на больших запросах порядок написания JOIN начинает влиять на план. Иногда это используют намеренно: join_collapse_limit = 1 фиксирует порядок вручную.

  3. Чем custom plan отличается от generic plan у подготовленного выражения?

    Показать ответ

    Custom plan строится под конкретные значения параметров и оценивает селективность точно - но планируется заново на каждый вызов. Generic plan строится один раз без привязки к значению параметра, по усреднённой селективности, и переиспользуется - экономит время планирования, но не видит конкретных значений. PostgreSQL начинает с custom-планов, а после нескольких вызовов может перейти на generic, если тот не дороже в среднем.

  4. Подготовленное выражение «вдруг» замедлилось после нескольких одинаковых вызовов. Почему?

    Показать ответ

    Скорее всего оно переключилось с custom на generic plan. После примерно пяти выполнений планировщик прикидывает, можно ли обойтись одним generic-планом. Если данные перекошены (значения параметра сильно различаются по селективности), generic plan усредняет и становится плохим для части значений - отсюда замедление при том же тексте и параметрах. Видно по росту generic_plans в pg_prepared_statements; лечится plan_cache_mode = force_custom_plan.

  5. Когда вообще стоит беспокоиться о времени планирования?

    Показать ответ

    Когда оно сопоставимо с временем выполнения или больше его. Это видно в EXPLAIN ANALYZE по строке Planning Time против Execution Time. Для короткого запроса, выполняемого за пару миллисекунд, но планируемого за десятки, планирование доминирует - и его стоит кешировать через подготовленные выражения. Для тяжёлого запроса, идущего минуты, время планирования незначимо, и кеш плана большой пользы не даст.

← Предыдущая28-joinsСледующая →30-reading-explain
Footer
linuxlab-
Copyright © 2026 LinuxLab. Все права защищены.
Учебники
Цены
О платформе
Конфиденциальность и куки