Learning Platform
Урок 08.05 · 22 мин
Продвинутый
costseq_page_costrandom_page_costcpu_tuple_costtuning

Каждый раз, когда ты делаешь 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% от стоимости обработки кортежа, потому что на кортеж приходится несколько операций.
Cost units: каноничные значения и физический смысл

Все числа относительны seq_page_cost = 1.0. Дефолты заточены под спиннинг-диск 1996 года.

seq_page_cost = 1.0референсная единица, остальные относительны
random_page_cost = 4.0HDD: random vs seq seek time ratio
cpu_tuple_cost = 0.01обработка кортежа = 1% страницы
cpu_operator_cost = 0.0025одна операция = 0.25% страницы
Total cost узла = α×SPC + β×RPC + γ×CTC + δ×COCлинейная комбинация, никакой нелинейности

Как складываются: пример 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’а планов на границе.

Что менять в реальности

  1. random_page_cost — главный параметр. На SSD/NVMe → 1.1. На HDD-RAID-10 → 2.5-3. На очень медленных HDD → 6.0+.
  2. seq_page_cost — обычно не трогают. Если меняют — кратно тащат за собой random_page_cost (отношение важнее абсолютных значений).
  3. cpu_tuple_cost, cpu_operator_cost — почти никогда. Они влияют на выбор между Hash Join и Nested Loop, между Sort и Hash Aggregate. Если у тебя CPU-bound запросы (много вычислений в SELECT) — можно поднять cpu_operator_cost, но это узкий случай.
  4. effective_cache_size (отдельный параметр, не cost-unit, но влияет на cost) — оценка размера кэша ОС + shared_buffers. Большое значение делает Index Scan дешевле (Postgres ожидает hit-ratio выше). На сервере с 64 GB RAM — обычно 48 GB.

Смотрим в действии

EXPLAIN с дефолтным random_page_cost. Датасет инициализируется ~5 секунд.

PostgreSQL

Запоминаем cost. Теперь снижаем random_page_cost и смотрим, изменится ли план или его стоимость.

Снижаем random_page_cost до 1.1 — стоимость Index Scan упадёт, Seq Scan останется. На границе план может перевернуться.

PostgreSQL

В простых случаях план тот же — Postgres и при 4.0 выбирает индекс. Но на запросах с большой выборкой (где Seq Scan на границе с Index Scan) это сместит баланс в сторону Index.

Запрос с большим predicate (~30% строк) — здесь random_page_cost решает между Seq Scan и Index Scan.

PostgreSQL

Сравни планы. Иногда видно, как при низком 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

Если для разных планов это соотношение сильно отличается — формула костинга врёт. Стандартный диагностический подход:

  1. Снять cost и actual для нескольких типичных запросов.
  2. Посчитать ratio cost/actual для каждого.
  3. Если 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 — было бы быстрее?»).

Проверка знанийKnowledge check
У тебя сервер с NVMe, random IOPS = 200K, sequential bandwidth = 3 GB/s. Какие cost-параметры будут разумны и почему?
ОтветAnswer
NVMe random latency ~5μs, sequential ~3μs/8KiB. Соотношение ~1.5-2:1. Разумно поставить random_page_cost = 1.1-1.2 (плюс небольшая премия за prefetch-misses), seq_page_cost = 1.0. Также имеет смысл уменьшить cpu_tuple_cost до 0.005 (потому что чтение страницы стало в ~10 раз быстрее, а CPU не ускорился пропорционально → CPU-часть теперь относительно дороже). effective_cache_size — поставить близко к (shared_buffers + 75% от свободной RAM). Главный практический эффект: Index Scan и Bitmap Scan станут привлекательнее в пограничных случаях, что обычно правильно на быстром storage. Дополнительно: на NVMe имеет смысл поднять effective_io_concurrency до 200+ (это разрешает читать несколько страниц параллельно при Bitmap Heap Scan).

Чек-лист

  • 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_* — для отладки, не для прода.
Memory hierarchy — registers, cache, RAM, disk и реальная latency EXPLAIN: четыре варианта анализа запросов в ClickHouse

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 4. Какое значение random_page_cost разумно для сервера с NVMe-диском?

Закончили урок?

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

Войдите чтобы оценить урок

Прогресс модуля
0 из 5