lenec ru

← все посты

pg_stat_statements: находим и оптимизируем медленные запросы в PostgreSQL

11K

Логи медленных запросов — это хорошо, но они показывают только то, что уже взорвалось. А что если нужно найти запросы, которые по отдельности быстрые, но вызываются 100 000 раз в минуту и суммарно съедают 80% CPU? Для этого есть pg_stat_statements — расширение, которое собирает агрегированную статистику по всем запросам в кластере.

Установка и настройка

Расширение входит в стандартную поставку PostgreSQL, но требует подключения через shared_preload_libraries:

-- postgresql.conf
shared_preload_libraries = 'pg_stat_statements'
pg_stat_statements.track = all    -- top, all или none
pg_stat_statements.max = 10000   -- макс. количество отслеживаемых запросов

-- После изменения shared_preload_libraries нужен рестарт:
-- sudo systemctl restart postgresql

-- Создаём расширение в нужной базе:
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;

Параметр track = all отслеживает запросы внутри функций и процедур. Если нужны только top-level запросы — ставьте top. Параметр max определяет, сколько уникальных запросов хранить — при переполнении самые редкие вытесняются.

Ключевые метрики

Представление pg_stat_statements содержит десятки колонок. Самые важные:

  • total_exec_time — суммарное время выполнения (мс). Главная метрика для поиска «тяжёлых» запросов.
  • calls — сколько раз запрос выполнялся.
  • mean_exec_time — среднее время одного вызова (total_exec_time / calls).
  • rows — суммарное количество возвращённых/затронутых строк.
  • shared_blks_hit / shared_blks_read — попадания в кеш vs чтение с диска.
  • queryid — хеш нормализованного запроса.

TOP-10 запросов по времени

Готовый запрос, который покажет главных потребителей ресурсов:

SELECT
  queryid,
  substring(query, 1, 80) AS short_query,
  calls,
  round(total_exec_time::numeric, 1) AS total_ms,
  round(mean_exec_time::numeric, 2) AS avg_ms,
  rows,
  round(
    100.0 * shared_blks_hit / nullif(shared_blks_hit + shared_blks_read, 0), 1
  ) AS cache_hit_pct
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;

Этот запрос сразу показывает: какой запрос суммарно сжирает больше всего времени, как часто вызывается, и какой у него процент попадания в кеш. Низкий cache_hit_pct — сигнал, что данные не помещаются в shared_buffers.

Вариации для других задач:

-- TOP по количеству вызовов (найти N+1)
SELECT query, calls, mean_exec_time
FROM pg_stat_statements
ORDER BY calls DESC LIMIT 10;

-- TOP по чтению с диска (I/O-bound запросы)
SELECT query, shared_blks_read, calls
FROM pg_stat_statements
ORDER BY shared_blks_read DESC LIMIT 10;

Нормализация запросов: queryid и параметры

PostgreSQL автоматически нормализует запросы — заменяет литералы на $1, $2 и т.д. Это позволяет группировать одинаковые по структуре запросы:

-- Эти два запроса будут одной записью в pg_stat_statements:
SELECT * FROM users WHERE id = 42;
SELECT * FROM users WHERE id = 999;

-- В pg_stat_statements они отобразятся как:
-- SELECT * FROM users WHERE id = $1

Поле queryid — хеш нормализованного запроса. Используйте его для трекинга во времени.

Автоматизация: reset и мониторинг

Статистика накапливается с момента последнего сброса. Для трендов нужно периодически снимать снапшоты и сбрасывать:

-- Сбросить всю статистику
SELECT pg_stat_statements_reset();

Для мониторинга через Grafana: cron каждые 5 минут снимает снапшот в отдельную таблицу, после чего вызывает reset. Grafana строит графики по дельтам.

-- Снапшот (вызывать по cron каждые 5 минут)
INSERT INTO pgss_snapshots (queryid, query, calls, total_exec_time, rows)
SELECT queryid, query, calls, total_exec_time, rows
FROM pg_stat_statements
WHERE dbid = (SELECT oid FROM pg_database WHERE datname = current_database());

Реальный кейс: нашли N+1 через pg_stat_statements

Ситуация: API отвечает медленно, но отдельные запросы в логах быстрые (1-3 мс). Смотрим TOP по calls:

SELECT substring(query, 1, 60), calls, mean_exec_time, total_exec_time
FROM pg_stat_statements ORDER BY calls DESC LIMIT 5;

--                    query                    | calls  | avg_ms | total_ms
-- SELECT * FROM products WHERE id = $1       | 847293 |   1.2  | 1016752
-- SELECT * FROM categories WHERE id = $1     | 847293 |   0.8  |  677834
-- SELECT * FROM users WHERE id = $1          |  12045 |   0.9  |   10840

847 тысяч вызовов одного и того же запроса за период! Это классический N+1: ORM загружает список заказов, потом для каждого отдельно тянет product и category. Суммарно эти два запроса съедают 1.7 миллиона мс — почти 30 минут чистого CPU-времени.

Решение: добавили JOIN в ORM-запрос (eager loading), количество вызовов упало с 847K до 12K, суммарное время — с 1.7M мс до 45K мс.

Вывод

pg_stat_statements — must-have расширение для любого PostgreSQL в проде. Оно находит проблемы, которые не видны в логах: частые лёгкие запросы, деградацию кеша, N+1 паттерны. Настройте снапшоты, подключите Grafana — и вы будете видеть проблемы до того, как пользователи начнут жаловаться.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.