open atlas
↑ К треку
SQL и PostgreSQL вглубь SQL · 08 · 03

Статистика и промах в оценке

Планировщик оценивает строки из pg_statistic — n_distinct, списки MCV, гистограммы. Коррелированные колонки ломают допущение независимости; CREATE STATISTICS — фикс до того, как промах каскадирует в плохой join.

SQL Senior ◷ 16 min
Уровень
ОсновыJuniorMiddleSenior

Дашбордовый запрос, соединяющий 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 не скажет ему правду.

Вспомните перед уходом
  1. 01
    Какие три статистики ANALYZE хранит на колонку и для чего каждая?
  2. 02
    Почему WHERE city='Paris' AND country='France' сильно недооценивается и как это починить?
  3. 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-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 8 завершено
Связанные уроки

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

Примени это

Примени этот урок в реальном проекте.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.