linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
Intro
Lessons
Footer
linuxlab-УчебникиЦеныО платформеКонфиденциальность и куки
Copyright © 2026 LinuxLab. Все права защищены.
linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
  • Введение
  • Главы
  • How it worksскоро
  • Уроки
  • База знаний
  • Собеседование
Часть VI — Планировщик и выполнение

$ глава 26 · 55 минут

Статистика планировщика

Планировщик считает стоимость из числа строк, а число строк - из селективности условий. Но откуда он знает, что departure = 'SVO' пройдут 80% строк, а flight_id = 7 - две сотых процента? Он не смотрит в данные при планировании. Он смотрит в заранее собранную сводку: гистограммы и списки частых значений в pg_statistic.

В этой главе мы вскроем эту сводку через pg_stats, поймём, как из гистограммы выводится селективность > X, и предскажем оценку планировщика вручную. А потом упрёмся в его слабое место - предположение о независимости колонок - и починим его расширенной статистикой.

26.1 ANALYZE собирает сводку

Статистику собирает команда ANALYZE (её же запускает autovacuum). Она не читает всю таблицу - берёт случайную выборку (по умолчанию около 30 000 строк) и по ней строит сводку, которую кладёт в системную таблицу pg_statistic. Читать её удобнее через view pg_stats:

sql
ANALYZE flights;
SELECT attname, null_frac, n_distinct, most_common_vals, histogram_bounds
FROM pg_stats WHERE tablename = 'flights';

Здесь всё, на что планировщик опирается о колонке: доля NULL, число различных значений, список самых частых значений и гистограмма распределения остальных. Это и есть его картина мира. Если она устарела - решения будут неверные, хотя сама модель стоимости работает безупречно.

26.2 Четыре кирпича статистики колонки

Для каждой колонки pg_stats хранит несколько ключевых полей:

ПолеЧто значит
null_fracдоля NULL в колонке
n_distinctчисло различных значений (или их доля со знаком минус)
most_common_vals (MCV)список самых частых значений
most_common_freqsих частоты, в той же позиции
histogram_boundsграницы корзин для остальных значений
correlationнасколько физический порядок совпадает с порядком значений

Селективность любого условия планировщик собирает из этих кусков. Для col = 'X', если 'X' есть в MCV, он берёт частоту прямо оттуда - точное число. Если нет в MCV - считает, что значение «обычное», и оценивает как (1 − сумма частот MCV − null_frac) / число оставшихся различных значений.

26.3 MCV: частые значения - отдельно

Распределения почти никогда не равномерны: пара значений встречается на каждом шагу, остальные - редко. Если бы планировщик считал все значения одинаково частыми, он бы грубо ошибался на «популярных». Поэтому самые частые значения вынесены в отдельный список MCV с точными частотами, а гистограмма строится только по остальным.

Это разделение и даёт точность. Запрос WHERE departure = 'SVO', где SVO - частое значение, оценивается по его записи в MCV почти идеально. Запрос по редкому значению, которого в MCV нет, оценивается через равномерное предположение по «хвосту». Сколько значений попадёт в MCV, регулирует default_statistics_target (по умолчанию 100): больше target - длиннее MCV и гистограмма, точнее оценки, но дороже ANALYZE.

26.4 Гистограмма - корзины равной площади

Гистограмма описывает распределение значений, которых нет в MCV. Её ключевая идея: корзины не равной ширины, а равной площади. Границы выбираются так, чтобы в каждую корзину попадало примерно одинаковое число строк.

sql
SELECT histogram_bounds FROM pg_stats
WHERE tablename = 'tickets' AND attname = 'flight_id';
-- {1,6,11,17,22,28,...,50}

Если в массиве 101 граница, это 100 корзин, и в каждой - примерно 1% строк. Поэтому там, где значения плотные, корзины узкие, а где редкие - широкие. Такое устройство даёт ровную точность по всему диапазону: на любом участке планировщик знает, что между двумя соседними границами лежит ровно 1% данных.

26.5 Селективность диапазона своими руками

Гистограмма позволяет оценить col > X без обращения к данным. Логика: найти, в какую корзину попадает X, и сложить доли всех корзин правее.

Пусть 100 корзин (каждая - 1% строк), а граница X приходится ровно между 80-й и 81-й границей. Тогда правее лежит 20 корзин - примерно 20% строк. Селективность col > X ≈ 0.20, и для таблицы в 10 000 строк планировщик оценит rows ≈ 2000.

sql
EXPLAIN SELECT * FROM tickets WHERE flight_id > 40;
-- rows=... сверь с долей корзин правее значения 40 в histogram_bounds

Если предсказанная по гистограмме доля совпала с rows в плане - ты только что повторил работу планировщика. Если нет - скорее всего статистика устарела. Подробности расчёта - в selectivity-crossover.

26.5.1 Подводный камень: устаревшая статистика

Статистика - снимок на момент ANALYZE. Между запусками данные меняются, а сводка нет. Самый болезненный случай - массовая загрузка: залили миллион строк, и до следующего ANALYZE планировщик думает, что таблица пустая или прежнего размера.

sql
EXPLAIN SELECT * FROM tickets WHERE flight_id = 7;  -- rows=1 (по старой статистике)
-- ... загрузили данные ...
ANALYZE tickets;
EXPLAIN SELECT * FROM tickets WHERE flight_id = 7;  -- rows=900 (после обновления)

Симптом устаревшей статистики - большое расхождение между rows в EXPLAIN и фактическим числом в EXPLAIN ANALYZE. Первое, что делают при «непонятно плохом плане», - запускают ANALYZE и смотрят, не починилось ли. Autovacuum делает это сам по порогам, но сразу после большой загрузки ANALYZE стоит позвать руками.

26.6 Слабое место: независимость колонок

По умолчанию планировщик считает колонки независимыми. Селективность двух условий он перемножает: P(A и B) = P(A) × P(B). Когда колонки и правда независимы - это верно. Когда коррелированы - грубая ошибка.

Классика - город и страна. city = 'СПб' имеет селективность, и country = 'Россия' имеет селективность. Но СПб всегда в России, поэтому условие city = 'СПб' AND country = 'Россия' отбирает ровно столько же строк, сколько одно city = 'СПб'. Планировщик же перемножит две доли и оценит совместное условие в разы заниженно - а на заниженной оценке выберет неподходящий план.

Это не баг, а сознательное упрощение: хранить совместное распределение всех пар колонок невозможно. Но там, где корреляция есть и мешает, её можно явно подсказать.

26.7 Расширенная статистика

CREATE STATISTICS собирает статистику по группе колонок вместе и снимает предположение о независимости там, где это нужно.

sql
CREATE STATISTICS flights_dep_arr (dependencies, ndistinct)
  ON departure, arrival FROM flights;
ANALYZE flights;

Три вида расширенной статистики:

  • dependencies - функциональные зависимости (departure почти определяет arrival): планировщик перестаёт перемножать их селективности;
  • ndistinct - число различных комбинаций группы колонок (важно для GROUP BY по нескольким колонкам);
  • mcv - частые комбинации значений группы, как MCV, но по нескольким колонкам сразу.

После CREATE STATISTICS и ANALYZE оценка rows для коррелированных условий приближается к реальной, и план исправляется. Подробнее - в extended-statistics.

Уроки в sandbox

lab-26.1. Предсказать селективность по гистограмме и починить корреляцию

Сначала достанем гистограмму и предскажем оценку rows руками. Потом сломаем планировщика коррелированными колонками и починим через CREATE STATISTICS. Перед каждым EXPLAIN записывай ожидаемое.

  1. Выполни ANALYZE tickets;, затем достань гистограмму: SELECT histogram_bounds FROM pg_stats WHERE tablename='tickets' AND attname='flight_id';.

  2. Выбери значение X примерно посередине диапазона и предскажи долю строк с flight_id > X по числу корзин правее.

  3. Сверь: EXPLAIN SELECT * FROM tickets WHERE flight_id > X; - rows должно отвечать предсказанной доле.

  4. Построй коррелированный кейс: EXPLAIN SELECT * FROM flights WHERE departure='SVO' AND arrival='LED';. Сравни rows с фактом через EXPLAIN ANALYZE - оценка занижена (колонки считаются независимыми).

  5. Создай расширенную статистику: CREATE STATISTICS f_da (dependencies) ON departure, arrival FROM flights; ANALYZE flights;.

  6. Повтори EXPLAIN ANALYZE того же запроса - estimated rows теперь ближе к actual.

sandbox с автопроверкой - открыть в песочнице

Резюме

  • Планировщик не читает данные при планировании - он берёт сводку из pg_statistic (view pg_stats), которую собирает ANALYZE по случайной выборке.
  • Ключевые поля колонки: null_frac, n_distinct, most_common_vals + freqs, histogram_bounds, correlation. Из них собирается селективность любого условия.
  • Частые значения вынесены в MCV с точными частотами - по ним оценка почти идеальна; редкие оцениваются через гистограмму и равномерное предположение.
  • Гистограмма - корзины равной площади: границы выбраны так, что в каждой ~1% строк. Это даёт ровную точность по всему диапазону значений.
  • Селективность `col > X` выводится из числа корзин правее X; предсказанную долю можно сверить с rows в EXPLAIN до выполнения запроса.
  • Устаревшая статистика - частый корень плохих планов; симптом - расхождение rows и actual. Лечится ANALYZE, особенно сразу после массовой загрузки.
  • По умолчанию колонки считаются независимыми (селективности перемножаются); для коррелированных это занижает оценку. CREATE STATISTICS (dependencies/ndistinct/mcv) чинит.

Контрольные вопросы

  1. Откуда планировщик берёт число строк, если он не выполняет запрос?

    Показать ответ

    Из статистики в pg_statistic, собранной заранее командой ANALYZE. ANALYZE берёт случайную выборку строк (по умолчанию около 30 000) и строит сводку: долю NULL, число различных значений, список частых значений (MCV) с частотами и гистограмму. При планировании запроса селективность условий вычисляется из этой сводки, и из неё выводится оценка rows. Данные при этом не читаются - поэтому план зависит от свежести статистики, а не от текущего содержимого таблицы.

  2. Зачем частые значения хранят отдельно в MCV, а не в общей гистограмме?

    Показать ответ

    Потому что реальные распределения неравномерны: несколько значений встречаются очень часто, остальные редко. Если усреднить всё в гистограмму, оценка для популярных значений будет грубо неверной. MCV хранит самые частые значения с их точными частотами, поэтому условие по такому значению оценивается почти идеально. Гистограмма же строится по «хвосту» - значениям не из MCV - и описывает их равномерно. Разделение даёт точность и на частых, и на редких.

  3. Почему гистограмма делает корзины равной площади, а не равной ширины?

    Показать ответ

    Чтобы точность была ровной по всему диапазону. Корзины равной площади содержат примерно одинаковое число строк (например, по 1% каждая при 100 корзинах). Там, где значения плотные, корзины узкие; где редкие - широкие. Это значит, что между любыми двумя соседними границами лежит известная фиксированная доля данных, и селективность диапазона оценивается одинаково хорошо везде. Корзины равной ширины давали бы плохую оценку в плотных участках распределения.

  4. Запрос с двумя условиями по коррелированным колонкам получает заниженную оценку строк. Почему и как чинить?

    Показать ответ

    По умолчанию планировщик считает колонки независимыми и перемножает их селективности: P(A и B) = P(A) × P(B). Для коррелированных колонок (город и страна) это неверно - совместное условие отбирает почти столько же строк, сколько одно из них, а перемножение даёт заниженную долю. Чинится расширенной статистикой: CREATE STATISTICS ... (dependencies) ON col1, col2 плюс ANALYZE. После этого планировщик учитывает зависимость и перестаёт занижать оценку.

  5. Какой первый шаг при подозрительно плохом плане и почему?

    Показать ответ

    Запустить ANALYZE на задействованных таблицах и посмотреть, не сменился ли план. Самый частый корень плохих планов - устаревшая статистика: после массовой загрузки или большого изменения данных сводка не отражает реальность, и планировщик считает по старым числам. Симптом виден в EXPLAIN ANALYZE как сильное расхождение между estimated rows и actual rows. Свежий ANALYZE обновляет статистику, и часто план чинится сам.

← Предыдущая25-cost-modelСледующая →27-access-methods
Footer
linuxlab-
Copyright © 2026 LinuxLab. Все права защищены.
Учебники
Цены
О платформе
Конфиденциальность и куки