Когда приложение начинает тормозить, первый вопрос — «что происходит в базе?». Без инструментов ответ строится на догадках. PostgreSQL накапливает статистику о каждом запросе, каждой таблице, каждом соединении — нужно только знать, где смотреть.
Разные инструменты ловят разное: лог отбирает запросы по времени одного вызова, а pg_stat_statements складывает время всех вызовов — и на одной и той же нагрузке они показывают разных виновников.
Лог ловит длинный вызов, pg_stat_statements — дорогой в сумме: частый запрос на 12 мс не пройдёт ни один разумный порог лога, но именно на него уходит 97 % суммарного времени — 600 000 мс против 18 000 мс за час.
Топ медленных запросов — pg_stat_statements
По умолчанию PostgreSQL не запоминает, какие запросы выполнялись. Это меняет расширение pg_stat_statements: оно накапливает статистику по всем запросам — сколько раз вызывался, сколько времени занял суммарно и в среднем, сколько данных читал с диска.
Чтобы включить, нужно добавить расширение в конфигурацию и перезапустить сервер:
# postgresql.conf
shared_preload_libraries = 'pg_stat_statements'
pg_stat_statements.max = 10000
pg_stat_statements.track = all
CREATE EXTENSION pg_stat_statements;
После этого можно смотреть топ запросов по суммарному времени:
живой пример
SELECT
substring(query, 1, 80) AS query,
calls,
round(total_exec_time::numeric, 0) AS total_ms,
round(mean_exec_time::numeric, 1) AS mean_ms,
round((100 * total_exec_time / sum(total_exec_time) OVER ())::numeric, 1) AS pct
FROM pg_stat_statements
ORDER BY total_exec_time DESC LIMIT 20;
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Запросы нормализованы: конкретные значения заменяются на $1, $2 — поэтому один и тот же запрос с разными параметрами группируется в одну строку.
Три вещи про включение, на которых спотыкаются в первый раз.
Перезапуск нужен только для первой строки. shared_preload_libraries читается при старте, поэтому добавление библиотеки требует перезапуска сервера — не перечитывания конфигурации, а именно остановки и запуска. А вот CREATE EXTENSION перезапуска не требует вовсе: это обычная команда, она создаёт представления в текущей базе. Порядок такой: правим конфигурацию, перезапускаем, потом создаём расширение.
Расширение создают в каждой базе, где собираются смотреть. Статистику собирает один общий сборщик на весь сервер, но представления для доступа к ней живут в конкретной базе. Подключились к другой базе на том же сервере — pg_stat_statements там просто нет, хотя статистика по её запросам собирается. Это самая частая причина недоумения «я же включил». Проверить, где создано, можно так:
живой пример
SELECT current_database(), extname FROM pg_extension WHERE extname = 'pg_stat_statements';
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
И оговорка про права: без прав наблюдателя вы увидите только свои запросы, а текст чужих будет заменён на отметку о недостатке прав. Для дежурного это означает роль pg_monitor или pg_read_all_stats.
Редкие запросы молча пропадают. pg_stat_statements.max = 10000 — это не «столько строк показывать», а сколько разных запросов помнить. Когда уникальных больше, самые редкие вытесняются, и в статистике их не остаётся вовсе. На базе с динамически собираемыми запросами (условия склеиваются в коде, а не передаются параметрами) счёт уникальных идёт на сотни тысяч, и вытеснение происходит постоянно.
Чем это опасно: вы ищете виновника роста нагрузки, а его в таблице нет, потому что он вытеснен. Признак — стоит проверить перед разбором:
живой пример
SELECT dealloc, stats_reset FROM pg_stat_statements_info;
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
dealloc — сколько раз происходило вытеснение с момента сброса. Ноль — статистике можно верить целиком. Тысячи — поднимайте max (это память, порядка килобайта на запись, и перезапуск) и заодно ищите, откуда берётся столько уникальных текстов: обычно это склейка значений вместо параметров, которую стоит починить и по другой причине — она лишает базу возможности переиспользовать планы.
Цена самого расширения. Она есть, хотя и небольшая: на каждый запрос считается хеш нормализованного текста и обновляется запись в общей памяти под блокировкой. На обычной нагрузке это проценты, и включать его стоит всегда. Заметным это становится в двух случаях: очень короткие запросы с огромным темпом (десятки тысяч в секунду — тогда обновление общей записи начинает конкурировать) и track = all, который учитывает ещё и вложенные запросы внутри функций и процедур, умножая число записей. Для базы, где вся логика в функциях, track = top (умолчание) экономит и память, и время.
Если нужно измерить изменение за конкретный период — сбросьте статистику в начале:
живой пример
SELECT pg_stat_statements_reset();
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Автоматический план медленного запроса — auto_explain
pg_stat_statements показывает какой запрос медленный. Но чтобы понять почему — нужен план выполнения (EXPLAIN ANALYZE). Расширение auto_explain делает это автоматически: как только запрос превышает порог по времени, план попадает в лог PostgreSQL.
shared_preload_libraries = 'pg_stat_statements,auto_explain'
auto_explain.log_min_duration = '500ms'
auto_explain.log_analyze = on
auto_explain.log_buffers = on
auto_explain.log_format = json
Опция log_analyze = on добавляет реальные числа выполнения, а не только оценки планировщика. Стоит это недёшево: PostgreSQL начинает засекать время на каждом узле плана, и больнее всего это бьёт по коротким частым запросам — у них само измерение сопоставимо с работой. На высоконагруженных системах её включают в окне диагностики, а если нужно постоянно — вместе с auto_explain.sample_rate, который оставляет инструментирование только у части запросов:
auto_explain.sample_rate = 0.05 # планы снимаются с 5% запросов
Лог медленных запросов — log_min_duration_statement
Самый простой способ записать медленные запросы в лог — параметр log_min_duration_statement. Каждый запрос, который занял больше порога, попадёт в файл лога с текстом и временем выполнения.
log_min_duration_statement = '500ms'
log_line_prefix = '%m [%p] %q%u@%d '
В продакшене разумный порог — 500 мс до 1 секунды. Значение 100 мс создаст поток логов, который сложно анализировать.
Отличие от auto_explain: этот параметр записывает только текст запроса и время. auto_explain пишет полный план — это нужно при анализе конкретной проблемы.
Что происходит прямо сейчас — pg_stat_activity
pg_stat_activity показывает все активные сессии: что выполняется, сколько времени идёт транзакция, есть ли ожидание блокировки.
живой пример
SELECT
pid,
now() - xact_start AS xact_age,
now() - query_start AS query_age,
state,
wait_event_type, wait_event,
substring(query, 1, 80) AS query
FROM pg_stat_activity
WHERE state IS DISTINCT FROM 'idle'
ORDER BY xact_age DESC LIMIT 20;
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
IS DISTINCT FROM, а не !=: у фоновых процессов state пустой, и обычное сравнение выбросило бы их из выборки молча. А вот idle in transaction это условие оставляет — и правильно, именно они интереснее всего.
На что смотреть:
xact_age > 5 минут— транзакция висит слишком долго и удерживает ресурсы.wait_event_type = 'Lock'стабильно у нескольких процессов — очередь блокировок.state = 'idle in transaction'иxact_age > 1 минуты— соединение открыло транзакцию и забыло закрыть. Это не просто неряшливость: такая транзакция мешает autovacuum и накапливает bloat.
Кто кого блокирует — pg_locks
Если видите wait_event_type = 'Lock', можно найти конкретного виновника:
живой пример
SELECT
blocked.pid AS blocked_pid,
blocking.pid AS blocking_pid,
blocked.query AS blocked_query,
blocking.query AS blocking_query,
now() - blocked.query_start AS waiting_for
FROM pg_stat_activity blocked
CROSS JOIN LATERAL unnest(pg_blocking_pids(blocked.pid)) AS b(pid)
JOIN pg_stat_activity blocking ON blocking.pid = b.pid;
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Всю работу здесь делает функция pg_blocking_pids(pid): она возвращает номера процессов, которых ждёт указанный. Сводить pg_locks с самим собой вручную не надо и не стоит — блокировка опознаётся не одним типом, а всем набором полей (база, отношение, страница, версия строки, номер транзакции), и соединение по locktype свяжет между собой посторонние строки и назовёт виновником случайную сессию.
Здоровье таблиц и индексов — pg_stat_user_tables
PostgreSQL обновляет строки не на месте, а оставляя старые версии (это MVCC). Со временем накапливаются «мёртвые» строки — bloat. pg_stat_user_tables показывает, насколько таблица захламлена и когда последний раз отработал autovacuum:
живой пример
SELECT relname, n_live_tup, n_dead_tup,
last_autovacuum, last_autoanalyze,
n_tup_upd, n_tup_hot_upd,
seq_scan, idx_scan
FROM pg_stat_user_tables;
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Что важно:
n_dead_tup / n_live_tup— доля мёртвых строк. Если растёт — autovacuum не справляется.last_autovacuum— давно было? На активных таблицах должен запускаться каждые несколько минут или часов.n_tup_hot_upd / n_tup_upd— чем выше доля HOT-обновлений, тем эффективнее работает таблица (обновление без изменения индексов).
Одна незакрытая транзакция запрещает автовакууму убирать мёртвые строки, и цепочка доходит до диска: смотрите, на каком шаге обрывается уборка.
Для индексов отдельная таблица. Индексы, которые не используются — кандидаты на удаление:
живой пример
SELECT indexrelname, idx_scan
FROM pg_stat_user_indexes
WHERE idx_scan = 0; -- не использовался с момента последнего сброса статистики
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Этот запрос даёт список кандидатов на разговор, а не на удаление, и разница здесь дорогая. Четыре проверки перед тем, как что-то удалять.
Индексы под ограничениями удалять нельзя. Первичный ключ и уникальное ограничение реализованы индексом, и idx_scan у них законно бывает нулевым: их никто не читает, они нужны для проверки при вставке. Удаление такого индекса означает удаление ограничения, то есть потерю целостности. Отсеиваются они условием:
живой пример
SELECT s.relname, s.indexrelname, s.idx_scan,
pg_size_pretty(pg_relation_size(s.indexrelid)) AS size
FROM pg_stat_user_indexes s
JOIN pg_index i ON i.indexrelid = s.indexrelid
WHERE s.idx_scan = 0
AND NOT i.indisunique
AND NOT i.indisprimary
ORDER BY pg_relation_size(s.indexrelid) DESC;
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Статистика у каждой реплики своя. Счётчики считаются локально: индекс, не используемый на основном сервере, может активно читаться отчётами на реплике — и на основном вы увидите ноль. Проверять надо на всех серверах, где ходят запросы, иначе удаление индекса обрушит отчёты, о которых вы не знали.
После сброса счётчик врёт. idx_scan = 0 означает «не использовался с момента последнего сброса статистики», а не «никогда». Если счётчики сбросили вчера, ноль ничего не значит. Дату сброса смотрят в pg_stat_database.stats_reset; разумное окно для вывода — не меньше нескольких недель, чтобы в него попали месячные отчёты и редкие сценарии.
Редкое не значит ненужное. Индекс, используемый раз в месяц закрытием периода, покажет единицы вызовов — и по порогу «мало» попадёт в список на удаление. Без него отчёт будет идти не минуту, а час, и выяснится это в конце месяца.
Как это делают аккуратно: собирают список, смотрят его размер (удалять стоит то, что действительно занимает место), выясняют у авторов, откуда индекс появился, и — главный приём — сначала скрывают, а не удаляют. Пометить индекс невидимым для планировщика можно через UPDATE pg_index SET indisvalid = false, но это правка системного каталога, и лучше так не делать; безопасный способ — удалить в транзакции и посмотреть план нужных запросов, откатив её, или удалить и держать наготове команду создания (CREATE INDEX CONCURRENTLY, чтобы не блокировать таблицу). И удаляют по одному, с интервалом, а не пачкой из двадцати.
Соединения — connection pool
Каждое соединение к PostgreSQL потребляет память и ресурс. Если приложение открывает больше соединений, чем настроено через max_connections, новые будут отклонены. Смотреть состояние соединений:
живой пример
SELECT state, count(*) FROM pg_stat_activity GROUP BY state;
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Состояния: active (выполняет запрос), idle (ждёт следующего), idle in transaction (в транзакции, но молчит), idle in transaction (aborted) (транзакция прервана, но не закрыта).
Сигналы тревоги:
activeблизко кmax_connections— пул исчерпан, следующие запросы будут ждать или падать.idle in transactionстабильно больше пяти — где-то в приложении транзакции не закрываются.
Кто нагружает диск — pg_stat_io
«Чтение с диска выросло» из порядка диагностики ниже — полезный сигнал, который до PostgreSQL 16 не отвечал на следующий вопрос: а кто читает? Клиентские запросы, автовакуум или фоновая запись? Раньше это выясняли косвенно, теперь есть представление pg_stat_io.
Оно разбивает ввод-вывод по трём осям: кто (обычный обратный вызов, автовакуум, контрольная точка, фоновая запись), на чём (обычные данные, временные файлы) и что делал (чтение, запись, расширение файла, сброс на диск). Самый полезный вид — сравнить объёмы по инициатору:
живой пример
SELECT backend_type, object, context,
reads, writes, extends, evictions,
round((read_time / 1000)::numeric, 1) AS read_s,
round((write_time / 1000)::numeric, 1) AS write_s
FROM pg_stat_io
WHERE reads > 0 OR writes > 0
ORDER BY reads + writes DESC;
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Что из этого читают на практике:
backend_type = 'autovacuum worker'с большими объёмами — автовакуум перемалывает диск, и это либо запущенная уборка после накопившегося мусора, либо слишком агрессивные настройки. Лечится не отключением автовакуума (это верный путь к аварии), а его настройкой и разбором того, откуда столько мёртвых строк.context = 'vacuum'противcontext = 'normal'— сколько ввода-вывода уходит на обслуживание, а сколько на запросы. Если обслуживание съедает большую часть, у базы проблема не с запросами.object = 'temp relation'— временные файлы, то есть сортировки и соединения, не уместившиеся в память. Прямой указатель на то, чтоwork_memмал для текущих запросов; тот же сигнал даётlog_temp_files.evictions— сколько раз страницу приходилось вытеснять из буферного кэша, чтобы освободить место. Растущие вытеснения при стабильной нагрузке означают, что рабочий набор перестал помещаться в память, и это честнее, чем доля попаданий в кэш.writesуclient backend— обычные запросы сами пишут на диск, вместо того чтобы отдать это фоновой записи и контрольной точке: признак, что буферный кэш мал или контрольные точки настроены слишком редко.
Время (read_time, write_time) заполняется только при включённом track_io_timing — без него видны объёмы, но не задержки, а половина смысла именно в задержках.
Счётчики накопительные с момента сброса, поэтому смотрят разницу между двумя снимками, а не абсолютные числа: сняли сейчас, сняли через пять минут, вычли.
Сколько база работала, а сколько ждала
pg_stat_activity отвечает «что происходит прямо сейчас» — и этого мало, когда нужно понять, чем база занималась последний час. Начиная с PostgreSQL 14 в pg_stat_database есть накопительные длительности, и они отвечают на вопрос, который иначе решается только сбором выборок:
живой пример
SELECT datname,
round((active_time / 1000)::numeric, 1) AS active_s,
round((idle_in_transaction_time / 1000)::numeric, 1) AS idle_in_tx_s,
round((session_time / 1000)::numeric, 1) AS session_s,
sessions_abandoned, sessions_fatal, sessions_killed
FROM pg_stat_database
WHERE datname = current_database();
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Как это читать. active_time — сколько суммарно база выполняла запросы; session_time — сколько соединения вообще были открыты. Отношение первого ко второму говорит, работала база или ждала приложение: при активных пяти процентах от времени сессий узкое место точно не в базе. idle_in_transaction_time — самое ценное число: столько времени транзакции были открыты и ничего не делали. Это прямая мера того, что приложение держит транзакции вокруг сетевых вызовов или пользовательских пауз, и именно это блокирует уборку мусора и держит блокировки. Заметная величина здесь — повод искать в приложении, а не в базе.
sessions_abandoned — сколько соединений оборвалось без закрытия (клиент упал, сеть моргнула, контейнер убит), sessions_killed — сколько прибили принудительно. Растущие числа здесь объясняют загадочные «соединения кончились» лучше любых догадок.
И ещё одно представление, о котором стоит знать заранее, потому что нужно оно в неприятный момент: прогресс длительных операций. Когда уборка идёт третий час и непонятно, движется ли она:
живой пример
SELECT p.pid, c.relname, p.phase,
p.heap_blks_scanned, p.heap_blks_total,
round(100.0 * p.heap_blks_scanned / nullif(p.heap_blks_total, 0), 1) AS pct
FROM pg_stat_progress_vacuum p
JOIN pg_class c ON c.oid = p.relid;
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Видно фазу и долю пройденного — то есть ответ на «ждать или отменять». Такие же представления есть у создания индекса (pg_stat_progress_create_index), у полной перезаписи таблицы (pg_stat_progress_cluster), у базового резервного копирования и у COPY. Все они показывают только идущие операции, поэтому пользы от них ноль, если узнать о них после инцидента.
Ключевые метрики на дашборде
Минимальный набор метрик, которые стоит вывести в систему мониторинга:
| Метрика | Источник | Когда смотреть |
|---|---|---|
| TPS (транзакций/сек) | pg_stat_database.xact_commit + xact_rollback | падение от обычного уровня |
| Используемые соединения | pg_stat_activity.state | > 80% от max_connections |
| Долгие транзакции | pg_stat_activity.xact_age | > 5 минут |
| Отставание реплики | pg_stat_replication.replay_lag | > 30 секунд |
| Cache hit ratio | pg_stat_database.blks_hit / (blks_hit + blks_read) | резкое падение против обычного уровня |
| Внеплановые контрольные точки | pg_stat_checkpointer.num_requested (до PostgreSQL 17 — pg_stat_bgwriter.checkpoints_req) | стабильный рост |
| Размер базы | pg_database_size() | неожиданный скачок |
| Последний autovacuum | pg_stat_user_tables.last_autovacuum | > 1 дня на активных таблицах |
| Bloat | pgstattuple | > 30% на активных таблицах |
Про cache hit ratio стоит сказать отдельно, потому что её чаще всего понимают неправильно. Она считает попадания только в буферный кэш самого PostgreSQL. Всё, что база прочитала мимо него, попадает в blks_read и выглядит как «сходили на диск» — хотя страница почти наверняка лежала в страничном кэше операционной системы и диска там не было. Поэтому 92% — не приговор, а 99% ничего не гарантируют: в них легко уложиться, читая одну и ту же горячую таблицу, пока отчётный запрос перемалывает всё остальное. Смотреть на неё имеет смысл не по порогу, а по изменению против обычного для этой базы уровня — и только вместе с track_io_timing, который показывает реальное время ввода-вывода.
Начиная с PostgreSQL 17 счётчики контрольных точек переехали из pg_stat_bgwriter в отдельное представление pg_stat_checkpointer, а checkpoints_timed/checkpoints_req там называются num_timed/num_requested. Дашборд, собранный по старым именам, на 17-й версии просто перестанет рисовать эти панели.
Экспорт метрик в Prometheus делается через postgres_exporter — он подключается к базе и публикует всё перечисленное в формате, который понимают Grafana и Alertmanager.
Что включить в продакшене
Базовый минимум, который должен быть везде:
pg_stat_statements— без него непонятно, какие запросы тяжёлые.log_min_duration_statement = '1s'— медленные запросы в лог.track_activity_query_size = 4096— стандартное значение 1024 обрезает длинные запросы вpg_stat_activity.track_io_timing = on— добавляет время IO к статистике запросов.
Дополнительно, если нужна глубже диагностика:
auto_explainсlog_min_duration = '500ms'— планы медленных запросов в лог.log_lock_waits = on— логировать ожидания блокировок.log_temp_files = 0— логировать запросы, которые сбрасывают временные файлы на диск (признак нехватки памяти для сортировки).
Когда менять конфигурацию нельзя
Половина статьи начинается словами «добавьте в postgresql.conf», и это предполагает права и возможность перезапуска. Часто ни того, ни другого нет: база управляемая у облачного провайдера, база чужой команды, доступ только на чтение, окно перезапуска через месяц. Разбираться всё равно надо, и вот чем.
Что доступно без всяких прав. Представления pg_stat_activity, pg_stat_user_tables, pg_stat_user_indexes, pg_stat_database, pg_locks, pg_stat_io работают всегда — это часть сервера, а не расширение. С ролью наблюдателя (pg_monitor) видны чужие сессии и полные тексты запросов. Этого достаточно, чтобы ответить на большинство вопросов: кто сейчас держит блокировку, какие таблицы захламлены, сколько соединений, растёт ли чтение с диска.
Если pg_stat_statements уже включён (у облачных провайдеров он обычно включён по умолчанию — проверьте, прежде чем страдать), нужен только CREATE EXTENSION в вашей базе, а это право обычно есть. Перезапуск не нужен.
Если его нет и включить нельзя — остаётся собирать статистику выборками самому: раз в секунду читать pg_stat_activity в свою таблицу и потом считать по ней, какие запросы занимали время. Это тот же принцип, по которому работают отчёты нагрузки, и он не требует ничего, кроме права на чтение и одной своей таблицы; подробный разбор — в статье про отчёт нагрузки своими руками.
Что можно менять на сессию, а не на сервер. Многое из перечисленного не требует общей конфигурации: SET log_min_duration_statement = 0 и SET auto_explain.log_min_duration = '100ms' (если библиотека загружена) действуют на текущее соединение. То же на роль или на базу через ALTER ROLE ... SET и ALTER DATABASE ... SET — это не требует перезапуска и не задевает остальных. Приём, которым пользуются постоянно: включить подробное журналирование только для своей отладочной сессии и воспроизвести в ней проблемный запрос.
И про log_min_duration_statement = 0. В списке частых ошибок он справедливо назван вредным — как постоянная настройка. Но есть законный случай: короткое окно диагностики, когда нужен полный список запросов с временем, а pg_stat_statements его не даёт (например, надо увидеть конкретные значения параметров или порядок запросов внутри одной транзакции). Тогда его включают на пять-десять минут, лучше на одну базу или роль, следя за местом на диске, и обязательно возвращают. Это инструмент, а не настройка: разница в том, что у него есть время начала и время конца, записанные заранее.
Как разбирать «всё медленно»
Порядок диагностики, когда пришло сообщение «всё медленно»:
pg_stat_activity— есть ли прямо сейчас долгие запросы или блокировки? Есть — смотрите, кто кого держит, и снимайте виновника.pg_stat_statements— топ-10 по суммарному времени за последний час. Что изменилось? Новый запрос в топе — ищите релиз; старый вырос — ищите рост данных или потерянный индекс.pg_stat_database.blks_readrate — чтение с диска выросло? Значит, рабочий набор перестал помещаться в память: база выросла или новый тяжёлый запрос вытеснил кэш.pg_stat_user_tables.n_dead_tup— bloat накопился? Проверьте, не отстал ли autovacuum, и запустите его вручную на пострадавшей таблице.pg_stat_replication— реплика не отстаёт? Отстаёт — запросы с неё отдают устаревшее, а на основном сервере копятся WAL.- Операционная система —
iostat,top,vmstat: диск, процессор или память на пределе? Пределы железа не лечатся настройкой запросов. - Сеть — количество соединений от приложения растёт? Растёт — течёт пул или приложение не отдаёт соединения, и лимит
max_connectionsблизко.
Часто причина находится на первых двух шагах.
Частые ошибки
Не включать pg_stat_statements — без него приходится догадываться о причинах нагрузки. Это первое, что нужно после установки PostgreSQL.
Ставить log_min_duration_statement = 0 — логируются все запросы, лог разрастается, полезное тонет в шуме.
Смотреть только TPS и connections — этого мало. Долгие транзакции и bloat убивают производительность постепенно, без видимого пика TPS.
Держать auto_explain.log_analyze = on постоянно на нагруженных системах — создаёт накладные расходы. Включать только во время диагностики.
Сбрасывать pg_stat_statements без предупреждения — теряется история, на которую смотрят коллеги.
Коротко
pg_stat_statements— главный инструмент: топ запросов по времени, числу вызовов, IO. Требуетshared_preload_librariesи перезапуска сервера.auto_explainавтоматически пишет план медленного запроса в лог; на высоком трафике включать только для диагностики.log_min_duration_statement = '500ms–1s'в продакшене — быстрый способ поймать медленные запросы без дополнительных расширений.pg_stat_activityпоказывает текущие сессии: долгие транзакции, блокировки, «забытые» соединения вidle in transaction.pg_stat_user_tables— состояние таблиц: bloat, последний autovacuum, соотношение HOT-обновлений.- Cache hit ratio считает попадания только в буферный кэш PostgreSQL: и низкое значение не означает «мало RAM», и высокое ничего не гарантирует — смотреть надо на отклонение от обычного уровня.
- Диагностика «медленно»: activity → statements → IO → bloat → replication → OS.
shared_preload_librariesтребует перезапуска,CREATE EXTENSION— нет, и создать расширение надо в каждой базе, где смотрите; вытеснение редких запросов видно вpg_stat_statements_info.dealloc.- Индексы с нулевым числом сканирований — не список на удаление: отсеять уникальные и первичные, проверить все реплики, учесть дату сброса статистики и месячные отчёты, удалять по одному.
pg_stat_ioотвечает, кто нагружает диск — запросы, автовакуум или контрольная точка, — и показывает временные файлы и вытеснения; время видно только приtrack_io_timing.pg_stat_databaseс 14-й версии хранит длительности: отношение активного времени к времени сессий говорит, работала база или ждала приложение, аidle_in_transaction_time— прямая мера транзакций, открытых зря.- Без прав на конфигурацию остаются все представления,
CREATE EXTENSIONв своей базе, настройки на сессию или роль и сбор выборок в свою таблицу.
Что почитать дальше
- EXPLAIN ANALYZE — как читать план выполнения запроса.
- VACUUM и bloat — почему мёртвые строки накапливаются и как с этим работать.
- Блокировки — детальный разбор
pg_locksи типов блокировок. - Индексы — когда индекс помогает, а когда мешает.