Что такое `pg_stat_statements` в PostgreSQL?
pg_stat_statements — расширение PostgreSQL, которое собирает агрегированную статистику выполнения SQL-запросов: количество вызовов, суммарное и среднее время, количество строк, блочный I/O. Это основной инструмент для поиска медленных и ресурсоёмких запросов.Что такое pg_stat_statements
pg_stat_statements — стандартное расширение PostgreSQL (входит в contrib), которое накапливает статистику выполнения всех SQL-запросов, обработанных сервером. Каждый уникальный запрос идентифицируется по нормализованной форме (литералы заменяются на $1, $2, ...), что позволяет группировать вызовы одного и того же запроса с разными параметрами.
Подключение
Расширение требует явной загрузки на уровне сервера:
-- postgresql.conf
shared_preload_libraries = 'pg_stat_statements'
pg_stat_statements.max = 10000 -- максимум уникальных запросов
pg_stat_statements.track = all -- all | top | none
pg_stat_statements.track_utility = on -- DDL, COPY и т.д.
После перезапуска сервера расширение создаётся в нужной базе:
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
Ключевые метрики представления
Представление pg_stat_statements (PostgreSQL 14+) содержит следующие важные колонки:
| Колонка | Описание |
|---|---|
query | Нормализованный текст запроса |
calls | Число вызовов |
total_exec_time | Суммарное время выполнения (мс) |
mean_exec_time | Среднее время (мс) |
rows | Суммарно возвращено строк |
shared_blks_hit | Попадания в shared buffer |
shared_blks_read | Чтения с диска |
temp_blks_written | Временные блоки (сортировки, хеши) |
wal_bytes | Объём сгенерированного WAL |
Практические запросы
Топ-10 запросов по суммарному времени
SELECT
round(total_exec_time::numeric, 2) AS total_ms,
calls,
round(mean_exec_time::numeric, 2) AS mean_ms,
round(
(100 * total_exec_time / sum(total_exec_time) OVER ())::numeric, 2
) AS pct,
query
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;
Запросы с высоким cache miss ratio
SELECT
query,
shared_blks_hit,
shared_blks_read,
round(
100.0 * shared_blks_read
/ NULLIF(shared_blks_hit + shared_blks_read, 0), 2
) AS miss_pct
FROM pg_stat_statements
WHERE shared_blks_hit + shared_blks_read > 1000
ORDER BY miss_pct DESC
LIMIT 10;
Сброс накопленной статистики
-- Полный сброс (суперпользователь)
SELECT pg_stat_statements_reset();
-- Сброс конкретного запроса (PostgreSQL 12+)
SELECT pg_stat_statements_reset(userid, dbid, queryid);
Важные нюансы для senior-уровня
pg_stat_statements.track = top— по умолчанию в некоторых дистрибутивах; не отслеживает запросы внутри функций.allвключает вложенные.queryid— нестабильный хеш: послеpg_upgradeили изменения схемы может измениться, нельзя использовать как долгосрочный ключ.- Производительность: overhead минимален (~1–3%), но при
maxменьше реального числа уникальных запросов старые записи вытесняются — следите заpg_stat_statements_info.dealloc. - PostgreSQL 13 добавил разделение
total_exec_timeнаtotal_plan_timeиtotal_exec_time; в ранних версиях планирование не выделялось отдельно.
Что хочет услышать интервьюер
Кандидат знает, что это расширение (не встроенная функция) и требует `shared_preload_libraries` + перезапуска
Понимает нормализацию запросов — почему `SELECT $1` объединяет все вызовы с разными литералами
Умеет интерпретировать ключевые метрики: `total_exec_time`, `mean_exec_time`, `shared_blks_read` для диагностики
Знает про `pg_stat_statements_reset()` и когда его применять (baseline до/после деплоя)
Осведомлён об ограничениях: `max`-лимит на уникальные запросы, нестабильность `queryid`, разница `track=top` vs `track=all`
Пример: Включение расширения и базовая настройка
-- 1. Добавить в postgresql.conf, затем перезапустить сервер
-- shared_preload_libraries = 'pg_stat_statements'
-- pg_stat_statements.max = 10000
-- pg_stat_statements.track = all
-- 2. Создать расширение в нужной БД
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
-- 3. Проверить, что расширение работает
SELECT count(*) FROM pg_stat_statements;
Пример: Топ ресурсоёмких запросов
-- Самые «дорогие» запросы по суммарному времени выполнения
SELECT
round(total_exec_time::numeric, 2) AS total_ms,
calls,
round(mean_exec_time::numeric, 2) AS mean_ms,
round(
100.0 * total_exec_time
/ sum(total_exec_time) OVER (), 2
) AS pct_of_total,
left(query, 120) AS query_preview
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;
Пример: Анализ cache miss и временных файлов
-- Запросы, которые часто промахиваются мимо shared buffer
-- или создают временные файлы (сортировки, хеш-джойны)
SELECT
calls,
round(
100.0 * shared_blks_read
/ NULLIF(shared_blks_hit + shared_blks_read, 0), 2
) AS disk_read_pct,
temp_blks_written, -- > 0 означает spill на диск
left(query, 100) AS query_preview
FROM pg_stat_statements
WHERE (shared_blks_hit + shared_blks_read) > 500
OR temp_blks_written > 0
ORDER BY disk_read_pct DESC NULLS LAST
LIMIT 10;
Типичные ошибки
Считают, что расширение активно по умолчанию — забывают про `shared_preload_libraries` и перезапуск
Ищут самый медленный средний запрос (`mean_exec_time`) и игнорируют `total_exec_time` — редкий, но длинный запрос может не быть узким местом
Не учитывают параметр `track`: при `top` статистика внутри PL/pgSQL-функций не собирается
Путают `shared_blks_hit` (из shared buffer) и `shared_blks_read` (с диска/OS cache) — неправильно оценивают cache hit ratio
Используют `queryid` как постоянный идентификатор для исторических сравнений — он может измениться после `ANALYZE` или апгрейда


