Содержание
Медленный запрос в PostgreSQL почти всегда ускоряется одним правильным индексом, но угадывать его не нужно: план выполнения из EXPLAIN ANALYZE показывает, где база читает лишние строки. Порядок простой — найти медленные запросы через pg_stat_statements, посмотреть план, добавить индекс под фильтр и сортировку и проверить результат. Лишние индексы при этом замедляют запись.
Разберём этот порядок по шагам, с примерами запросов и типичными ошибками.
Почему запросы в PostgreSQL становятся медленными?
Запросы становятся медленными, когда базе приходится читать гораздо больше строк, чем нужно для ответа. Пока таблица маленькая, полный перебор незаметен; когда в ней миллионы строк, тот же запрос начинает занимать секунды.
Типичные причины:
- нет индекса под условие
WHEREили под сортировкуORDER BY; - индекс есть, но условие написано так, что он не используется, — например, функция над столбцом;
- приложение делает сотни мелких запросов вместо одного (проблема N+1);
- устаревшая статистика, из-за которой планировщик выбирает неудачный план;
- запрос тянет лишние столбцы или все строки, когда нужна одна страница.
Индексы решают первые две причины. Остальные лечатся в коде приложения и настройках базы.
Как найти медленные запросы?
Медленные запросы находят по статистике базы, а не по ощущениям: расширение pg_stat_statements собирает время выполнения всех запросов, и достаточно отсортировать их по суммарному времени. Часто главным окажется не самый медленный запрос, а быстрый, который вызывается миллионы раз.
-- Топ запросов по суммарному времени выполнения
SELECT
round(total_exec_time::numeric / 1000, 1) AS total_sec,
calls,
round(mean_exec_time::numeric, 1) AS avg_ms,
left(query, 120) AS query
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;
Расширение нужно включить в shared_preload_libraries и создать командой CREATE EXTENSION pg_stat_statements. В управляемых облачных базах оно обычно уже доступно. Дополнительно полезен параметр log_min_duration_statement, который пишет в лог все запросы дольше заданного порога.
Как читать план выполнения запроса?
План выполнения показывает, как PostgreSQL на самом деле выполнил запрос: какие таблицы читал, каким способом и сколько времени заняла каждая операция. Команда EXPLAIN (ANALYZE, BUFFERS) выполняет запрос и выводит фактические числа.
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, total, created_at
FROM orders
WHERE customer_id = 4821 AND status = 'paid'
ORDER BY created_at DESC
LIMIT 20;
На что смотреть в первую очередь:
- Seq Scan по большой таблице — база читает её целиком; часто это признак отсутствия индекса.
- Rows Removed by Filter — сколько строк прочитано и выброшено; чем больше, тем хуже.
- Sort с большим числом строк — сортировка не использует индекс.
- Разница между rows в оценке и фактическим числом строк — признак устаревшей статистики.
Фото: Unsplash
Какой индекс создать под запрос?
Индекс создают под то, как запрос фильтрует и сортирует данные: сначала столбцы из условий равенства, затем столбец сортировки или диапазона. Для запроса выше подойдёт составной индекс, который сразу отдаёт нужные строки в нужном порядке.
CREATE INDEX CONCURRENTLY orders_customer_status_created_idx
ON orders (customer_id, status, created_at DESC);
CONCURRENTLY позволяет создать индекс, не блокируя запись в таблицу, — на продакшене это обязательно. Какие индексы выбирать под разные задачи:
| Тип индекса | Для чего подходит | Пример |
|---|---|---|
| B-tree (по умолчанию) | Равенство, диапазоны, сортировка | Заказы клиента по дате |
| Составной | Несколько условий и сортировка сразу | (customer_id, status, created_at) |
| Частичный | Условие по небольшой части таблицы | Только активные заказы: WHERE status = 'new' |
| По выражению | Условие с функцией над столбцом | lower(email) для поиска без учёта регистра |
| GIN | JSONB, массивы, полнотекстовый поиск | Поиск по полям в JSONB |
| BRIN | Огромные таблицы, данные упорядочены по времени | Логи и события по дате |
Когда индекс не используется
Индекс не поможет, если условие не совпадает с тем, как он построен. WHERE lower(email) = ... не использует индекс по email — нужен индекс по выражению. Составной индекс (customer_id, status) не поможет запросу, который фильтрует только по status. А если условие выбирает большую часть таблицы, полный перебор действительно быстрее, и планировщик прав.
Почему лишние индексы вредят?
Лишние индексы вредят, потому что каждый из них обновляется при каждой вставке и изменении строки, занимает место на диске и в памяти. Таблица с десятком индексов, половина из которых не используется, медленнее принимает данные и дольше восстанавливается из бэкапа.
Как держать индексы в порядке:
- проверяйте неиспользуемые индексы по
pg_stat_user_indexes: столбецidx_scanу них равен нулю; - удаляйте дубликаты — индекс
(a)не нужен, если есть(a, b); - не создавайте индекс «на всякий случай» — только под реальный запрос из статистики;
- после массовых изменений данных запускайте
ANALYZE, чтобы обновить статистику.
Если вы ещё выбираете базу для проекта, возможности индексов — один из доводов в пользу PostgreSQL, которые мы разбирали в статье PostgreSQL или MySQL.
Коротко
- Ищите медленные запросы через pg_stat_statements, сортируя по суммарному времени.
- Читайте план через
EXPLAIN (ANALYZE, BUFFERS): Seq Scan и много отброшенных строк — сигнал к индексу. - Составной индекс строится так: сначала столбцы равенства, потом сортировка или диапазон.
- На продакшене создавайте индексы с
CONCURRENTLY, чтобы не блокировать запись. - Удаляйте неиспользуемые индексы — они замедляют запись и занимают память.
Частые вопросы
Сколько индексов может быть у одной таблицы?
Технического ограничения, которое мешало бы на практике, нет, но каждый индекс замедляет запись. Держите столько, сколько нужно реальным запросам, и регулярно удаляйте неиспользуемые.
Индексирует ли PostgreSQL внешние ключи автоматически?
Нет. Индекс создаётся автоматически только для первичного ключа и уникальных ограничений. Столбцы внешних ключей, по которым идут выборки и удаления, стоит индексировать вручную.
Поможет ли индекс поиску по части слова?
Обычный B-tree помогает только поиску по началу строки. Для поиска по подстроке используют расширение pg_trgm с GIN-индексом, а для поиска по словам — полнотекстовый поиск PostgreSQL.



