Планировщик (Planner) — это ядро оптимизатора запросов в PostgreSQL, которое превращает SQL-запрос в эффективный план выполнения.

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

Из всех возможных способов выполнить запрос выбрать тот, который минимизирует «стоимость» (cost) — оценку времени и ресурсов.

Как работает планировщик

  1. Получает разобранный запрос (Query Tree)
  • После парсинга и анализа SQL превращается в внутреннее дерево запроса с OID’ами таблиц, типами колонок и условиями.
  1. Собирает информацию
  • Метаданные: есть ли индексы, ограничения (CHECK, FOREIGN KEY).
  • Статистика: из pg_stats — распределение значений, число строк, частые значения.
  • Параметры сервера: random_page_cost, cpu_tuple_cost, effective_cache_size и др.
  1. Генерирует возможные планы Для каждой операции рассматриваются альтернативы:
ОперацияВарианты
Чтение таблицыSeq Scan, Index Scan, Index Only Scan, Bitmap Heap Scan
СоединениеNested Loop, Hash Join, Merge Join
СортировкаSort, Index Scan (если индекс упорядочен)
АгрегацияHashAggregate, GroupAggregate
  1. Оценивает стоимость каждого плана Использует модель cost-based optimization:
  • I/O-стоимость: чтение страниц с диска или из кэша.
  • CPU-стоимость: обработка строк, сравнение значений.
  • Временная стоимость: сортировка, хеширование.
# Формула (упрощённо):
общая_стоимость = стоимость_чтения + (cpu_cost * число_строк)

Параметры влияют на баланс:

  • На SSD: random_page_cost = 1.1 (близко к seq_page_cost = 1.0) → чаще выбирает Index Scan.
  • На HDD: random_page_cost = 4.0 → предпочитает Seq Scan при больших объёмах.
  1. Выбирает план с минимальной стоимостью
  • Возвращает Plan Tree — исполняемое дерево операторов (SeqScan, HashJoin, Sort и т.д.).
  • Этот план передаётся Executor’у для выполнения.
# Пример: как планировщик выбирает
	# Запрос:
	SELECT * FROM orders WHERE status = 'shipped';
 
# Планировщик проверяет:
#
#	Есть ли индекс по status? → Да.
#	Сколько строк с status = 'shipped'?
#		Из статистики: 90% от 1 млн → 900 000 строк.
#	Сравнивает:
#		Index Scan: 900 000 случайных чтений → дорого.
#		Scan: 1 проход по всем страницам → дешевле.
#	Выбирает Seq Scan.
#
# Если бы 'shipped' было 0.1% строк → выбрал бы Index Scan.
 
 
##### Как увидеть работу планировщика?
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders WHERE status = 'shipped';
# Вывод покажет:
# - Выбранный план,
# - Оценку строк vs реальность,
# - Использование буферов.
 
# Если **оценка сильно отличается от реальности** — нужно `ANALYZE`.

Ключевые параметры, влияющие на планировщик

ПараметрРоль
random_page_costОтносительная стоимость случайного чтения (меньше для SSD)
- SSD: 1.1
- NVMe: 1.0
- HDD: 4.0
cpu_tuple_costСтоимость обработки одной строки
effective_cache_sizeОценка кэша ОС (влияет на ожидаемое hit/miss в shared_buffers)
enable_*Флаги для отключения методов (enable_seqscan = off — не рекомендуется!)
join_collapse_limitГлубина перестановки JOIN’ов
------------------------------------------------------------------------------------------------
ПараметрНужно настраивать?Рекомендуемое значение
random_page_costДа1.0–1.1 (SSD/NVMe)
effective_cache_sizeДа50–75% от RAM
cpu_*_costРедкоОставить по умолчанию
enable_*НикогдаНе трогать!
join_collapse_limitИногда8–12 (по умолчанию)