Прямой ответ
«База тормозит» — вопрос не «какой запрос медленный», а «какие запросы суммарно съедают время» — и отвечает на него pg_stat_statements (расширение из поставки: статистика по нормализованным запросам — WHERE id = 123 и WHERE id = 456 — одна запись WHERE id = $1 — вызовы/время/строки/блоки — вся нагрузка БД в одной таблице — планы-контекст); подключение: shared_preload_libraries = 'pg_stat_statements' (перезапуск — конфигурация) + CREATE EXTENSION + тюнинг (pg_stat_statements.max = 10000 — вместимость нормализованных запросов — дефолт 5000 мал для активных приложений, pg_stat_statements.track = all — включая вложенные вызовы функций, track_utility — DDL/служебные); главный запрос аудита (топ по суммарному времени): SELECT round(total_exec_time::numeric, 1) AS total_ms, calls, round(mean_exec_time::numeric, 2) AS mean_ms, rows, left(query, 100) FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 20 — два паттерна проблем видны сразу: «тяжёлый редкий» (mean 2000 мс × 50 вызовов — отчёт-запрос без индекса — индексы) и «лёгкий массовый» (mean 3 мс × 2 000 000 вызовов — 6 000 секунд суммарно — N+1 (N+1-статья — «каждый запрос по индексу — быстрые» — сумма убивает — второй паттерн находят только здесь); столбцы, которые читают (2026): total_exec_time (время выполнения — главный; в PG 13+ отдельно total_plan_time — планирование — «plan 40% времени» — prepared statements/партийный крокодил — планы), calls, mean_exec_time+stddev_exec_time (разброс — «mean 5 мс, stddev 200» — бимодальность — кэш/план-флипы — кэш-контекст), rows (прочитано против возвращено — «rows 500 000, вернули 10» — seq scan/фильтр поздний — индексы), shared_blks_hit/read (попадания/чтения блоков — hit ratio per query — «read 40k блоков» — мимо кэша — ёмкость-RAM), temp_blks_written (spill на диск — work_mem мал для сортировки/хэша — конфигурация — «temp written > 0» — маркер тяжёлых операций — vacuum-контекст), wal_records/wal_bytes (WAL-генерация — write-heavy-запросы — репликация-стоимость); нормализация-нюансы: $1/$2 — параметры схлопнуты (ищем по шаблону), IN-списки (PG 14+ — IN ($1, $2...) — отдельная запись от = $1), «один запрос — три нормализации» — группировка руками при аудите; сброс статистики: pg_stat_statements_reset() — «замеряем неделю с нуля» — ритуал перед/после оптимизации (бенчмарк-дисциплина — «до/после» честно).
Практика: от статистики к индексу
Связка «статистика → план → фикс» (рабочий цикл): (1) топ-20 по total_exec_time (запрос выше — кандидат №1: SELECT ... FROM orders o JOIN order_items i ON ... WHERE o.user_id = $1 — calls 840k, mean 4.2 мс, rows 6.1M — read-блоков 2.3M); (2) EXPLAIN (ANALYZE, BUFFERS) на живых параметрах (планы-чтение — seq scan на order_items 400k строк на каждый вызов (нет индекса на order_id — индексы — «запрос написан в 2021, данных было 10k»); (3) фикс: индекс CREATE INDEX CONCURRENTLY ON order_items(order_id) (без блокировок — блокировки-дисциплина — миграция) — mean 4.2→0.4 мс, total_exec_time кандидата −90% — суммарная нагрузка БД −35% (один индекс — треть нагрузки — типичный ROI — техдолг-приоритеты); (4) N+1-кандидат (паттерн «массовый лёгкий»): SELECT ... FROM customers WHERE id = $1 — 2.1M вызовов/неделю — ORM-ленивость (N+1 — фикс — selectinload — вызовы 2.1M → 40k — ORM-практика); (5) write-кандидат (wal_bytes-сортировка): частый UPDATE счётчика (констрейнты-контекст — hot-row — WAL-поток — репликация-стоимость — асинхронная агрегация — Redis-паттерны); смежные представления (полная картина): pg_stat_user_tables (seq_scan-счётчики по таблицам — «таблицу сканят 2M раз» — индекс-кандидат без текста запроса), pg_stat_bgwriter/pg_stat_wal (checkpoint/WAL-активность — PITR-контекст), pg_stat_replication (лаг реплик — реплики), pg_stat_activity (живые запросы/wait events — блокировки); экспорт в мониторинг: postgres_exporter для Prometheus (pg_stat_statements-коллектор — метрики по запросам — метрики — дашборд топ-запросов (Grafana — «нагрузка по запросам» — тренд — «новый запрос после релиза съедает 20%» — видно — recording rules — алерт на рост total_exec_time (алерты — VM-хранение).
Практический сценарий: «БД в 90% CPU» → 35% за день
Контекст: прод-Postgres (8 vCPU) — CPU 88–95% в вечерний пик, p95 API деградировал 200→600 мс; «добавим CPU» — отложили до диагноза (ёмкость-принцип — сначала понять, потом покупать); диагностика (2 часа): (1) pg_stat_statements-топ (неделя — reset не делали — ритуал): топ-3 дают 71% total_exec_time: №1 — отчёт-JOIN (mean 1.8 с × 12k — «отчёт по каждому пользователю в цикле» — N+1 отчётов! — N+1), №2 — поиск по товарам (mean 90 мс × 400k — seq scan + LIKE '%...%'), №3 — «лёгкий массовый» customers-by-id (2.1M — ORM); (2) планы по топ-3 (EXPLAIN — №1: вложенный цикл с seq scan (нет индекса отчётов + N+1-паттерн), №2: trgm-индекс отсутствует (fulltext), №3: индекс есть — чистый N+1; (3) фиксы дня: отчёты — один агрегирующий запрос + кэш (Redis — 12k вызовов → 300; поиск — GIN trgm-индекс (fulltext-практика — CONCURRENTLY — блокировки) — mean 90→12 мс; customers — selectinload в трёх местах (ORM — 2.1M → 40k); (4) проверка: reset статистики → сутки → новый топ (суммарное время −68%, CPU пик 92→35% (метрики — p95 API 600→180 мс — SLO восстановлен); (5) «CPU 16 vCPU» не купили (ёмкость — оптимизация дала ×2.5 запаса — деньги сэкономлены — «железо после индексов» — бенчмарк-культура); (6) закрепление: postgres_exporter в мониторинг (топ-запросы — дашборд — Grafana — еженедельный аудит топа — ритуал 15 минут (техдолг), алерт «новый запрос в топ-5» (релиз-регрессия видна — алерты — CI-проверка); вывод: 80% «БД тормозит» — три паттерна (тяжёлый без индекса, N+1, seq scan по росту данных) — все находятся одним запросом к pg_stat_statements — диагностика-дисциплина — «медленно» — не свойство БД, а свойство запросов.
FAQ
Включён ли pg_stat_statements по умолчанию и что стоит включение?
Нет (расширение требует shared_preload_libraries + CREATE EXTENSION — managed-облака обычно включили (RDS/Cloud SQL — параметр-группы — облачный-контекст — проверить), self-hosted — руками — конфигурация); стоимость: hash-таблица в shared memory (pg_stat_statements.max × ~1 КБ — 10k записей — ~10 МБ — копейки — ёмкость), overhead на запрос — единицы процентов (учёт статистики — атомики — на OLTP-нагрузке незаметно — бенчмарк — «выключили ради производительности» — ложная экономия), нормализация не идеальна (редкие edge-формы запросов сливаются/разъезжаются — аудит читает с пониманием — раздел выше); «включать только при проблемах» — нет: статистика — история (когда начал расти тяжелый запрос — без истории — «вчера всё было» — не докажешь — постоянная работа — метрики-культура); альтернатива-дополнение: auto_explain (лог планов медленных запросов — auto_explain.log_min_duration=500ms — тяжёлые планы в логе — логи — «план на момент инцидента» — pg_stat_statements хранит агрегат, auto_explain — конкретные планы — пара).
Как отличить «тяжёлый запрос» от «массового лёгкого» и что важнее? Оба паттерна — в топ-20 по total_exec_time (раздел выше): тяжёлый редкий (высокий mean, низкий calls — отчёт/аналитика — лечение: индекс/план/кэш/вынос на реплику (реплики)), массовый лёгкий (низкий mean, огромный calls — N+1/отсутствие кэша — лечение: батчинг/eager loading/кэш); важнее — суммарный вклад (total_exec_time — «что БД реально делала» — ёмкость — «самый медленный запрос» ≠ «самый дорогой» — частая ошибка «оптимизировали 3-секундный отчёт раз в день, а 2M по 3 мс остались»); приоритизация-формула: total_exec_time × простота фикса (индекс за час −35% нагрузки — сценарий выше — перед рефакторингом отчёта на неделю −8% — техдолг-ROI); латентность-перспектива (mean важен для UX конкретного запроса — p95 API (SLO) — «отчёт 3 секунды» — боль пользователя отчёта — обе линзы — аудит по total, продукт по mean — метрики-дизайн).
**Что делать с запросами, которые медленные, но «редкие и терпимые»? Критерии оставить: разовые/админские (ручной отчёт раз в месяц — 30 секунд — никто не ждёт в интерфейсе — ёмкость-контекст), ресурсно-безопасные (не блокируют других (блокировки — не едят весь work_mem/temp — конфигурация — statement_timeout для таких — отдельная роль-профиль — доступы); критерии чинить: рост частоты («раз в месяц» стало «раз в час» — автоматизация — тренд по calls в мониторинге (метрики — алерт на рост — алерты), влияние на соседей (temp spill/IO-пики — p99 других запросов плавает — гистограммы), риск роста данных (запрос O(N) — данные ×10 — «терпимый» станет аварийным — ёмкость-планирование — оценка на рост — планирование); практика: реестр «терпимых» (список с датой ревизии — техдолг-учёт — «терпимо до 10M строк / до автоматизации» — критерий записан — не «забыли» — ADR).
Как связать pg_stat_statements с планами и индексами в регулярной практике? Еженедельный ритуал (15 минут): топ-10 total_exec_time → по каждому: rows-эффективность (прочитано/возвращено — большое соотношение — план-ревизия — EXPLAIN), blks hit ratio (низкий — кэш/индекс — индексы), temp_blks (spill — work_mem/запрос — конфигурация), новые записи (появились после релиза — CI-контекст — ревью запросов нового кода — ревью-чеклист); инструментарий: pgBadger (анализ логов — медленные запросы с контекстом — логи), postgres_exporter+Grafana (тренды топ-запросов — метрики — дашборд), pganalyze-класс (managed-аналитика — индекс-рекомендации — buy-путь — выбор); авто-рекомендации осторожно («создать индекс X» без контекста — write-стоимость индекса (vacuum — ёмкость-записи — решение человека — индексы-дисциплина); закрепление находок: каждый фикс — миграция+тест (тесты — «индекс удалён в будущем рефакторинге» — тест производительности поймает — N+1-тесты — регресс-защита — техдолг-профилактика).
Источники: документация PostgreSQL (pg_stat_statements, monitoring stats, EXPLAIN) — ссылки в шапке.