PostgreSQLSeniorExperience
Представьте, запросы к PostgreSQL резко замедлились после релиза или роста данных. Как вы будете искать причину?
Алгоритм: pg_stat_activity (активные запросы/локи) → pg_stat_statements (топ по total_exec_time) → EXPLAIN ANALYZE BUFFERS (план, кэш, spill) → ANALYZE/VACUUM (статистика/bloat) → iostat + bgwriter (диск/checkpoint).
Расследование резкого замедления запросов в PostgreSQL
Резкое замедление после релиза или роста данных — разные сценарии, но подход один: сначала измеряем, потом выдвигаем гипотезы, не наоборот. Ниже — пошаговый playbook.
Фаза 1: Установить факты за 5 минут
-- 1. Кто тормозит прямо сейчас?
SELECT pid, now() - query_start AS age, state,
wait_event_type, wait_event, left(query, 200) AS q
FROM pg_stat_activity
WHERE state != 'idle'
ORDER BY age DESC LIMIT 20;
-- 2. Есть ли блокировки?
SELECT bl.pid AS blocked, a.query AS blocked_q,
kl.pid AS blocker, ka.query AS blocker_q
FROM pg_blocking_pids(bl.pid) AS kl(pid)
JOIN pg_stat_activity bl ON bl.pid = ANY(pg_blocking_pids(bl.pid))
JOIN pg_stat_activity a ON a.pid = bl.pid
JOIN pg_stat_activity ka ON ka.pid = kl.pid;
-- 3. Сколько соединений?
SELECT count(*), state FROM pg_stat_activity GROUP BY state;
Ответы на эти три вопроса сразу показывают: блок на локе → смотрим что держит лок; много idle in transaction → утечка незакрытых транзакций в приложении; max_connections исчерпан → нужен пуллер или срочная перезагрузка.
Фаза 2: Определить «плохие» запросы
-- Топ по total_exec_time (нужен pg_stat_statements)
SELECT queryid, calls, round(total_exec_time::numeric, 1) AS total_ms,
round(mean_exec_time::numeric, 2) AS mean_ms,
round(stddev_exec_time::numeric, 2) AS stddev_ms,
left(query, 120) AS q
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 15;
-- Сравниваем mean_exec_time с историческими данными (Grafana / pgBadger)
-- Резкий рост среднего — план изменился или статистика устарела
Фаза 3: Проверить план конкретного запроса
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT o.id, u.email, o.total
FROM orders o
JOIN users u ON u.id = o.user_id
WHERE o.status = 'pending'
ORDER BY o.created_at DESC
LIMIT 100;
Что смотреть в выводе:
- Rows estimated vs actual: расхождение в 100x — устаревшая статистика, запустить
ANALYZE. - Seq Scan на большой таблице: нет индекса или plannerне использует его (см. random_page_cost).
- Buffers: shared read=huge: данные не в кэше, нагрузка на диск. Проверить shared_buffers и реальное потребление RAM.
- Sort Method: external merge: work_mem не хватает, сортировка идёт через диск.
- Hash Batches: >1: hash join сделал spill на диск из-за малого work_mem.
Фаза 4: Гипотезы по сценарию «после релиза»
- Новый код добавил N+1 запросы: в pg_stat_statements появилось 10K calls/s запросов, которых раньше не было. Ищем по queryid или по query текстом.
- Миграция взяла лок и держит его: долгая транзакция ALTER TABLE. Проверяем pg_locks + pg_stat_activity.
- Новый запрос без индекса стал делать Seq Scan: в pg_stat_user_tables вырос seq_scan на нужной таблице.
- Изменился параметр приложения: например, limit убрали → запрос возвращает 1M строк вместо 100.
Фаза 5: Гипотезы по сценарию «после роста данных»
- Планировщик переключился с Index Scan на Seq Scan: при росте таблицы selectivity индекса упала, и планировщик решил что Seq дешевле. Решение:
ANALYZE+ проверить work_mem/random_page_cost. - Bloat: таблица выросла физически из-за удалений без vacuum.
SELECT n_dead_tup FROM pg_stat_user_tables. - IOPS лимит в облаке: EBS gp2 burst balance исчерпан при росте объёма IO.
- Checkpoint storm: при росте write-нагрузки checkpoint стал слишком частым. Проверить:
SELECT * FROM pg_stat_bgwriter;, если checkpoints_req растёт — увеличить max_wal_size.
Фаза 6: Быстрые remediation
-- Устаревшая статистика
ANALYZE VERBOSE orders;
-- Убить зависшую транзакцию
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE state = 'idle in transaction'
AND now() - state_change > interval '10 minutes';
-- Принудительный vacuum
VACUUM ANALYZE orders;
-- Если планировщик упорно выбирает Seq Scan (временный workaround)
SET enable_seqscan = off; -- только для отладки, не в production постоянно
Инструменты мониторинга
- pgBadger: анализ логов PostgreSQL, топ медленных запросов по времени.
- pg_activity: htop-подобный real-time монитор запросов.
- Grafana + postgres_exporter: метрики pg_stat_statements, pg_stat_bgwriter, pg_stat_replication в реальном времени.
- auto_explain: автоматически логирует EXPLAIN для запросов дольше порога.
-- auto_explain в postgresql.conf
shared_preload_libraries = 'pg_stat_statements,auto_explain'
auto_explain.log_min_duration = '500ms'
auto_explain.log_analyze = on
auto_explain.log_buffers = on
Подводные камни
- EXPLAIN ANALYZE выполняет запрос по-настоящему — для DELETE/UPDATE используйте BEGIN ... ROLLBACK, иначе изменения применятся.
- pg_stat_statements.max по умолчанию 5000 — при большом количестве уникальных запросов старые вытесняются и история теряется.
- Параметр work_mem глобальный, но умножается на количество параллельных sort/hash операций в запросе — осторожно при увеличении.
- Bloat индексов не показывается в pg_stat_user_tables — нужен pgstattuple или регулярный REINDEX CONCURRENTLY.
- После ANALYZE план может измениться и стать хуже в редких случаях — сохраняйте старый план через pg_hint_plan для отката.
- checkpoint_completion_target по умолчанию 0.9 — хорошее значение; не снижайте его, это размажет I/O от checkpoint.
- Рост числа соединений сам по себе деградирует PostgreSQL из-за lock manager overhead — держите активные соединения в пределах 2–4x CPU cores.
- В replica-сценарии медленные запросы на replica могут быть вызваны replication lag — replica читает старые страницы, не успевающие примениться.
What hurts your answer
- Сразу обвинять PostgreSQL, не проверив соседние слои системы
- Чинить симптом без минимального воспроизведения и evidence
- Не учитывать версии, конфигурацию, окружение и recent changes
What they're listening for
- Умеет локализовать проблему вокруг PostgreSQL
- Двигается от симптома к гипотезам и проверкам
- Отличает баг инструмента от ошибки использования или окружения