Learning Platform
Урок 08.01 · 22 мин
Продвинутый
pg_statshistogramMCVn_distinctstatistics

В прошлых модулях мы видели, как Postgres физически хранит данные и выполняет план. Теперь главный вопрос: откуда планировщик знает, какой план выгоден? Он же не выполняет каждый предикат «понарошку» прежде, чем выбрать алгоритм. Ответ — он смотрит в pg_stats. Это маленькая, плотно упакованная сводка распределения значений в каждой колонке. Она занимает килобайты, но определяет каждое решение оптимизатора. Если статистика устарела — план выберется глупый, даже если запрос идеален.

В этом уроке мы препарируем pg_stats: разбираем каждое поле, смотрим на свой customers, и понимаем, почему 100 баксетов гистограммы — это и компромисс, и иногда узкое место.

pg_stats: вид сверху

pg_stats — это

view над pg_statistic
, по одной строке на (schema, table, column). Каждая строка — это сжатая «фотография» колонки на момент последнего ANALYZE. Не путать с 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 Scan cost.

Как Postgres делит данные: MCV + histogram

Идея проста:

MCV
ловит «толстый хвост» — значения, которые встречаются часто (например, status = 'paid' — 60% строк). Они хранятся точно с частотой. А всё остальное распихивается в гистограмму, где значения сортируются и делятся на равночастотные баскеты — так, что в каждом баскете примерно одинаковое число строк.

MCV ловит частое, histogram описывает остальное

Сначала вынимаются M самых частых значений (точные частоты), затем оставшиеся сортируются и делятся на 100 баскетов равной населённости.

Все значения колонкинапример, status у 100000 orders
MCV (most_common_vals)'paid' 0.30, 'pending' 0.20, 'shipped' 0.15...
Histogram (histogram_bounds)100 баскетов, каждый ~1% строк, на остатке после MCV
Что не попало никудаоценивается через n_distinct и null_frac

Если колонка имеет мало уникальных значений (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 секунд.

PostgreSQL

Ожидание: n_distinct = 5, most_common_vals = [RU, DE, US, IL, GE] (порядок зависит от частоты), most_common_freqs — примерно по 0.20.

Теперь колонка с высокой кардинальностью.

Статистика для signup_date — много уникальных значений, основная масса уйдёт в histogram_bounds.

PostgreSQL

n_distinct тут отрицательное — -0.x означает «уникальность примерно x от общего числа строк». hist_size будет ~101: default_statistics_target + 1 граница (n баскетов = n+1 границ).

Гистограмма signup_date целиком. Видно, что границы — это даты, равноотстоящие по числу строк, а не по времени.

PostgreSQL

Это equi-depth histogram: разница в баскетах не во времени, а в числе строк. Если новых клиентов завалило в апреле — баскет «апрель» физически короче, но содержит столько же строк, как декабрьский.

n_distinct: что значит отрицательное число

Поле трюковое:

  • n_distinct > 0 — абсолютное число уникальных значений. Используется, когда ANALYZE может уверенно оценить (мало уникальности).
  • n_distinct < 0 — доля от общего числа строк. Используется, когда уникальность высокая. -1.0 = все строки уникальны (как id).

Зачем такая дуальность? Потому что для маленьких справочников (status, country) число фиксировано независимо от размера таблицы. А для high-cardinality колонок (email, id) уникальность пропорциональна числу строк — её удобнее хранить как долю.

Сравним n_distinct для разных колонок:

PostgreSQL

Ожидаем: 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

Несколько типичных слепых пятен:

  1. Корреляции между колонкамиpg_stats хранит статистику по каждой колонке отдельно. Оптимизатор по умолчанию считает, что WHERE city='Москва' AND country='RU' независимы — а это очень часто не так (см. урок про extended statistics).
  2. Свежие данные после INSERT/UPDATE — пока ANALYZE (или autovacuum) не пройдёт, статистика устаревает. На горячих таблицах рекомендация: уменьшать autovacuum_analyze_scale_factor.
  3. TOAST-значения целикомmost_common_vals ограничен размером строки в pg_statistic. Для очень больших TEXT-значений MCV не строится, fallback — n_distinct.
Проверка знанийKnowledge check
Ты выполнил ALTER TABLE orders ALTER COLUMN status SET STATISTICS 1000, потом ANALYZE orders. Что изменилось в pg_stats для колонки status, если у неё всего 6 уникальных значений?
ОтветAnswer
Почти ничего. default_statistics_target определяет МАКСИМАЛЬНЫЙ размер MCV и числа баскетов histogram. Если уникальных значений всего 6 — все 6 поместятся в most_common_vals, и histogram_bounds останется пустым (или почти пустым). 1000 у нас просто не используется. Увеличение target имеет смысл для колонок с высокой кардинальностью и неравномерным распределением (например, status_id у событий, где есть редкие, но запрашиваемые в WHERE значения). Для status с 6 значениями увеличение target — мёртвая трата ресурсов на ANALYZE.

Чек-лист

  • 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).
EXPLAIN: четыре варианта анализа запросов в ClickHouse

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Что хранит most_common_vals (MCV) в pg_stats?

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

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

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

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