Learning Platform
Урок 08.03 · 24 мин
Продвинутый
selectivityestimationpredicateMCVhistogram

Когда ты пишешь WHERE city = 'Москва', планировщик задаёт себе вопрос: сколько строк это вернёт? От ответа зависит всё — выбор индекса, join algorithm, порядок таблиц. Эта величина называется

selectivity
, и для каждого типа предиката есть своя формула. Здесь мы разберём механику — как именно pg_stats превращается в число rows=12345 в EXPLAIN.

Equality: col = X

Самый частый предикат. Алгоритм:

  1. Посмотреть, есть ли X в most_common_vals. Если да — взять соответствующее значение из most_common_freqs. Конец.
  2. Если нет — оценка (1 − sum(most_common_freqs) − null_frac) / (n_distinct − mcv_count). Логика: значение, которого нет в MCV, делит «оставшуюся вероятность» поровну между всеми не-MCV уникальными значениями.

Пример. Колонка status имеет MCV [paid: 0.30, pending: 0.20, shipped: 0.15], sum(freqs) = 0.65, null_frac = 0, n_distinct = 6.

  • WHERE status = 'paid' → 0.30 (из MCV напрямую).
  • WHERE status = 'cancelled' (не в MCV, но реально есть) → (1 − 0.65) / (6 − 3) = 0.35 / 3 ≈ 0.117.
  • WHERE status = 'unknown' (вообще нет такого значения) → тоже 0.117, потому что планировщик не отличает «есть, но редкое» от «нет вообще». Это известная слабость.
Equality estimation: MCV vs остальные

Если X в MCV — точная частота. Если нет — равномерная оценка по оставшимся.

Запрос: WHERE status = Xищем X в MCV
X в most_common_valsselectivity = most_common_freqs[i] (точно)
X не в MCV(1 − Σfreqs − null_frac) / (n_distinct − mcv_size)
estimated_rows = selectivity × reltuplesэто число пойдёт в EXPLAIN

Range: col > X, col < X, col BETWEEN A AND B

Для неравенств планировщик использует histogram_bounds. Логика:

  1. Посчитать долю MCV, удовлетворяющую условию (просто фильтр по массиву).
  2. Посчитать долю гистограммы выше/ниже X. Если X попадает между двумя баскетами bound[i] и bound[i+1], оценивается линейной интерполяцией: какая доля баскета выше X.
  3. Сложить.

Пример: histogram_bounds для signup_date имеет границы [2022-01-01, 2022-04-15, 2022-07-30, ..., 2024-12-31], 100 равночастотных баскетов. Запрос WHERE signup_date > 2024-01-01. Находим, что 2024-01-01 попадает в баскет 75 (например). Тогда selectivity = (101 − 75) / 100 = 0.26 — около 26% строк попадут.

Посмотрим histogram для signup_date и оценим WHERE signup_date > '2024-01-01'. Датасет инициализируется ~5 секунд.

PostgreSQL

EXPLAIN: смотрим, какую оценку планировщик даёт для предиката по дате.

PostgreSQL

В выводе видишь rows=... — это selectivity × reltuples. Сравни с реальным числом строк (запусти простой SELECT count(*) WHERE ...) — оценка обычно в пределах 10-20%.

IN: сумма по элементам

WHERE col IN (X1, X2, X3) оценивается как сумма selectivity отдельных равенств:

sel(IN) = sel(col = X1) + sel(col = X2) + sel(col = X3)

(с обрезкой до 1.0, если сумма больше). Например, при тех же MCV для status:

  • WHERE status IN ('paid', 'pending') → 0.30 + 0.20 = 0.50.

Это работает корректно, потому что значения в IN дизъюнктны (status не может быть одновременно 'paid' и 'pending'). Для больших IN-списков (тысячи значений) Postgres переключается на оценку через n_distinct — точное суммирование становится дорогим.

AND и OR

Здесь начинается фокус. Постгрес считает, что предикаты по разным колонкам независимы:

sel(p1 AND p2) = sel(p1) × sel(p2)
sel(p1 OR p2)  = sel(p1) + sel(p2) − sel(p1) × sel(p2)

Это допущение часто очень ошибочно. Классика — WHERE city = 'Москва' AND country = 'RU'. Если в таблице:

  • 0.1% строк с country = 'RU',
  • 0.5% строк с city = 'Москва',
  • но все Москва — это RU,

то реальная selectivity = 0.005 (= sel(city=‘Москва’) — все они уже RU). А планировщик скажет: 0.005 × 0.001 = 0.000005, в 1000 раз меньше. И выберет план под 1 ожидаемую строку вместо 5000.

Это та самая проблема, которую решают extended statistics (следующий урок).

Простой пример: оценка для AND и реальное число строк. Здесь корреляция небольшая, но идею демонстрирует.

PostgreSQL

В output ищем rows=A loops=B (estimated) и потом actual rows=C — если расхождение >> 2x, у тебя проблема с оценкой.

Selectivity vs cost: где это всё применяется

Selectivity не самоцель — это вход в формулу cost каждого узла плана:

  • Seq Scan: cost = n_pages × seq_page_cost + n_tuples × cpu_tuple_cost + filter_cost × n_tuples. Selectivity влияет на размер выхода, который пойдёт в следующий узел.
  • Index Scan: cost = index_pages × random_page_cost + heap_pages_estimate × random_page_cost + n_matched × cpu_tuple_cost. Selectivity напрямую определяет heap_pages_estimate.
  • Join: для nested loop важно, сколько строк во внешней таблице (= selectivity outer-фильтра), потому что внутренний цикл выполнится столько раз.

Если selectivity ошиблась в 100x — все эти cost-числа становятся бессмысленными, и Postgres выберет неправильный план.

Когда оценки врут систематически

Помимо корреляций между колонками, типичные ловушки:

  1. Predicate на функцию: WHERE lower(email) = '[email protected]'. Postgres не знает распределение lower(email), fallback = 0.005 (магическая константа). Может промахнуться. Решение — функциональный индекс.
  2. JOIN на колонке с разным распределением: customers JOIN orders ON country. Постгрес считает, что country у customer и у orders распределены одинаково, и берёт min(n_distinct). Часто верно, но для skewed-данных — нет.
  3. Свежие данные (см. прошлый урок): после массового INSERT статистика устаревает.
  4. LIKE / regex: WHERE name LIKE 'A%' оценивается через histogram по prefix — работает. А WHERE name LIKE '%suffix' — fallback на магическую константу.

Пример проблемного предиката: функция в WHERE. Сравни estimated и actual.

PostgreSQL

Видно, что планировщик не может «развернуть» lower(country) без функционального индекса и берёт fallback. Реальная selectivity — 20% (если 5 стран и одна из них RU).

Проверка знанийKnowledge check
Колонка status имеет MCV = ['paid', 'pending', 'shipped', 'delivered'] с частотами [0.4, 0.2, 0.1, 0.05]. null_frac = 0, n_distinct = 6. Какую selectivity Postgres даст для WHERE status = 'refunded'?
ОтветAnswer
'refunded' не в MCV. Формула: (1 − Σfreqs − null_frac) / (n_distinct − mcv_size). Σfreqs = 0.4 + 0.2 + 0.1 + 0.05 = 0.75. n_distinct − mcv_size = 6 − 4 = 2. Selectivity = (1 − 0.75 − 0) / 2 = 0.25 / 2 = 0.125. Постгрес «делит» оставшиеся 25% строк между двумя нерассмотренными значениями ('refunded' и, например, 'cancelled') поровну. Реальное распределение может быть совсем другим — например, refunded 1%, cancelled 24% — но статистика этого не знает. Если 'refunded' — горячий предикат, поднимай statistics_target или строй partial index.

Чек-лист

  • Equality: в MCV — точная частота, иначе равномерное деление остатка по n_distinct.
  • Range (>, <, BETWEEN): через histogram_bounds с линейной интерполяцией внутри баскета.
  • IN: сумма по элементам (с обрезкой до 1.0).
  • AND/OR между колонками: предполагается независимость → sel = sel1 × sel2. Часто врёт, если колонки коррелированы.
  • Функции в WHERE, LIKE с %-префиксом — fallback на магические константы, селективность врёт.
  • Если ошибка >> 2x — это серьёзный сигнал на оптимизацию (extended stats, partial index, переписать запрос).
WHERE и предикаты — точные правила фильтрации Бинарный поиск: O(log n) на отсортированном массиве

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

Результат: 0 из 0
Прикладной
Вопрос 1 из 4. Колонка country имеет MCV = ['RU', 'DE', 'US'] с частотами [0.4, 0.3, 0.2]. null_frac = 0. n_distinct = 5. Какую selectivity Postgres даёт для WHERE country = 'JP' (значение не в MCV)?

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

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

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

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