PostgreSQL
Охота на медленные запросы
Приложение внезапно стало в 10 раз медленнее после деплоя. Логи приложения чистые. Как найти виновный запрос среди тысяч в секунду - за 5 минут, а не за 5 часов?
- **Basecamp** публично описал инцидент где N+1 запросы в новой фиче привели к 40000 запросов в секунду вместо ожидаемых 500 - pg_stat_statements показал проблему за 2 минуты: один паттерн запроса с 80000 calls/min и mean_time 0.3ms давал суммарно 70% нагрузки на БД.
- **GitLab** встроил обязательную проверку pg_stat_statements в свой SRE runbook: при любом инциденте производительности первый шаг - сравнить топ-10 запросов по total_exec_time с baseline из прошлой недели. Это позволяет изолировать регрессию за 5-10 минут.
- **Heroku** автоматически включает log_min_duration_statement = 2000 на всех Postgres планах и отправляет slow query alerts в Datadog - команды получают уведомление в Slack в течение минуты после появления медленного запроса.
- **Notion** при анализе post-incident обнаружил, что 60% медленных запросов содержали `SELECT *` на таблице blocks с 200+ колонками; замена на выборку нужных полей снизила сетевой трафик на 40% и время ответа API с 1.2s до 0.3s в среднем.
pg_stat_statements: аналитика по запросам
`pg_stat_statements` - расширение PostgreSQL, которое накапливает агрегированную статистику по всем выполненным запросам: суммарное время, количество вызовов, блоковые операции. Это первый инструмент для поиска медленных запросов на продакшене - в отличие от логов, агрегирует одинаковые запросы с разными параметрами.
pg_stat_statements нормализует запросы: `WHERE id = $1` объединяет все варианты `WHERE id = 1`, `WHERE id = 42` и т.д. Это делает агрегацию по паттернам запросов, а не конкретным значениям - именно то, что нужно для анализа.
pg_stat_statements показывает запрос с mean_exec_time = 2ms, но total_exec_time = 40000000ms (11 часов). Что это говорит о запросе?
auto_explain: планы для медленных запросов
`auto_explain` - расширение, которое автоматически логирует план выполнения для запросов, превысивших заданный порог времени. В отличие от pg_stat_statements, показывает не только время, но и полный EXPLAIN ANALYZE план - включая какие узлы были медленными.
auto_explain с log_analyze=true выполняет EXPLAIN ANALYZE для каждого медленного запроса - это добавляет незначительный overhead (измерения времени). На продакшене рекомендуется устанавливать log_min_duration не ниже 500ms, иначе объём логов станет неуправляемым.
В чём ключевое преимущество auto_explain перед ручным EXPLAIN ANALYZE для диагностики?
log_min_duration_statement
`log_min_duration_statement` - параметр PostgreSQL для логирования медленных запросов непосредственно в PostgreSQL log. В отличие от auto_explain, логирует только время и текст запроса (без плана), что значительно дешевле по overhead. Первый шаг любого performance investigation.
Установка log_min_duration_statement = 0 логирует все запросы - это катастрофически нагружает диск и I/O на нагруженном сервере. Разумный продакшен порог: 500ms-2000ms в зависимости от требований к latency приложения.
В логах появляются строки `LOG: temporary file: path ..., size 524288000`. Что это означает?
pgBadger: анализ логов
`pgBadger` - инструмент для парсинга и анализа PostgreSQL логов. Генерирует HTML-отчёт со статистикой: топ медленных запросов, распределение по времени суток, lock waits, connection counts, checkpoint activity. Незаменим при расследовании инцидентов - можно восстановить картину нагрузки за конкретный период.
Для работы pgBadger критически важен корректный `log_line_prefix`. Рекомендуемый формат: `'%t [%p]: [%l-1] user=%u,db=%d,app=%a,client=%h '`. Без поля `%t` (timestamp) pgBadger не может построить временную шкалу.
pgBadger показывает пик медленных запросов каждый день в 03:00 ночи. Что это скорее всего означает?
Типичные антипаттерны медленных запросов
Большинство проблем с производительностью PostgreSQL сводится к нескольким повторяющимся паттернам. Знание этих антипаттернов позволяет быстро диагностировать и исправлять типичные проблемы.
- **N+1 запросы**: приложение делает 1 запрос, получает N строк, затем N отдельных запросов для каждой. Решение: JOIN или IN ($ids) в одном запросе.
- **SELECT * вместо конкретных колонок**: лишние колонки увеличивают сетевой трафик и мешают Index Only Scan.
- **LIKE '%pattern%'**: ведущий wildcard делает индекс бесполезным; Seq Scan на каждом запросе. Решение: pg_trgm extension для trigram индексов.
- **Функции в WHERE**: `WHERE LOWER(email) = $1` не использует индекс на email. Решение: functional index `CREATE INDEX ON users (LOWER(email))`.
- **Отсутствие LIMIT**: аналитический запрос без LIMIT в OLTP-сервисе читает миллионы строк. Всегда добавлять LIMIT для пагинации.
- **Implicit type cast**: `WHERE user_id = '12345'` (integer vs text) вызывает implicit cast и Seq Scan. Совпадение типов обязательно.
Медленный запрос можно исправить только добавлением индекса
Большинство медленных запросов исправляется переписыванием самого запроса: устранением функций в WHERE, добавлением LIMIT, использованием JOIN вместо N+1, выбором конкретных колонок вместо SELECT *
Индекс помогает только если запрос написан так, чтобы его использовать. Функция в WHERE, implicit cast, LIKE с leading wildcard - всё это делает индекс невидимым для планировщика. Сначала переписать запрос, потом думать об индексах.
Запрос `SELECT * FROM events WHERE DATE(created_at) = '2024-03-15'` не использует индекс на created_at. Почему?
Ключевые идеи
- **pg_stat_statements** - первый инструмент: агрегирует запросы по паттернам, показывает total_exec_time и temp_blks_written; работает всегда без ручного вмешательства.
- **auto_explain** автоматически логирует EXPLAIN ANALYZE для запросов выше порога - позволяет поймать редкие деградации с реальными данными без воспроизведения.
- **log_min_duration_statement** - дешёвый способ зафиксировать медленные запросы; log_temp_files обнаруживает disk spills из-за малого work_mem.
- **Топ антипаттерны**: функции в WHERE, LIKE с leading wildcard, SELECT *, N+1 запросы, отсутствие LIMIT, implicit type cast - большинство решается переписыванием запроса, не добавлением индексов.
Связанные темы
Диагностика медленных запросов соединяет все инструменты оптимизации PostgreSQL:
- EXPLAIN ANALYZE — После обнаружения медленного запроса через pg_stat_statements - следующий шаг изучить его план через EXPLAIN ANALYZE
- Планировщик и статистика — Медленный запрос часто означает плохой план из-за устаревшей статистики - нужно понять как планировщик строит оценки
- Обслуживание индексов — Отсутствие нужного индекса или bloat на существующем - частая причина медленных запросов, обнаруживаемая через Seq Scan в EXPLAIN
Вопросы для размышления
- Включён ли pg_stat_statements на вашем продакшен кластере, и когда последний раз просматривалась статистика медленных запросов?
- Какой порог log_min_duration_statement адекватен для вашего приложения с учётом его SLA по latency?
- Есть ли в кодовой базе запросы с функциями в WHERE (LOWER(), DATE(), EXTRACT()) которые могут не использовать индексы?