26.1 ANALYZE собирает сводку
Статистику собирает команда ANALYZE (её же запускает autovacuum).
Она не читает всю таблицу - берёт случайную выборку (по умолчанию
около 30 000 строк) и по ней строит сводку, которую кладёт в
системную таблицу pg_statistic. Читать её удобнее через view
pg_stats:
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. Её ключевая идея: корзины не равной ширины, а равной площади. Границы выбираются так, чтобы в каждую корзину попадало примерно одинаковое число строк.
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.
EXPLAIN SELECT * FROM tickets WHERE flight_id > 40;
-- rows=... сверь с долей корзин правее значения 40 в histogram_bounds
Если предсказанная по гистограмме доля совпала с rows в плане -
ты только что повторил работу планировщика. Если нет - скорее всего
статистика устарела. Подробности расчёта - в selectivity-crossover.
26.5.1 Подводный камень: устаревшая статистика
Статистика - снимок на момент ANALYZE. Между запусками данные
меняются, а сводка нет. Самый болезненный случай - массовая
загрузка: залили миллион строк, и до следующего ANALYZE
планировщик думает, что таблица пустая или прежнего размера.
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 собирает статистику по группе колонок вместе и
снимает предположение о независимости там, где это нужно.
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 записывай ожидаемое.
Выполни
ANALYZE tickets;, затем достань гистограмму:SELECT histogram_bounds FROM pg_stats WHERE tablename='tickets' AND attname='flight_id';.Выбери значение X примерно посередине диапазона и предскажи долю строк с
flight_id > Xпо числу корзин правее.Сверь:
EXPLAIN SELECT * FROM tickets WHERE flight_id > X;- rows должно отвечать предсказанной доле.Построй коррелированный кейс:
EXPLAIN SELECT * FROM flights WHERE departure='SVO' AND arrival='LED';. Сравни rows с фактом черезEXPLAIN ANALYZE- оценка занижена (колонки считаются независимыми).Создай расширенную статистику:
CREATE STATISTICS f_da (dependencies) ON departure, arrival FROM flights; ANALYZE flights;.Повтори 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) чинит.
Контрольные вопросы
Откуда планировщик берёт число строк, если он не выполняет запрос?
Показать ответ
Из статистики в
pg_statistic, собранной заранее командойANALYZE. ANALYZE берёт случайную выборку строк (по умолчанию около 30 000) и строит сводку: долю NULL, число различных значений, список частых значений (MCV) с частотами и гистограмму. При планировании запроса селективность условий вычисляется из этой сводки, и из неё выводится оценка rows. Данные при этом не читаются - поэтому план зависит от свежести статистики, а не от текущего содержимого таблицы.Зачем частые значения хранят отдельно в MCV, а не в общей гистограмме?
Показать ответ
Потому что реальные распределения неравномерны: несколько значений встречаются очень часто, остальные редко. Если усреднить всё в гистограмму, оценка для популярных значений будет грубо неверной. MCV хранит самые частые значения с их точными частотами, поэтому условие по такому значению оценивается почти идеально. Гистограмма же строится по «хвосту» - значениям не из MCV - и описывает их равномерно. Разделение даёт точность и на частых, и на редких.
Почему гистограмма делает корзины равной площади, а не равной ширины?
Показать ответ
Чтобы точность была ровной по всему диапазону. Корзины равной площади содержат примерно одинаковое число строк (например, по 1% каждая при 100 корзинах). Там, где значения плотные, корзины узкие; где редкие - широкие. Это значит, что между любыми двумя соседними границами лежит известная фиксированная доля данных, и селективность диапазона оценивается одинаково хорошо везде. Корзины равной ширины давали бы плохую оценку в плотных участках распределения.
Запрос с двумя условиями по коррелированным колонкам получает заниженную оценку строк. Почему и как чинить?
Показать ответ
По умолчанию планировщик считает колонки независимыми и перемножает их селективности: P(A и B) = P(A) × P(B). Для коррелированных колонок (город и страна) это неверно - совместное условие отбирает почти столько же строк, сколько одно из них, а перемножение даёт заниженную долю. Чинится расширенной статистикой:
CREATE STATISTICS ... (dependencies) ON col1, col2плюсANALYZE. После этого планировщик учитывает зависимость и перестаёт занижать оценку.Какой первый шаг при подозрительно плохом плане и почему?
Показать ответ
Запустить
ANALYZEна задействованных таблицах и посмотреть, не сменился ли план. Самый частый корень плохих планов - устаревшая статистика: после массовой загрузки или большого изменения данных сводка не отражает реальность, и планировщик считает по старым числам. Симптом виден вEXPLAIN ANALYZEкак сильное расхождение между estimated rows и actual rows. Свежий ANALYZE обновляет статистику, и часто план чинится сам.