awesome-everything EN
↑ Обратно к восхождению

Базы данных

Кеш планов, настройка константных стоимостей и внутренности планировщика

Суть Подготовленные операторы переключаются на общий план после 5 выполнений — ловушка для косых параметров. SSD требует random_page_cost = 1.1. GEQO обрабатывает большие соединения. JIT помогает CPU-bound запросам. Каждый механизм имеет режим отказа и исправление.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 18 min

Параметризованный запрос работал быстро месяцами. Однажды P99 задержка возрастает до 4 секунд. Среднее по-прежнему 3 мс. Тот же запрос, другие параметры — распределение косое. После 5 выполнений Postgres переключился на общий план, не знающий значений параметров. Одна строка ALTER ROLE исправляет это.

Внутренняя структура планировщика

Для каждого запроса планировщик строит пути — лёгкие, частичные планы для фрагментов запроса (сканирование одной таблицы, соединения небольших наборов отношений). Пути сравниваются по стоимости; только дешевейший путь за «набор отношений + порядок сортировки» сохраняется (остальные отбрасываются). Для многоотношённых соединений рассматриваются все комбинации с помощью динамического программирования — исчерпывающим образом до geqo_threshold (умолчание 12) отношений, затем переключается на Genetic Query Optimizer (эвристический поиск) выше этого порога.

Ментальная модель для старших: когда видите неожиданный план, спрашивайте «какие ещё пути рассматривал планировщик и почему они были отброшены?». Ответ почти всегда — разница в стоимости, обусловленная оценками строк.

Общие vs кастомные планы (подготовленные операторы)

Каждый параметризованный запрос от драйвера ($1, $2, …) и каждая PL/pgSQL-функция с параметрами проходит через кеш планов. Жизненный цикл:

  1. Первые 5 выполнений: Postgres планирует каждое с реальными значениями параметров — кастомные планы, оптимальные для конкретного параметра.
  2. После 5-го выполнения: Postgres вычисляет среднюю стоимость 5 кастомных планов и сравнивает её со стоимостью общего плана (без привязки к параметрам).
  3. Если стоимость общего плана в пределах ~10% от средней стоимости кастомных: Postgres навсегда переключается на общий план на время жизни подготовленного оператора.

Переключение невидимо для приложения. Для равномерно распределённых параметров общий план обычно подходит. Для косых распределений (workspace_id, где у некоторых workspace 8% строк, а у других 0.01%), общий план оптимизируется под «типичный» параметр и катастрофически неверен для выбросов.

Симптомы: высокое stddev_exec_time в pg_stat_statements (бимодальная задержка — быстро для распространённых параметров, медленно для выбросов).

Диагностика (PG 16+): EXPLAIN (GENERIC_PLAN) на подготовленной форме — показывает точно какой план получают все выполнения после переключения.

Исправление:

ALTER ROLE app SET plan_cache_mode = 'force_custom_plan';

Каждое выполнение перепланируется с реальным параметром. Стоимость планирования: ~0.4–2 мс — ничтожно по сравнению с 4-секундной хвостовой задержкой.

Найди ошибку

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

log
# Строка pg_stat_statements для проблемного запроса:
query: "SELECT * FROM orders WHERE workspace_id = $1 AND status = $2 ORDER BY created_at DESC LIMIT 50"
calls: 482,910
total_exec_time: 1,852,400 ms
mean_exec_time: 3.84 ms
stddev_exec_time: 412.6 ms   # ОГРОМНОЕ stddev -- симптом переключения плана
min_exec_time: 0.8 ms
max_exec_time: 4,290 ms

# EXPLAIN (ANALYZE, BUFFERS) на ТОМ ЖЕ запросе с workspace_id=42 (большой объём):
Limit (cost=0..14.2 rows=50) (actual time=2.1..2.3 rows=50 loops=1)
-> Index Scan using idx_orders_workspace_status_created on orders
     (cost=0..14000 rows=50000) (actual time=2.1..2.3 rows=50 loops=1)
     Index Cond: ((workspace_id=42) AND (status='pending'::text))
     Buffers: shared hit=12
Planning Time: 0.4 ms
Execution Time: 2.4 ms

# EXPLAIN (GENERIC_PLAN, BUFFERS) на подготовленной форме (PG 16+):
Limit (cost=0.43..28.5 rows=50)
-> Index Scan using idx_orders_created on orders
     (cost=0.43..15000.0 rows=27000)
     Filter: ((workspace_id = $1) AND (status = $2))
Planning Time: 0.2 ms

# Контекст:
# Распределение workspace_id: workspace 1 занимает 0.01% строк; workspace 42 — 8%.
# Распределение status: 'pending' 15%, 'shipped' 80%, 'cancelled' 5%.

Почему тот же подготовленный оператор показывает mean 3.84мс, но stddev 412мс? Каково немедленное исправление?

Настройка константных стоимостей для SSD

Postgres поставляется с random_page_cost = 4.0 и seq_page_cost = 1.0 — откалиброваными для вращающихся HDD, где случайные чтения в 4× медленнее последовательных. На NVMe SSD отношение 1.5–2×; на системах, где рабочий набор помещается в RAM, приближается к 1.0.

-- В postgresql.conf для SSD-систем:
random_page_cost = 1.1
seq_page_cost    = 1.0

-- Сообщите планировщику, сколько данных кешировано ОС:
effective_cache_size = '24GB'  -- ~75% общей RAM

Эффект: планировщик становится более склонен использовать Index Scan и Index Only Scan вместо Seq Scan — что является правильным смещением для современного железа. Это единственная наиболее эффективная настройка планировщика на любом SSD-обеспеченном Postgres.

effective_cache_size — не выделение памяти, а подсказка планировщику о доступном объёме OS page-cache + shared_buffers. Правильная установка делает разницу стоимости между кешированными страницами индекса и некешированными страницами кучи более точной.

GEQO и большие соединения

Для запросов, соединяющих более geqo_threshold (умолчание 12) отношений, планировщик переключается с исчерпывающего динамического программирования на Genetic Query Optimizer — эвристику, которая быстра, но не гарантирует оптимальность. Эвристика рандомизирована — одинаковый запрос может производить разные планы при разных запусках.

Для аналитических запросов с 15–30 соединёнными таблицами GEQO может производить нестабильные планы. Обходные пути:

-- Принудительный исчерпывающий поиск (медленнее планирование, стабильные планы):
SET geqo = off;

-- Зафиксировать порядок JOIN в явном порядке SQL:
SET from_collapse_limit = 1;
SET join_collapse_limit = 1;

Трюк с from_collapse_limit = 1 полезен, когда вы вручную настроили порядок JOIN сложного запроса и хотите, чтобы он оставался настроенным при изменениях данных.

JIT-компиляция

PG 11+ поддерживает JIT-компиляцию вычисления выражений (предикаты фильтрации, проекции, агрегация). JIT включается когда общая стоимость плана превышает jit_above_cost (умолчание 100,000).

JIT помогает CPU-bound запросам на больших сканированиях (миллионы строк, сложные выражения) на 10–30%. Для коротких OLTP-запросов добавляет накладные расходы — время компиляции превышает экономию времени выполнения.

-- Для OLTP-нагрузок, где JIT вредит больше, чем помогает:
SET jit_above_cost = 500000;
-- Или полностью отключить для OLTP-ролей:
SET jit = off;

Используйте EXPLAIN (ANALYZE, JIT) для просмотра тайминов JIT по фазам и принятия решения о настройке порогов.

Числа кеша планов и настройки планировщика
Кастомных планов до переключения на общий
5
Допустимая разница стоимости общего плана vs кастомного
~10%
geqo_threshold (умолчание)
12 отношений
jit_above_cost (умолчание)
100,000
jit_inline_above_cost (умолчание)
500,000
from_collapse_limit / join_collapse_limit
8 / 8
Время планирования на выполнение (типичный OLTP)
0.4-2 мс
random_page_cost (настройка для SSD)
1.1
Рекомендация effective_cache_size
~75% общей RAM
Викторина

Подготовленный оператор работал быстро 5 выполнений. На 6-м становится медленным. Какова наиболее вероятная причина?

Викторина

Какая возможность PostgreSQL 16 позволяет инспектировать какой план получат все выполнения подготовленного оператора после 6-го, без реального выполнения 5 раз?

Викторина

Вы установили random_page_cost = 1.1 на SSD-обеспеченном Postgres. Что происходит с планами для запросов с селективными предикатами?

Вспомните перед уходом
  1. 01
    Опишите полный жизненный цикл того, как Postgres выбирает план для параметризованного запроса — от первого выполнения до переключения на общий план.
  2. 02
    Почему effective_cache_size важен для планировщика, даже если он не выделяет никакой памяти?
Итог

Кеширование планов в Postgres имеет специфический режим отказа: после 5 выполнений подготовленного оператора Postgres сравнивает среднюю стоимость кастомного плана с общим планом и может навсегда переключиться на общий. При косых распределениях параметров общий план оптимален для «типичного» параметра и катастрофически медленен для выбросов — производя бимодальную задержку (низкое среднее, высокое stddev). Диагностируйте через stddev_exec_time в pg_stat_statements и EXPLAIN (GENERIC_PLAN) (PG 16+). Исправляйте через plan_cache_mode = 'force_custom_plan' для роли. Для SSD-систем устанавливайте random_page_cost = 1.1 — планировщик будет правильно предпочитать Index Scan над Seq Scan для селективных предикатов. Устанавливайте effective_cache_size в ~75% общей RAM. Отключайте или поднимайте пороги JIT для OLTP-нагрузок. Используйте join_collapse_limit = 1 для фиксации явного порядка JOIN для сложных аналитических запросов, которые GEQO может дестабилизировать.

Практика

Сделай это, чтобы превратить узнавание в навык.

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

Trademarks belong to their respective owners. Editorial reference only.