В прошлых модулях мы видели, как Postgres физически хранит данные и выполняет план. Теперь главный вопрос: откуда планировщик знает, какой план выгоден? Он же не выполняет каждый предикат «понарошку» прежде, чем выбрать алгоритм. Ответ — он смотрит в pg_stats. Это маленькая, плотно упакованная сводка распределения значений в каждой колонке. Она занимает килобайты, но определяет каждое решение оптимизатора. Если статистика устарела — план выберется глупый, даже если запрос идеален.
В этом уроке мы препарируем pg_stats: разбираем каждое поле, смотрим на свой customers, и понимаем, почему 100 баксетов гистограммы — это и компромисс, и иногда узкое место.
pg_stats: вид сверху
pg_stats — это
pg_statisticANALYZE. Не путать с pg_stat_user_tables (это runtime-статистика — кол-во scans, inserts, и т.д.).
Главные поля:
null_frac— доля NULL в колонке (от 0 до 1).avg_width— средняя ширина значения в байтах (для оценки размера результата).n_distinct— число уникальных значений. Положительное = абсолютное число, отрицательное = доля от общего числа строк (-0.5 = половина строк уникальна).most_common_vals(MCV) — массив самых частых значений.most_common_freqs— параллельный массив частот для MCV (тоже от 0 до 1).histogram_bounds— границы баскетов гистограммы для остальных значений (тех, что не попали в MCV).correlation— корреляция между физическим порядком на диске и порядком сортировки (от -1 до 1). Важно дляIndex Scancost.
Как Postgres делит данные: MCV + histogram
Идея проста:
status = 'paid' — 60% строк). Они хранятся точно с частотой. А всё остальное распихивается в гистограмму, где значения сортируются и делятся на равночастотные баскеты — так, что в каждом баскете примерно одинаковое число строк.
Сначала вынимаются M самых частых значений (точные частоты), затем оставшиеся сортируются и делятся на 100 баскетов равной населённости.
Если колонка имеет мало уникальных значений (status — 6 штук), почти всё уйдёт в MCV, гистограмма пустая. Если значений много и они распределены равномерно (id, placed_at), MCV почти пустой, всё лежит в гистограмме.
default_statistics_target
default_statistics_target — это сколько значений хранить в MCV и сколько баскетов в гистограмме. По умолчанию 100. Можно поменять:
- глобально через
postgresql.conf, - per-database через
ALTER DATABASE ... SET default_statistics_target = ..., - per-column через
ALTER TABLE ... ALTER COLUMN ... SET STATISTICS ....
Чем больше target — тем точнее статистика, но дольше ANALYZE и больше места под pg_statistic. Типичные ходы:
1000— для колонок с тяжёлыми skewed-распределениями и предикатами в горячих запросах.10— для colonok, по которым предикатов не бывает (просто хранятся).100— оставить как есть в 95% случаев.
Смотрим pg_stats для customers
Сначала убедимся, что ANALYZE отработал — наш seed для medium его явно вызывает.
Статистика для колонки country (5 стран → всё уйдёт в MCV). Датасет инициализируется ~5 секунд.
Ожидание: n_distinct = 5, most_common_vals = [RU, DE, US, IL, GE] (порядок зависит от частоты), most_common_freqs — примерно по 0.20.
Теперь колонка с высокой кардинальностью.
Статистика для signup_date — много уникальных значений, основная масса уйдёт в histogram_bounds.
n_distinct тут отрицательное — -0.x означает «уникальность примерно x от общего числа строк». hist_size будет ~101: default_statistics_target + 1 граница (n баскетов = n+1 границ).
Гистограмма signup_date целиком. Видно, что границы — это даты, равноотстоящие по числу строк, а не по времени.
Это equi-depth histogram: разница в баскетах не во времени, а в числе строк. Если новых клиентов завалило в апреле — баскет «апрель» физически короче, но содержит столько же строк, как декабрьский.
n_distinct: что значит отрицательное число
Поле трюковое:
n_distinct > 0— абсолютное число уникальных значений. Используется, когда ANALYZE может уверенно оценить (мало уникальности).n_distinct < 0— доля от общего числа строк. Используется, когда уникальность высокая.-1.0= все строки уникальны (какid).
Зачем такая дуальность? Потому что для маленьких справочников (status, country) число фиксировано независимо от размера таблицы. А для high-cardinality колонок (email, id) уникальность пропорциональна числу строк — её удобнее хранить как долю.
Сравним n_distinct для разных колонок:
Ожидаем: id ≈ -1, email ≈ -1, country = 5, is_vip = 2, signup_date ≈ -0.x.
correlation: почему важна для индекса
correlation — насколько порядок значений в колонке совпадает с физическим порядком на диске. Колонка id (SERIAL) почти всегда имеет correlation ≈ 1 — Postgres вставлял по возрастанию, и они так и лежат. Колонка email (выгляднет случайной) — correlation ≈ 0.
Зачем планировщику: при Index Scan по B-tree кортежи возвращаются в порядке индекса. Если correlation высокая — следующий ctid почти всегда на той же странице (последовательное чтение). Если correlation = 0 — каждый ctid на новой странице (random I/O, и random_page_cost бьёт по плану).
Что НЕ попадает в pg_stats
Несколько типичных слепых пятен:
- Корреляции между колонками —
pg_statsхранит статистику по каждой колонке отдельно. Оптимизатор по умолчанию считает, чтоWHERE city='Москва' AND country='RU'независимы — а это очень часто не так (см. урок про extended statistics). - Свежие данные после INSERT/UPDATE — пока
ANALYZE(или autovacuum) не пройдёт, статистика устаревает. На горячих таблицах рекомендация: уменьшатьautovacuum_analyze_scale_factor. - TOAST-значения целиком —
most_common_valsограничен размером строки вpg_statistic. Для очень больших TEXT-значений MCV не строится, fallback — n_distinct.
Чек-лист
pg_stats— view надpg_statistic, по одной строке на колонку. Обновляется черезANALYZE(вручную или autovacuum).- Ключевые поля:
null_frac,n_distinct,most_common_vals/most_common_freqs,histogram_bounds,correlation. - MCV хранит топ-N частых значений точно. Остальное — в equi-depth histogram с
default_statistics_targetбаскетами. default_statistics_target = 100по умолчанию. Можно крутить per-column черезALTER TABLE ... ALTER COLUMN ... SET STATISTICS.n_distinct < 0— доля, не абсолютное число.-1.0означает полную уникальность.- Корреляции между колонками здесь нет — для них смотри
CREATE STATISTICS(урок 4).