Стоимостный планировщик
Планировщик оценивает каждый кандидатный скан и join по оценочной стоимости и берёт самый дешёвый. В Postgres нет хинтов намеренно — ты тюнишь входы, а не выход.
Новый инженер, пришедший из Oracle, заводит тикет: «Postgres не даёт добавить index hint. Как заставить его использовать my_index?» Ответ — «нельзя, и это намеренно» — звучит безумно, пока ты не видел, как захинченный Oracle-запрос застрял в плане, ставшем катастрофой после утроения данных. Postgres сделал ставку 25 лет назад: тюнь входы планировщика, никогда не перекрывай его выход. Этот урок — как ставка окупается и где кусается.
Как он выбирает: по стоимости, не по правилам
Когда на проде кусает «неверный план», ответ почти никогда не в новом синтаксисе SQL — а в понимании, какое число планировщик взял неверно. Планировщик берёт проанализированный запрос и перечисляет кандидатные физические планы — разные методы скана, разные алгоритмы join, разные порядки join’ов — затем присваивает каждому числовую стоимость и берёт минимальную. Глава об execution-plans трека databases глубоко выводит стоимостную модель и таксономию сканов/join’ов; здесь суть — цикл решения, который SQL-автор видит снаружи.
Стоимость складывается из оценочного числа строк (из статистики, урок 03), умноженного на константы операций. Самые важные дефолты: seq_page_cost = 1.0 (читать страницу последовательно), random_page_cost = 4.0 (читать страницу случайно), cpu_tuple_cost = 0.01 (обработать строку). Index scan делает случайные чтения страниц; sequential scan — дешёвые последовательные. Поэтому вердикт планировщика «index или seq scan?» это на самом деле арифметика: сколько строк совпадает × случайная цена против вся таблица × последовательная цена.
Выбор скана для WHERE status = 'shipped':
- Мало совпадающих строк → Index Scan (дёшево прыгнуть прямо к ним).
- Много совпадающих строк → Seq Scan (случайный I/O на строку стоил бы больше, чем прочитать всю таблицу по порядку). Это правильно, а не баг — Seq Scan по неселективному фильтру обгоняет индекс.
- Средняя доля → Bitmap Index Scan (собрать указатели на совпадающие страницы, отсортировать их, прочитать страницы в физическом порядке — точность индекса без случайной молотьбы).
Выбор join для orders ⋈ line_items:
- Крошечная внешняя сторона → Nested Loop (для каждой внешней строки зондировать индекс на внутренней — отлично, когда «мало × индексный поиск»).
- Две большие несортированные стороны → Hash Join (построить hash-таблицу на меньшей, прогнать большую мимо неё).
- Два уже отсортированных входа → Merge Join (идти по обоим в ногу).
-- Смотри, как скан переключается при смене селективности:
EXPLAIN SELECT * FROM orders WHERE status = 'cancelled'; -- редко → Index Scan
EXPLAIN SELECT * FROM orders WHERE status = 'placed'; -- часто → Seq ScanПочему в Postgres нет хинтов
Oracle и SQL Server дают прибить план хинтом. Postgres отказывает, и логика устойчива: хинт — это замороженное решение против движущегося набора данных. Запрос, который ты сегодня хинтишь «используй индекс», — это запрос, который через год делает 50M случайных чтений, когда таблица выросла и индекс перестал быть селективным. Планировщик, накормленный свежей статистикой, сам бы переключился на Seq Scan. Хинт запрещает эту адаптацию.
Поэтому сеньорский воркфлоу — починить входы, чтобы планировщик сам пришёл к верному плану:
- Статистика — запусти
ANALYZEили поднимиdefault_statistics_target/ добавь extended statistics (урок 03). Большинство «плохих планов» — это плохие оценки. - Индексы — добавь или удали индекс, чтобы сменить множество кандидатов. Планировщик берёт только из доступного.
- Конфиг — на SSD/NVMe
random_page_cost = 4.0это дефолт для вращающихся дисков, дико перенаказывающий index scan; снижение до1.1— самая частая прод-правка.effective_cache_sizeговорит планировщику, на сколько OS-кеша он может рассчитывать. - Переписать запрос — иногда
LATERAL,EXISTSвместоINили разбиение запроса меняют пространство кандидатов к лучшему. join_collapse_limit— после ~8 соединяемых таблиц планировщик перестаёт исчерпывающе переставлять join’ы (пространство поиска факториально) и доверяет твоему порядку. Повышение даёт оптимизировать большие join’ы ценой времени планирования.
Вместе эти входы — статистика, индексы, константы стоимости и форма запроса — это всё, с чем работает планировщик. Почини любой из них правильно, и планировщик сам выведет хороший план; перескочи к управлению выходом — и получишь хрупкий замороженный план, который больно ударит при росте данных.
▸Частая ошибка
Самый частый самонанесённый плохой план: оставить random_page_cost на 4.0 на NVMe-хранилище. На современных SSD случайное чтение страницы лишь чуть дороже последовательного, поэтому 4.0 говорит планировщику, что index scan стоит ~4× от реального — и он избегает прекрасных индексов в пользу Seq Scan. Команды часами таращатся на «почему он не использует мой индекс?». Фикс в одну строку: ALTER SYSTEM SET random_page_cost = 1.1; затем SELECT pg_reload_conf();. Это не хинт — это коррекция цены, и планировщик заново корректно выводит каждый план. (Настоящее расширение pg_hint_plan существует для редкого случая, когда план правда нужно прибить, но тянись к нему последним, никогда первым.)
EXPLAIN показывает Seq Scan для WHERE status = 'placed', что совпадает с 40% большой таблицы. Это баг?
БД на NVMe SSD, но планировщик упорно берёт Seq Scan вместо хороших индексов. Лучшая первая правка конфига?
Заполни пропуск: поскольку в Postgres нет хинтов, ты не можешь перекрыть _______ планировщика; вместо этого ты тюнишь его входы — статистику, индексы и константы стоимости — чтобы он сам пришёл к верному плану.
- 01Как планировщик выбирает между Index Scan и Seq Scan?
- 02Почему в Postgres намеренно нет хинтов и что тюнишь вместо них?
- 03БД на NVMe SSD, планировщик избегает хороших индексов — вероятная причина и фикс в одну строку?
Стоимостный планировщик — это движок ценообразования: из твоего запроса он перечисляет кандидатные физические планы — Seq/Index/Bitmap сканы, Nested-Loop/Hash/Merge join’ы и порядки join’ов — присваивает каждому оценочную стоимость (строки × константы операций вроде seq_page_cost, random_page_cost, cpu_tuple_cost) и запускает самый дешёвый. Seq Scan по неселективному фильтру — верный ответ, а не сбой. Поскольку в Postgres нет хинтов по дизайну — хинт замораживает решение против движущихся данных — ты тюнишь входы: обнови статистику (большинство плохих планов — плохие оценки, урок 03), добавь или удали индексы, чтобы сменить набор кандидатов, поправь константы стоимости (снижение random_page_cost на SSD — классический выигрыш), перепиши запрос или подними join_collapse_limit для join’ов многих таблиц. Выбор за планировщиком; за тобой — цены, кандидаты и оценки, которые его кормят. Теперь, когда видишь удивляющий тебя план, первый вопрос не «как его заставить?», а «какой вход я передал неверно?»
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.