Learning Platform
Урок 08.02 · 22 мин
Продвинутый
ANALYZEsamplingautovacuumstatistics_targetreservoir sampling

В прошлом уроке мы смотрели на готовую статистику. А кто её строит? Команда ANALYZE. И главный вопрос — как она это делает на таблице в 500 миллионов строк, не сжигая 20 минут IO на каждый запуск.

Ответ: ANALYZE не сканирует всю таблицу. Он берёт случайный

сэмпл
, и по нему оценивает распределение всей колонки. Это даёт удивительный результат: время ANALYZE почти не зависит от размера таблицы. Но даёт и неприятный — на гигантских таблицах с длинным хвостом редких значений сэмпл их не увидит.

Размер сэмпла

Формула простая: sample_size = 300 × statistics_target. При default_statistics_target = 100 это 30 000 строк независимо от того, 100K строк в таблице или 100M. На таблице в 10K — ANALYZE прочитает всё. На таблице в 100M — он прочитает 0.03% строк, и по этим 30K оценит и MCV, и гистограмму, и корреляцию.

300 — это не случайное число: статистический анализ показал, что для оценки MCV/histogram с приемлемой точностью нужно ~300 строк на баскет. Если поднимаешь statistics_target до 1000 — ANALYZE будет читать 300 000 строк, и времени потратит втрое больше.

ANALYZE: фиксированный размер сэмпла, какая бы таблица ни была

При том же statistics_target число читаемых строк постоянно, но процент от таблицы меняется драматически.

Таблица 10K строкANALYZE читает 100% строк, ~0.1s
Таблица 1M строкANALYZE читает 3%, ~1-2s
Таблица 100M строкANALYZE читает 0.03%, ~30s
При statistics_target = 1000сэмпл = 300 000 строк, время × 10

Как ANALYZE выбирает строки

Алгоритм называется two-stage reservoir sampling (по работе Vitter):

  1. Стадия 1: выбирается случайный сэмпл страниц. Не строк! Если в таблице 10000 страниц, ANALYZE берёт sample_size случайных страниц и читает их целиком.
  2. Стадия 2: внутри прочитанных страниц reservoir-алгоритмом выбирается sample_size кортежей. Reservoir держит окно фиксированной длины: каждая новая строка с вероятностью sample_size/seen_so_far заменяет случайную существующую.

Почему такой двухуровневый алгоритм? Потому что чтение случайных строк через row-by-row было бы 30000 random I/O. А чтение случайных страниц даёт ~30000 строк дешевле — мы платим за чтение страницы один раз и забираем все её ~100 кортежей.

Побочный эффект: если у тебя на одной странице все одинаковые значения (например, разлили по партициям), то reservoir-сэмпл «недополучает» вариативности. Это известная проблема ANALYZE для partitioned tables с heavy cluster.

Когда сэмпла недостаточно

Есть несколько сценариев, где ANALYZE врёт:

  1. Очень редкие, но запрашиваемые значения. Допустим, в таблице 100M строк есть 50 строк со статусом 'critical'. Сэмпл в 30K строк увидит их с вероятностью 1 − (1 − 50/100M)^30000 ≈ 1.5%. Почти никогда. В итоге WHERE status = 'critical' оценивается как «нет такого значения» или «1 строка» — а реально их 50, и план через индекс был бы выгоден.
  2. Сильно skewed распределения с длинным хвостом. Например, лог событий, где 99.9% — 'click', 0.1% — другие 200 типов. MCV-список из 100 значений захватит топ-100 типов после 'click', но их частоты будут оценены по сэмплу, а не по реальности. Ошибка ~10-20%.
  3. Только что заинсёрченные данные. ANALYZE пока не успел. Текущая статистика говорит «таких значений нет» → план будет Seq Scan или ошибочный Index Scan с плохим селективатером.

Решения:

  • Поднимать statistics_target для конкретной колонки (ALTER TABLE ... ALTER COLUMN ... SET STATISTICS 1000).
  • Вручную дёргать ANALYZE после массивных загрузок (COPY не вызывает autovacuum).
  • Для редких значений — добавить partial index на «горячее» подмножество, тогда индекс будет рекомендован независимо от селективности.

Autovacuum: кто дёргает ANALYZE сам

ANALYZE руками никто не запускает в проде. Postgres делает это сам через

autovacuum daemon
. Логика проста:

  • Postgres считает в pg_stat_user_tables.n_mod_since_analyze — сколько INSERT/UPDATE/DELETE прошло с последнего ANALYZE.
  • Когда эта цифра превышает autovacuum_analyze_threshold + autovacuum_analyze_scale_factor × reltuples — таблица помечается для ANALYZE.
  • По умолчанию: threshold = 50, scale_factor = 0.1 → ANALYZE запустится после ~10% изменений в таблице.

Для большой таблицы это редко (10% от 100M = 10M изменений между ANALYZE — это часы или дни). Поэтому для горячих таблиц настраивают:

ALTER TABLE orders SET (
  autovacuum_analyze_scale_factor = 0.02,
  autovacuum_analyze_threshold = 1000
);

Это даст ANALYZE после 2% изменений, что для критичных таблиц обычно правильный баланс.

Смотрим в практике

Информация о последнем ANALYZE и текущей оценке reltuples. Датасет инициализируется ~5 секунд.

PostgreSQL

n_mod_since_analyze — это «бюджет» до следующего autoanalyze. last_analyze — руками, last_autoanalyze — autovacuum’ом.

Внесём 5000 INSERT'ов и посмотрим, как изменился n_mod_since_analyze. До этого ANALYZE был выполнен в seed.

PostgreSQL

Видно, что n_mod_since_analyze поднялся на 5000. Когда он превысит 0.1 × n_live_tup, autovacuum пометит таблицу для ANALYZE.

Запустим ANALYZE вручную и проверим, что счётчик обнулился.

PostgreSQL

n_mod_since_analyze = 0, last_analyze обновился. Если бы это был autovacuum — обновился бы last_autoanalyze.

Sample size vs accuracy: считаем руками

Пусть колонка имеет распределение с пиком (значение X — 0.1% строк). Сэмпл в 30K увидит X ожидаемо 30 раз. Это даёт оценку частоты с относительной ошибкой ~20% (стандартная для биномиального распределения: σ = √(np), у нас np = 30, σ ≈ 5.5, относительная — 18%).

Если поднять statistics_target = 1000 → sample = 300K, увидим 300 случаев, относительная ошибка σ/np ≈ √300/300 = 5.7%. Точнее, но ANALYZE будет читать в 10 раз больше страниц.

Правило большого пальца: не трогай statistics_target, пока не докажешь через EXPLAIN ANALYZE, что планировщик ошибается из-за сэмпла.

Проверка знанийKnowledge check
У тебя таблица 1 миллиард строк с колонкой error_code. Реальное распределение: 'OK' — 99.99%, и 50 редких ошибок ('OOM', 'TIMEOUT', ...) — по 0.0001% каждая. Что увидит ANALYZE с default_statistics_target = 100?
ОтветAnswer
Sample size = 30000. Ожидаемое число строк с конкретной редкой ошибкой в сэмпле: 30000 × 0.000001 = 0.03 — то есть почти всегда 0. ANALYZE построит MCV = ['OK'] с частотой 1.0, а histogram_bounds для error_code будет пустым (потому что после MCV не осталось значений в сэмпле). Запрос WHERE error_code = 'OOM' будет оценён как «нет такого значения» → estimate = 1 row, и планировщик выберет Index Scan. По счастью, для этого случая Index Scan и правда оптимален — но планировщик «угадал» случайно. Если бы реальное число было 100K и оценка 1 — то Index Scan вместо нужного Bitmap Scan дал бы тормоза. Решение: ALTER TABLE ... ALTER COLUMN error_code SET STATISTICS 5000 → sample = 1.5M → каждая редкая ошибка будет встречена 1-2 раза, попадёт в MCV.

Чек-лист

  • ANALYZE строит статистику по случайному сэмплу, не по полной таблице.
  • Размер сэмпла: 300 × statistics_target строк (= 30000 по умолчанию). Не зависит от размера таблицы.
  • Алгоритм: two-stage reservoir sampling — сначала случайные страницы, потом reservoir по строкам.
  • Редкие значения (< 1/sample_size = 0.003%) почти никогда не попадают в MCV → ошибки оценки.
  • autovacuum_analyze_scale_factor = 0.1 по умолчанию (= ANALYZE раз в 10% изменений). Для горячих таблиц снижают до 0.02 или меньше.
  • Поднимать statistics_target стоит только после доказательства, что планировщик ошибается. Это удорожает ANALYZE линейно.
Инструменты измерений: timeit, sys.getsizeof, pympler, tracemalloc, perf Part Naming, Wide/Compact, Sparse Columns — итоги модуля хранилища

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Сколько строк по умолчанию читает ANALYZE из таблицы в 1 миллиард строк?

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

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

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

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