Запрос тормозит. Не гадайте, смотрите план.
EXPLAIN и EXPLAIN ANALYZE
EXPLAIN SELECT ...показывает план и оценки планировщика, запрос не выполняется.EXPLAIN ANALYZE SELECT ...выполняет запрос и показывает фактическое время и число строк. Осторожно сUPDATEиDELETE: они реально изменят данные (оборачивайте вBEGIN ... ROLLBACK).EXPLAIN (ANALYZE, BUFFERS)добавляет, сколько страниц прочитано из кеша (shared hit) и с диска (read).
Index Scan using orders_customer_id_idx on orders
(cost=0.43..8.45 rows=3 width=40) (actual time=0.020..0.025 rows=3 loops=1)
Index Cond: (customer_id = 42)
cost=0.43..8.45: стоимость до первой строки и до последней, в условных единицах.rows=3: оценка планировщика. Рядомactual rows: факт.loops: сколько раз выполнялся узел. Время и строки указаны на один проход.
План читается изнутри наружу: самые вложенные узлы выполняются первыми.
Виды чтения
- Seq Scan: читает всю таблицу. Нормально для маленьких таблиц и когда нужна большая доля строк.
- Index Scan: идёт по индексу и за каждой строкой в таблицу.
- Index Only Scan: всё нужное в индексе, в таблицу почти не ходит.
- Bitmap Index Scan + Bitmap Heap Scan: собирает из индекса битовую карту страниц, потом читает страницы по порядку. Хорош для «средней» селективности и для комбинации нескольких индексов.
Виды соединений
- Nested Loop: для каждой строки внешней стороны ищет пару во внутренней. Отлично, когда внешних строк мало, а внутри есть индекс.
- Hash Join: строит хеш-таблицу по меньшей стороне, потом проходит большую. Хорош для больших наборов и равенства.
- Merge Join: обе стороны отсортированы по ключу, идёт «молнией».
Главный сигнал: оценка против факта
Если rows=10, а actual rows=500000, планировщик ошибся и выбрал плохой план (например, Nested Loop на полмиллиона строк). Причины: устаревшая статистика, коррелирующие колонки, сложные выражения. Лечение: ANALYZE таблица, увеличить default_statistics_target для колонки, расширенная статистика CREATE STATISTICS для зависимых колонок.
Типовые проблемы и решения
- Нет индекса под условие или JOIN: добавить, учитывая порядок колонок.
- Функция над колонкой в WHERE: индекс по выражению или переписать условие (
created_at >= '2026-01-01' AND created_at < '2026-01-02'вместоdate(created_at) = ...). - Сортировка на диске: в плане
Sort Method: external merge Disk. Помогает индекс под ORDER BY или большеwork_mem. - Большой OFFSET: keyset-пагинация
WHERE (created_at, id) < ($1, $2) ORDER BY created_at DESC, id DESC LIMIT 20. - SELECT *: тянет лишние колонки, мешает Index Only Scan.
- N+1: приложение делает 1 запрос за списком и N запросов за деталями. Лечится JOIN,
WHERE id = ANY($1)или пакетной загрузкой в ORM. - Раздутая таблица после массовых UPDATE и DELETE: проверить autovacuum.
Наблюдение
pg_stat_statements: какие запросы суммарно съедают больше всего времени. Оптимизируют их, а не самый медленный одиночный.log_min_duration_statement: писать в лог запросы дольше N мс.auto_explain: автоматически логировать планы медленных запросов.
Частые ошибки
- Смотреть только
EXPLAINбезANALYZEи верить оценкам. - Запустить
EXPLAIN ANALYZE DELETEна проде. - Считать любой Seq Scan проблемой.
- Оптимизировать запрос, который выполняется раз в день, вместо того, что вызывается 1000 раз в секунду.
Что спросят на собесе
- Как найти и разобрать медленный запрос.
- Чем EXPLAIN отличается от EXPLAIN ANALYZE.
- Какие бывают сканы и соединения, когда какой хорош.
- Что такое проблема N+1 и как её лечить.
- Почему планировщик выбрал seq scan, хотя индекс есть.