Learning Platform
Урок 02.05 · 24 мин
Продвинутый
Monitoringpg_relation_sizepgstattuplebloatpg_stat_user_tablesStorage analytics

Четыре предыдущих урока строили теорию: страницы, кортежи, fillfactor, файлы. Этот урок — прикладной. У тебя есть Postgres-сервер в проде, и тебе нужно ежедневно отвечать на вопросы: какие таблицы растут быстрее всего, где образуется bloat, какие TOAST’и переростают heap, какая HOT ratio падает. Здесь — готовый набор инструментов и SQL’ей для этого.

Базовая иерархия размеров

PostgreSQL предоставляет несколько функций, и важно понимать разницу:

Иерархия функций размера

От размера одной fork до полного размера таблицы со всеми индексами и TOAST.

pg_relation_size(rel)только основной heap-fork (main)
pg_relation_size(rel, 'fsm')только Free Space Map
pg_relation_size(rel, 'vm')только Visibility Map
pg_table_size(rel)heap + fsm + vm + TOAST (без индексов)
pg_indexes_size(rel)сумма всех индексов таблицы
pg_total_relation_size(rel)heap + forks + TOAST + индексы (полная картина)

Ключевая ошибка новичков — использовать pg_relation_size и думать, что это полный размер таблицы. На самом деле для таблицы с большим JSONB и парой индексов реальный размер легко в 10x больше того, что показывает pg_relation_size.

Сравнение всех функций размера для customers:

PostgreSQL

Для customers (только текстовые колонки, без больших JSONB) разница между heap и total обычно небольшая — таблица узкая, без TOAST. На реальных таблицах с TOAST разница может быть огромная.

Топ самых больших таблиц

Стандартный запрос для daily monitoring:

Топ-10 таблиц текущей БД по полному размеру (heap + TOAST + индексы). Сравни heap_size и total_size — если они сильно отличаются, у таблицы либо много индексов, либо большой TOAST:

PostgreSQL

Это базовая «обзорная» команда: при подключении к незнакомой БД её стоит запустить первой. Сразу видишь, где сосредоточены данные, и какие таблицы заслуживают внимания.

Dead tuples и bloat ratio

Bloat
— это раздутие таблицы из-за dead tuples. После каждого UPDATE/DELETE остаётся версия кортежа, помеченная как удалённая, но физически она там лежит, пока её не убрал VACUUM. Если автовакуум не успевает (или слишком ленив), bloat растёт.

Простейшая оценка через pg_stat_user_tables:

Bloat ratio по pg_stat_user_tables — это грубая оценка, но почти всегда достаточная для алертов:

PostgreSQL

Эмпирические пороги:

  • dead_pct < 10% — норма.
  • dead_pct = 10-25% — обрати внимание, проверь автовакуум.
  • dead_pct > 25% — алерт. Скорее всего, autovacuum не успевает; пора либо VACUUM руками, либо тюнинговать параметры (autovacuum_vacuum_scale_factor).

Замечание для pglite: автовакуум работает упрощённо или вовсе выключен — там этот запрос не покажет реальной динамики. На реальной БД он самый полезный.

pgstattuple: точная оценка bloat

pg_stat_user_tables показывает только то, что Postgres увидел при последнем VACUUM/ANALYZE. Реальный bloat может быть выше — например, dead tuples, оставшиеся от долгой транзакции, которая держит snapshot.

Точная оценка — через расширение pgstattuple:

CREATE EXTENSION IF NOT EXISTS pgstattuple;

SELECT * FROM pgstattuple('orders');

Возвращает:

  • table_len — полный размер heap в байтах;
  • tuple_count — число живых tuples;
  • tuple_len — суммарный размер живых tuples;
  • tuple_percent — % полезного содержимого;
  • dead_tuple_count, dead_tuple_len, dead_tuple_percent — то же про dead;
  • free_space — суммарный free space в страницах;
  • free_percent.

В pglite расширения pgstattuple обычно нет, но логику запоминай: на реальной БД ты будешь его использовать постоянно.

Альтернатива без расширения — статистическая оценка bloat через pg_class.reltuples, relpages и среднюю длину кортежа. Это не точно, но не требует прав суперюзера:

Грубая оценка bloat без pgstattuple: сравниваем фактический размер таблицы с ожидаемым по числу строк × средней длине:

PostgreSQL

bloat_ratio_pct > 150% — таблица существенно раздута. Этот же запрос (с поправкой на схему) — основа всех известных bloat-чеков (например, в check_postgres от Bucardo).

Размер индексов отдельно

Индексы тоже bloat’ятся, и часто сильнее, чем таблицы. Стандартный запрос:

Размеры всех индексов на customers и orders, в порядке убывания:

PostgreSQL

Колонка idx_scan — количество использований индекса с момента сброса статистики. Если индекс существует, но idx_scan = 0 и таблица активная — это неиспользуемый индекс, кандидат на удаление. Каждый неиспользуемый индекс — это: место на диске, замедление каждого INSERT/UPDATE, дополнительная нагрузка на autovacuum.

Скорость роста таблицы

Размер сам по себе не информативен — важна скорость роста. Простейший способ замерить: периодически снимать pg_total_relation_size и сравнивать. Если у тебя есть таблица аудита, в которой каждый день сохраняется размер всех таблиц:

CREATE TABLE storage_history (
  measured_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  table_name  TEXT NOT NULL,
  total_bytes BIGINT NOT NULL,
  PRIMARY KEY (table_name, measured_at)
);

-- ежедневный job:
INSERT INTO storage_history (table_name, total_bytes)
SELECT schemaname || '.' || relname, pg_total_relation_size(schemaname || '.' || relname)
FROM pg_stat_user_tables;

Потом — самый практичный запрос: «топ-таблиц по скорости роста за последние 7 дней»:

Симуляция: создаём storage_history с двумя замерами (5 дней назад и сейчас), считаем daily growth:

PostgreSQL

Этот тип отчёта показывает не что большое, а что становится большим. Часто алерт по росту срабатывает за неделю до того, как место кончится — даёт время среагировать.

TOAST и Free Space Map

Расширение pg_freespacemap (стандартное, нужно CREATE EXTENSION) даёт подробную карту FSM:

CREATE EXTENSION pg_freespacemap;

SELECT * FROM pg_freespace('orders') LIMIT 10;
-- блок, доступное место (округлено до 32 байт)

Это полезно при отладке: если на таблице много dead tuples и при этом FSM показывает много свободного места, но INSERT’ы продолжают аппендить новые страницы — что-то не так с автовакуумом или с долгими транзакциями, держащими snapshot.

В pglite этого расширения обычно нет; на реальной БД — ставится одной командой.

Размер TOAST отдельно

Часто полезно понять, какая часть полного размера — это TOAST:

Размер heap, TOAST и индексов раздельно, и доля каждой части:

PostgreSQL

Если toast_pct > 50% — таблица в первую очередь TOAST-bound. Запросы, выбирающие varlena-колонки, будут читать TOAST отдельно. Имеет смысл подумать: (1) вынести большие колонки в отдельную таблицу с lazy join, (2) сменить стратегию (EXTERNAL для уже-сжатых, MAIN для маленьких varlena), (3) включить колоночное сжатие lz4 (если ещё pglz).

Минимальный «daily storage report»

Собирая всё вышесказанное — минимальный отчёт, который стоит запускать раз в день:

Сводный отчёт: топ-таблиц по полному размеру, dead %, последний autovacuum, HOT ratio:

PostgreSQL

Этот запрос — основа большинства production-dashboard’ов. В Grafana / Datadog / etc. его обычно крутят раз в час и алертят на dead_pct > 25, hot_pct < 50, или резкое падение last_autovacuum.

Проверка знанийKnowledge check
У тебя в системе мониторинга алерт: «public.user_events: dead_pct = 67%, hot_pct = 12%, last_autovacuum = 14 дней назад, total_size = 80 GiB, n_live_tup = 50M». Опиши, что произошло, в каком порядке диагностировать, и какие три действия попробовать.
ОтветAnswer
Картина классическая для OLTP-таблицы с проблемами автовакуума. Гипотеза: на таблице много UPDATE'ов, но (1) либо обновляются индексированные колонки → cold updates → высокий dead %, низкий HOT %; (2) либо автовакуум остановлен/блокирован долгой транзакцией, поэтому 14 дней нет cleanup. Диагностика по шагам: (a) проверить pg_stat_activity на наличие транзакций старше нескольких часов (xact_start); если есть — терминировать или дождаться, иначе VACUUM не может убрать кортежи, видимые их snapshot. (b) проверить autovacuum_max_workers и не упирается ли он; SELECT * FROM pg_stat_progress_vacuum — если для этой таблицы вакуум в прогрессе. (c) проверить индексы: SELECT * FROM pg_stat_user_indexes WHERE relname='user_events' — найти, какие колонки в индексах и пересекаются ли с тем, что UPDATE'ится. Действия: (1) убить долгие транзакции, держащие snapshot; (2) ALTER TABLE user_events SET (autovacuum_vacuum_scale_factor = 0.02, autovacuum_vacuum_cost_limit = 2000) — сделать автовакуум для этой таблицы агрессивнее; (3) рассмотреть снижение fillfactor (например, 80) и удаление неиспользуемых индексов на горячих колонках — это поднимет HOT ratio и снизит будущий bloat.

Чек-лист

  • pg_relation_size — только heap (без TOAST и индексов). Часто вводит в заблуждение.
  • pg_total_relation_size — полный размер: heap + forks + TOAST + индексы. Использовать по умолчанию.
  • pg_indexes_size — сумма всех индексов; pg_relation_size(rel, 'fsm'/'vm') — отдельные forks.
  • pg_stat_user_tables — основной источник n_live_tup, n_dead_tup, n_tup_hot_upd, last_autovacuum. Это «грубая, но достаточная» аналитика.
  • pgstattuple — точный bloat (но требует расширения и обычно суперюзера).
  • pg_stat_user_indexesidx_scan = 0 на активной таблице → кандидат на DROP.
  • Топ-3 запроса для daily monitoring: топ по pg_total_relation_size, по dead_pct, по скорости роста (через свою таблицу истории).
  • Все алерты строй на относительных метриках (%), а не на абсолютных размерах — иначе они быстро устаревают.
VIEW и MATERIALIZED VIEW: как 'обзывать' запросы system.query_log: анализ медленных запросов в ClickHouse

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Чем pg_relation_size('orders') отличается от pg_total_relation_size('orders')?

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

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

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

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