Статистика и промах в оценке
Планировщик оценивает строки из pg_statistic — n_distinct, списки MCV, гистограммы. Коррелированные колонки ломают допущение независимости; CREATE STATISTICS — фикс до того, как промах каскадирует в плохой join.
Дашбордовый запрос, соединяющий orders с line_items и фильтрующий WHERE city = 'Paris' AND country = 'France', год работал за 40 мс. Однажды утром он 22 секунды. Ничего не деплоили, всплеска данных нет. EXPLAIN ANALYZE показывает: фильтр оценён в 12 строк, факт 480 000 — промах в 40 000×. Планировщик, поверив в 12 строк, выбрал nested loop и теперь зондирует индекс 480 000 раз. Данные не менялись; корреляция между двумя колонками наконец сломала математику планировщика. Этот урок — про этот баг и его фикс в одну строку.
Что планировщик знает о твоих данных
Прежде чем чинить плохую оценку, нужно знать, что именно планировщик читает. Каждая оценка планировщика (урок 02) идёт из одного места: каталога pg_statistic, читаемого через вью pg_stats. ANALYZE (вручную или автоматически через autovacuum — урок 04) семплирует каждую таблицу и хранит три вещи на колонку. Глава статистики трека databases разбирает внутренности семплирования; здесь читаем это как рычаги, которые дёргает SQL-автор:
n_distinct— сколько различных значений в колонке. Ведёт оценку «сколько строк совпадёт с одним значением?» → примерноrow_count / n_distinct.- Most Common Values (
most_common_vals+most_common_freqs) — топ-значения и их точные частоты. Дляstatus = 'placed'планировщик не гадает; он читает сохранённую частоту. Поэтому перекошенные колонки хорошо оцениваются для частых значений. - Гистограмма (
histogram_bounds) — корзины равного числа строк для всего, что не в списке MCV, для диапазонных предикатов вродеtotal > 100.
-- Осмотри, во что планировщик верит про колонку:
SELECT attname, n_distinct, most_common_vals, most_common_freqs
FROM pg_stats
WHERE tablename = 'orders' AND attname = 'status';default_statistics_target (по умолчанию 100) управляет, сколько записей MCV и корзин гистограммы хранится. Подними его на колонке со множеством различных значений или сильным перекосом (ALTER TABLE orders ALTER COLUMN status SET STATISTICS 1000;), меняя более медленный ANALYZE на более резкие оценки.
Ловушка коррелированных колонок
Вот математика, которая ломается. По умолчанию планировщик оценивает многоколоночный фильтр, считая колонки независимыми: он перемножает их индивидуальные селективности. Для WHERE city = 'Paris' AND country = 'France':
оценка = P(city='Paris') × P(country='France') × row_countЕсли 1% строк — Paris, а 2% — France, планировщик предскажет 0.01 × 0.02 = 0.0002 → 0.02% строк. Но city и country функционально зависимы — каждая строка Paris и есть строка France. Реальная селективность — просто 1%, в пятьдесят раз выше. Планировщик недооценивает в ~50×, ждёт горстку строк, выбирает nested loop и детонирует, когда реальность выдаёт 480 000.
Это самый частый сеньорский промах оценки на проде, и он невидим, пока поверх него не построят join. Допущение независимости нормально для действительно несвязанных колонок; оно катастрофично для коррелированных (city/country, brand/model, zip/state, status/shipped_at).
Фикс — extended statistics — ты говоришь Postgres, что эти колонки ходят вместе:
CREATE STATISTICS orders_city_country (dependencies, ndistinct, mcv)
ON city, country FROM orders;
ANALYZE orders; -- extended stats заполняются только на следующем ANALYZEТри вида, каждый чинит свой промах:
dependencies— захватывает функциональную зависимость (Paris ⇒ France), чиня недооценку перемноженной селективности выше.ndistinct— захватывает комбинированное число различных значений группы колонок, чиня оценкиGROUP BY a, b(независимость пересчитывает группы).mcv— многоколоночный список most-common-values для перекошенных комбинаций.
Все три атакуют один корень: планировщик рассуждал о колонках по отдельности. Без dependencies получаешь взрывной nested loop; без ndistinct GROUP BY завышает число групп и тратит лишнюю память на хэш-таблицу; без mcv запросы по редким комбинациям получают усреднённую оценку, промахивающуюся в любую сторону.
После CREATE STATISTICS + ANALYZE тот же запрос перепланируется: оценка 480 000, факт 480 000, планировщик берёт hash join, 22 с → 60 мс. Без переписывания запроса, без индекса, без хинта — ты поправил оценку.
▸Почему это работает
Почему Postgres просто не собирает многоколоночную статистику автоматически для каждой пары? Комбинаторика. У таблицы из 30 колонок 435 пар колонок и куда больше троек; автосбор всех сделал бы ANALYZE квадратично дорогим и раздул каталог. Поэтому Postgres по умолчанию берёт дешёвое допущение независимости и даёт включить CREATE STATISTICS ровно на тех группах колонок, про которые ты знаешь, что они коррелированы. Найти их — детективная работа: проверь EXPLAIN ANALYZE на узлы, где оценочные и фактические строки резко расходятся на многоколоночном фильтре — этот зазор и есть подпись отсутствующего объекта extended statistics.
WHERE city='Paris' AND country='France' оценивается в 12 строк, но реально возвращает 480 000. В чём корневая причина?
Какой вид CREATE STATISTICS чинит недооценку от перемножения двух коррелированных одноколоночных селективностей?
Заполни пропуск: по умолчанию планировщик считает колонки _______, поэтому перемножает их селективности; для коррелированных колонок это сильно недооценивает, пока CREATE STATISTICS не скажет ему правду.
- 01Какие три статистики ANALYZE хранит на колонку и для чего каждая?
- 02Почему WHERE city='Paris' AND country='France' сильно недооценивается и как это починить?
- 03Как один промах оценки каскадирует в плохой join и как его заметить?
Каждая оценка планировщика восходит к pg_statistic, заполняемому ANALYZE: n_distinct (строк на значение), список MCV (точные частоты для частых значений, поэтому перекос оценивается хорошо) и гистограмма (диапазонные предикаты). Подними default_statistics_target на колонке, чтобы их заострить. Сеньорская ловушка — допущение независимости: для многоколоночного фильтра планировщик перемножает одноколоночные селективности, что нормально для несвязанных колонок, но недооценивает коррелированные — city='Paris' AND country='France' может промахнуться в 40 000×. Этот единственный промах каскадирует: веря в горстку строк, планировщик выбирает nested loop, который затем зондирует индекс сотни тысяч раз, превращая 40 мс в 22 с. Фикс — CREATE STATISTICS (dependencies, ndistinct, mcv) на коррелированной группе плюс ANALYZE — ты чинишь оценку, и планировщик сам заново выводит hash join. Охоться на это, проверяя EXPLAIN ANALYZE на большие зазоры оценка-против-факта на многоколоночных предикатах.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.
Примени это
Примени этот урок в реальном проекте.