← назад к разделу

Когда приложение начинает тормозить, первый вопрос — «что происходит в базе?». Без инструментов ответ строится на догадках. PostgreSQL накапливает статистику о каждом запросе, каждой таблице, каждом соединении — нужно только знать, где смотреть.

Разные инструменты ловят разное: лог отбирает запросы по времени одного вызова, а pg_stat_statements складывает время всех вызовов — и на одной и той же нагрузке они показывают разных виновников.

Один час работы базы · два запросаA: WHERE user_id = $112 мс × 50 000 вызововB: ночной отчёт900 мс × 20 вызовов log_min_duration_statement = '500ms' — порог по времени одного вызова0 мс1000 мспорог 500 мс A · 12 мсB · 900 мс в лог за час попадёт только B — 20 строк; A не попадёт ни разу pg_stat_statements · суммарное время вызовов за час, мсB20 вызовов18 000 мс · 3%0300 000600 000 600 000 мс · 97%A50 000 вызовов Виновник нагрузки — A: 12 мс, которых в логе нет ни строки

Лог ловит длинный вызов, 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-обновлений, тем эффективнее работает таблица (обновление без изменения индексов).
Долгая транзакция открыта и молчит autovacuum не убирает Мёртвые строки n_dead_tup растёт мусор копится в страницах Таблица пухнет страниц больше, строк нет кэш перестаёт вмещать Чтение с диска blks_read растёт

Одна незакрытая транзакция запрещает автовакууму убирать мёртвые строки, и цепочка доходит до диска: смотрите, на каком шаге обрывается уборка.

Для индексов отдельная таблица. Индексы, которые не используются — кандидаты на удаление:

живой пример

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 ratiopg_stat_database.blks_hit / (blks_hit + blks_read)резкое падение против обычного уровня
Внеплановые контрольные точкиpg_stat_checkpointer.num_requested (до PostgreSQL 17 — pg_stat_bgwriter.checkpoints_req)стабильный рост
Размер базыpg_database_size()неожиданный скачок
Последний autovacuumpg_stat_user_tables.last_autovacuum> 1 дня на активных таблицах
Bloatpgstattuple> 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 его не даёт (например, надо увидеть конкретные значения параметров или порядок запросов внутри одной транзакции). Тогда его включают на пять-десять минут, лучше на одну базу или роль, следя за местом на диске, и обязательно возвращают. Это инструмент, а не настройка: разница в том, что у него есть время начала и время конца, записанные заранее.

Как разбирать «всё медленно»

Порядок диагностики, когда пришло сообщение «всё медленно»:

  1. pg_stat_activity — есть ли прямо сейчас долгие запросы или блокировки? Есть — смотрите, кто кого держит, и снимайте виновника.
  2. pg_stat_statements — топ-10 по суммарному времени за последний час. Что изменилось? Новый запрос в топе — ищите релиз; старый вырос — ищите рост данных или потерянный индекс.
  3. pg_stat_database.blks_read rate — чтение с диска выросло? Значит, рабочий набор перестал помещаться в память: база выросла или новый тяжёлый запрос вытеснил кэш.
  4. pg_stat_user_tables.n_dead_tup — bloat накопился? Проверьте, не отстал ли autovacuum, и запустите его вручную на пострадавшей таблице.
  5. pg_stat_replication — реплика не отстаёт? Отстаёт — запросы с неё отдают устаревшее, а на основном сервере копятся WAL.
  6. Операционная система — iostat, top, vmstat: диск, процессор или память на пределе? Пределы железа не лечатся настройкой запросов.
  7. Сеть — количество соединений от приложения растёт? Растёт — течёт пул или приложение не отдаёт соединения, и лимит 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 и типов блокировок.
  • Индексы — когда индекс помогает, а когда мешает.