Базы данных

Индексы в PostgreSQL: когда помогают, когда мешают и как это проверить

27.08.2026 · TitarX · 👁 0

Совет «добавьте индекс» звучит так же часто, как «попробуйте перезагрузить». Иногда помогает. Иногда запрос остаётся медленным, а вставка становится заметно дороже — и непонятно, стало ли лучше в целом.

Разберёмся, что индекс делает на самом деле и как убедиться, что он действительно используется.

Чем вы платите

Индекс — это дополнительная структура, которую база обязана обновлять при каждой записи. Пять индексов на таблице означают, что один INSERT превращается в шесть операций записи: сама строка плюс пять обновлений индексов.

Отсюда правило, которое экономит много времени: индексы добавляют не «на всякий случай», а под конкретный запрос, который действительно выполняется и действительно медленный.

Как узнать, что происходит

Единственный надёжный способ — спросить у планировщика:

EXPLAIN (ANALYZE, BUFFERS)
SELECT id, title
FROM articles
WHERE category_id = 3
ORDER BY published_at DESC
LIMIT 20;

EXPLAIN покажет план, ANALYZE — реально выполнит запрос и добавит фактическое время, BUFFERS — объём прочитанных страниц.

Главное, что нужно увидеть в выводе: Seq Scan означает последовательное чтение всей таблицы, Index Scan — обращение через индекс. И ещё одна строка, которую часто пропускают: расхождение между rows= в оценке и в факте. Если планировщик ожидал 10 строк, а получил 100 000, он выбрал стратегию под неверную гипотезу — и проблема, скорее всего, в устаревшей статистике, а не в индексе.

Почему индекс не используется

Несколько типичных причин.

Функция поверх колонки. Индекс по created_at не поможет запросу с DATE(created_at) = '2026-01-01': для базы это выражение, а не колонка. Решения два — переписать условие диапазоном или создать индекс по выражению:

-- диапазон вместо функции: индекс по created_at снова работает
WHERE created_at >= '2026-01-01' AND created_at < '2026-01-02';

-- либо индекс по тому же выражению
CREATE INDEX idx_articles_created_date ON articles ((DATE(created_at)));

Порядок колонок в составном индексе. Индекс (category_id, published_at) годится для фильтра по категории и для комбинации «категория + сортировка по дате», но не для запроса, где есть только published_at. Работает правило префикса: использовать можно любое начало списка колонок, но не середину.

Низкая селективность. Если условию удовлетворяет половина таблицы, последовательное чтение дешевле похода в индекс и обратно к строкам. Планировщик выбирает Seq Scan осознанно, и это правильное решение.

Устаревшая статистика. После массовой загрузки данных имеет смысл выполнить ANALYZE tablename; — иначе планировщик рассуждает о таблице, которой уже нет.

Частичный индекс

Когда запросы всегда касаются небольшого подмножества строк, индексировать всю таблицу незачем:

CREATE INDEX idx_articles_published
    ON articles (published_at DESC)
    WHERE is_published = true;

Такой индекс меньше, быстрее обновляется и точнее отражает реальную нагрузку — при условии, что запрос содержит то же условие WHERE is_published = true, иначе планировщик им не воспользуется.

Поиск по тексту

Для LIKE '%подстрока%' обычный B-tree бесполезен: он упорядочен по началу строки. Здесь нужен GIN с триграммами:

CREATE EXTENSION IF NOT EXISTS pg_trgm;

CREATE INDEX idx_articles_title_trgm
    ON articles USING gin (title gin_trgm_ops);

Для полноценного полнотекстового поиска с учётом морфологии используется tsvector и GIN по нему — это отдельная тема, но начинать стоит с вопроса, нужен ли вам поиск по подстроке или всё-таки поиск по словам.

Порядок действий

Практическая последовательность выглядит так:

  1. найти медленные запросы (расширение pg_stat_statements),
  2. посмотреть их план через EXPLAIN (ANALYZE, BUFFERS),
  3. убедиться, что статистика свежая,
  4. добавить индекс под конкретный запрос,
  5. перепроверить план и измерить влияние на запись.

В продакшне индексы создают через CREATE INDEX CONCURRENTLY: обычный вариант держит блокировку на запись всё время построения.

Что в итоге

Индекс — это сделка: вы платите скоростью записи и местом на диске за скорость чтения. Сделка выгодна, когда заключена под конкретный запрос и проверена планом. «Добавить индекс на всякий случай» — это плата без покупки.