open atlas
↑ К треку
SQL и PostgreSQL вглубь SQL · 08 · 02

Стоимостный планировщик

Планировщик оценивает каждый кандидатный скан и join по оценочной стоимости и берёт самый дешёвый. В Postgres нет хинтов намеренно — ты тюнишь входы, а не выход.

SQL Middle ◷ 15 min
Уровень
ОсновыJuniorMiddleSenior

Новый инженер, пришедший из 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. Хинт запрещает эту адаптацию.

Поэтому сеньорский воркфлоу — починить входы, чтобы планировщик сам пришёл к верному плану:

  1. Статистика — запусти ANALYZE или подними default_statistics_target / добавь extended statistics (урок 03). Большинство «плохих планов» — это плохие оценки.
  2. Индексы — добавь или удали индекс, чтобы сменить множество кандидатов. Планировщик берёт только из доступного.
  3. Конфиг — на SSD/NVMe random_page_cost = 4.0 это дефолт для вращающихся дисков, дико перенаказывающий index scan; снижение до 1.1 — самая частая прод-правка. effective_cache_size говорит планировщику, на сколько OS-кеша он может рассчитывать.
  4. Переписать запрос — иногда LATERAL, EXISTS вместо IN или разбиение запроса меняют пространство кандидатов к лучшему.
  5. 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 нет хинтов, ты не можешь перекрыть _______ планировщика; вместо этого ты тюнишь его входы — статистику, индексы и константы стоимости — чтобы он сам пришёл к верному плану.

Вспомните перед уходом
  1. 01
    Как планировщик выбирает между Index Scan и Seq Scan?
  2. 02
    Почему в Postgres намеренно нет хинтов и что тюнишь вместо них?
  3. 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-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 7 завершено
Связанные уроки

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.