В прошлом уроке мы видели, как Postgres оценивает AND между предикатами по разным колонкам: sel(A AND B) = sel(A) × sel(B). Эта формула предполагает статистическую независимость — что значение в одной колонке не несёт информации о другой. Иногда это правда. Часто — нет.
WHERE city = 'Москва' AND country = 'RU' — классика. Если городов 1000, а стран 50, то независимо: 1/1000 × 1/50 = 0.00002. На таблице в 1M строк — оценка 20 строк. Реально — 50 000 (все Москвы — это RU, и в Москве ~5% населения). Ошибка 2500x, план разваливается.
CREATE STATISTICS (с Postgres 10) — это способ сказать планировщику: «эти колонки связаны, посчитай для них совместную статистику».
Три типа extended statistics
Команда: CREATE STATISTICS name [(type [, ...])] ON col1, col2 [, ...] FROM table;
Типов три, и они независимы (можно один, можно все):
ndistinct— число уникальных пар (или комбинаций) значений. Помогает сGROUP BY a, bи при оценке join.dependencies— функциональные зависимости между колонками. «Значениеcountryопределяется значениемcity». Используется для оценкиWHEREс конъюнкциями.mcv— multi-column MCV. Список самых частых комбинаций значений с их частотами. Самый точный, но дороже всех.
Можно создать только нужные — каждый тип решает свою задачу.
Где живёт extended statistics
В pg_statistic_ext (определение) и pg_statistic_ext_data (собственно данные после ANALYZE). Аналог pg_statistic — pg_stats_ext view для людей:
SELECT statistics_name, kind, columns, ...
FROM pg_stats_ext;
Главное: ANALYZE не строит extended stats автоматически. После CREATE STATISTICS нужно явно вызвать ANALYZE (на всю таблицу или конкретную), чтобы статистика заполнилась. После этого автовакуум подхватывает её регулярно.
Пример на customers
В нашем seed country распределён по id % 5 — равномерно. Это плохой пример для демонстрации корреляции. Сделаем синтетический случай прямо в песочнице.
Создаём таблицу addresses, где city и country сильно коррелированы. Датасет инициализируется ~5 секунд.
Здесь точная зависимость: city → country (каждый город принадлежит ровно одной стране). 10 уникальных городов, 5 стран, 10 уникальных пар.
Без extended stats: оценка для AND будет в 5 раз меньше реальной.
Оценка: sel(city='Москва') = 0.1, sel(country='RU') = 0.2, итог 0.02 → estimate ~1000. Реально: 5000 строк (все Москвы — это все RU из города 0). Ошибка 5x. На простом запросе это не критично, но в join’е может развалить план.
Добавляем extended statistics и пересобираем ANALYZE.
Теперь оценка должна стать близкой к реальной — rows ≈ 5000. Планировщик «понимает», что условия не независимы.
Как работает каждый тип
dependencies
Хранит коэффициент dep(city → country) — насколько значение country детерминируется значением city. От 0 (независимы) до 1 (country функционально зависит от city). Для нашего примера = 1.
Использование в оценке: если dep(city → country) = 1, то sel(city = 'Москва' AND country = 'RU') = sel(city = 'Москва') (правое условие «бесплатно», его уже гарантирует левое).
Это самая лёгкая статистика по объёму — несколько чисел на пару колонок.
ndistinct
Хранит n_distinct для комбинации колонок: сколько уникальных (city, country) пар существует. Без extended stats Postgres считает: n_distinct(city, country) = n_distinct(city) × n_distinct(country) = 10 × 5 = 50. Реально — 10 (потому что для каждого city существует ровно один country).
Это критично для GROUP BY city, country — без extended stats планировщик ожидает 50 групп, а реально 10. На больших таблицах это влияет на hash-table size и выбор Hash Aggregate vs Sort + GroupAggregate.
mcv
Multi-column MCV: точно хранит самые частые комбинации. Например, [(Москва, RU): 0.1, (Берлин, DE): 0.1, ...]. Это работает для предикатов с разным набором колонок (WHERE city = X, WHERE country = Y, WHERE city = X AND country = Y).
Самый точный тип, но и самый дорогой по объёму: до default_statistics_target комбинаций × количество строк хранения. Используй только когда dependencies недостаточно.
Когда какой тип нужен
dependencies— если у тебя есть один-ко-многим или функциональная зависимость (zip_code → city,model → brand).ndistinct— если ты часто делаешьGROUP BYпо нескольким колонкам, или join по составному ключу.mcv— если распределение комбинаций сильно skewed и важна точная оценка на конкретных горячих значениях.
Создавать «всё сразу» — нормально для важных таблиц. Стоимость: лишний CPU на ANALYZE + килобайты на хранение.
Посмотрим, что лежит в pg_stats_ext для наших addresses.
В выводе ожидаем: n_distinct ~ {"1, 2": 2} (2 уникальные пары), dependencies — JSON вида {"1 => 2": 1.0} (city полностью определяет country).
Ловушки и нюансы
- Не работает для
OR-предикатов. Extended stats применяются только к конъюнкциям (AND). - Не помогает с join’ами. Это статистика внутри одной таблицы. Для join’ов корреляции остаются «магическими константами».
- Требует обновления.
ANALYZEобновит, autovacuum тоже. Но если статистика устарела — оценки тоже устаревают. - Помогает не всегда. Если корреляция слабая (
dep = 0.3), эффект небольшой. Имеет смысл, когда зависимость близка к 1.
Чек-лист
sel(A AND B) = sel(A) × sel(B)верно только при независимости. Корреляции → недооценка.CREATE STATISTICS name (type, ...) ON col1, col2 FROM table— три типа:dependencies,ndistinct,mcv.- После CREATE нужен
ANALYZE, чтобы статистика заполнилась. dependencies— для AND-предикатов с функциональными зависимостями. Самый лёгкий.ndistinct— дляGROUP BYс несколькими колонками и join’ов по составному ключу.mcv— multi-column MCV, самый точный и дорогой.- Смотреть результат:
pg_stats_ext. Только AND, только внутри таблицы (не join).