Каждый раз, когда ты делаешь EXPLAIN, видишь магическое число вроде (cost=12.34..56789.01 rows=1234 width=64). Откуда оно? Это не миллисекунды, не байты, не I/O — это безразмерные единицы. Они получаются как линейная комбинация четырёх параметров из postgresql.conf:
seq_page_cost = 1.0(стоимость прочитать одну страницу последовательно).random_page_cost = 4.0(стоимость прочитать одну случайную страницу).cpu_tuple_cost = 0.01(CPU на обработку одного кортежа в плане).cpu_operator_cost = 0.0025(CPU на одну операцию — сравнение, арифметика, hash).
Любая величина cost в плане — это α × seq_page_cost + β × random_page_cost + γ × cpu_tuple_cost + δ × cpu_operator_cost. Меняешь параметры — меняются все планы.
Семантика дефолтов
Дефолты записаны относительно seq_page_cost = 1.0. Это референсная единица: «прочитать страницу последовательно стоит 1». Все остальные — отношения к ней.
random_page_cost = 4.0означает «случайная страница в 4 раза дороже последовательной». Это число подобрано для HDD, где random seek ~10ms, sequential ~2.5ms. Соотношение 4:1.cpu_tuple_cost = 0.01— обработать кортеж стоит 1% страницы. Это потому что в страницу влезает ~100 кортежей, и обработка всех кортежей страницы должна примерно равняться стоимости их чтения.cpu_operator_cost = 0.0025— типичная операция (сравнение, hash) стоит 0.25% страницы. Это ~25% от стоимости обработки кортежа, потому что на кортеж приходится несколько операций.
Все числа относительны seq_page_cost = 1.0. Дефолты заточены под спиннинг-диск 1996 года.
Как складываются: пример Seq Scan
Seq Scan customers (cost=0.00..170.00 rows=10000):
cost = pages × seq_page_cost + tuples × cpu_tuple_cost
= 100 × 1.0 + 10000 × 0.01
= 100 + 100
= 200
(Реальное число в EXPLAIN может отличаться на ~10% — фильтр добавляет cpu_operator_cost за каждое сравнение.)
Seq Scan — простой план: читаем все страницы последовательно, для каждой строки выполняем CPU-работу. Cost линеен по размеру таблицы.
Пример Index Scan
Index Scan using customers_pkey on customers (cost=0.29..8.31 rows=1):
cost = index_pages × random_page_cost -- спуск по B-tree (3-4 уровня)
+ heap_pages × random_page_cost -- чтение страниц с найденными строками
+ tuples × cpu_tuple_cost -- обработка найденных строк
+ tuples × cpu_operator_cost × keys -- сравнения по ключу
Для запроса по primary key (одна строка): ~3 × 4.0 + 1 × 4.0 + 1 × 0.01 + 1 × 0.0025 = 16.0125. Постгрес округлит до 8.31, потому что использует более сложную формулу для частично-кэшированных страниц.
Главное наблюдение: random_page_cost — самый сильный множитель в Index Scan. Если он 4.0 — индекс «дорог», даже на 1 строку cost ~16. На больших выборках это убивает план быстро.
Как меняются под SSD/NVMe
Дефолты 1996 года. Сегодня:
- SSD SATA: random seek ~150μs, sequential ~50-100μs/8KiB. Соотношение 1.5:1 – 2:1.
- NVMe: random ~50μs, sequential ~10μs. Соотношение 5:1, но абсолютные числа гораздо ниже, и сэйчер на CPU.
- Cloud (EBS, Cloud SQL): разное, но обычно 1.5-2.
Стандартная рекомендация для SSD/NVMe:
ALTER SYSTEM SET random_page_cost = 1.1;
SELECT pg_reload_conf();
Зачем 1.1, а не 1.0? Потому что даже на NVMe чисто-случайный access чуть медленнее последовательного (TLB misses, prefetch не работает). И — главное — мы хотим, чтобы при выборе между Seq Scan и Index Scan Postgres немного предпочитал Seq Scan на больших выборках. 1.0 сравняло бы их, и было бы много flipping’а планов на границе.
Что менять в реальности
random_page_cost— главный параметр. На SSD/NVMe → 1.1. На HDD-RAID-10 → 2.5-3. На очень медленных HDD → 6.0+.seq_page_cost— обычно не трогают. Если меняют — кратно тащат за собойrandom_page_cost(отношение важнее абсолютных значений).cpu_tuple_cost,cpu_operator_cost— почти никогда. Они влияют на выбор междуHash JoinиNested Loop, междуSortиHash Aggregate. Если у тебя CPU-bound запросы (много вычислений вSELECT) — можно поднятьcpu_operator_cost, но это узкий случай.effective_cache_size(отдельный параметр, не cost-unit, но влияет на cost) — оценка размера кэша ОС + shared_buffers. Большое значение делает Index Scan дешевле (Postgres ожидает hit-ratio выше). На сервере с 64 GB RAM — обычно 48 GB.
Смотрим в действии
EXPLAIN с дефолтным random_page_cost. Датасет инициализируется ~5 секунд.
Запоминаем cost. Теперь снижаем random_page_cost и смотрим, изменится ли план или его стоимость.
Снижаем random_page_cost до 1.1 — стоимость Index Scan упадёт, Seq Scan останется. На границе план может перевернуться.
В простых случаях план тот же — Postgres и при 4.0 выбирает индекс. Но на запросах с большой выборкой (где Seq Scan на границе с Index Scan) это сместит баланс в сторону Index.
Запрос с большим predicate (~30% строк) — здесь random_page_cost решает между Seq Scan и Index Scan.
Сравни планы. Иногда видно, как при низком RPC Postgres переключается с Seq Scan на Bitmap Index Scan.
Калибровка: как понять, ошибся ли я
Способ один: сравнить cost-оценку с actual time в EXPLAIN ANALYZE.
Cost: 100..200 → ratio 100 cost units per second
Actual: 50ms = 0.05s → 100 cost / 0.05 s = 2000 cost units/sec
Если для разных планов это соотношение сильно отличается — формула костинга врёт. Стандартный диагностический подход:
- Снять cost и actual для нескольких типичных запросов.
- Посчитать ratio cost/actual для каждого.
- Если SeqScan-запросы имеют ratio 1, а Index Scan — 10 → у тебя
random_page_costслишком высокий.
Этот процесс называется cost calibration и в большинстве проектов им не занимаются — оставляют дефолты или меняют random_page_cost на 1.1 «потому что SSD».
Параметры без cost: hint’ы для планировщика
Есть параметры, не связанные с cost напрямую, но влияющие на выбор плана:
enable_seqscan = on/off— запретить Seq Scan. Постгрес найдёт любой другой план, даже если он в 100 раз дороже.enable_indexscan,enable_bitmapscan,enable_hashjoin,enable_mergejoin,enable_nestloop— аналогично.default_statistics_target = 100(с него начинался первый урок).from_collapse_limit = 8,join_collapse_limit = 8— после этого числа таблиц планировщик переключается с экзогенного перебора на GEQO (генетический).
Это для отладки, не для прода. В проде enable_* крутят только для proof of concept («а если бы был Hash Join — было бы быстрее?»).
Чек-лист
- Cost — безразмерная величина, линейная комбинация 4 параметров.
seq_page_cost = 1.0— референс.random_page_cost = 4.0— для HDD. На SSD/NVMe → 1.1.cpu_tuple_cost = 0.01,cpu_operator_cost = 0.0025— почти никогда не трогаются.- Главный параметр для тюнинга —
random_page_cost. Снижение → больше Index Scan’ов. effective_cache_size— отдельно, но влияет на cost индексов. Ставить ~75% RAM.- Калибровка: считать
cost / actual_time, искать узлы, где ratio выбивается. enable_*— для отладки, не для прода.