Почему индекс не используется
Индекс создан, а запрос всё равно идёт через Seq Scan — это не баг, а закономерность: есть несколько типичных причин, по которым планировщик не может или не хочет пользоваться индексом.
«Индекс не используется» — ситуация, когда подходящий по столбцу индекс существует, но в плане запроса стоит
Seq Scan. Чаще всего виноваты функция или каст на столбце, плохая статистика, низкая селективность условия или форма предиката, к которой индекс не приспособлен.
Это финальный урок раздела и самый практичный: вы научились выбирать тип индекса, читать план и проектировать составные и покрывающие индексы — теперь разберём, почему всё это иногда не срабатывает. Зная типичные ловушки, вы будете находить причину за минуты, глядя в EXPLAIN, а не переписывать запрос наугад.
Зачем это на практике
«Добавил индекс — не помогло» — одна из самых частых жалоб на производительность. Причина почти всегда в одном из нескольких шаблонов ниже. Умение распознать их экономит часы: вместо того чтобы плодить индексы или винить СУБД, вы точечно меняете запрос или индекс. Все случаи объединяет один диагностический ход — посмотреть план в EXPLAIN и увидеть Seq Scan там, где ждали Index Scan.
Функция на столбце ломает индекс
Самая распространённая причина. Обычный индекс хранит значения столбца как есть. Стоит обернуть столбец функцией в условии — и индекс становится неприменим, ведь в нём нет вычисленных значений.
CREATE INDEX idx_users_email ON users (email);
-- индекс НЕ работает: в нём хранится email, а не lower(email)
SELECT * FROM users WHERE lower(email) = '[email protected]'; -- Seq Scan
-- индекс НЕ работает: функция на столбце даты
SELECT * FROM orders WHERE date(created_at) = '2026-06-15'; -- Seq Scan
Два лекарства. Первое — индекс по выражению из прошлого урока: CREATE INDEX ... ON users (lower(email)), и тогда WHERE lower(email) = ... ускорится. Второе, часто лучшее, — переписать условие так, чтобы столбец остался «голым». Вместо функции по дате используйте диапазон по самому столбцу:
-- столбец без функции → обычный B-tree по created_at сработает
SELECT * FROM orders
WHERE created_at >= '2026-06-15' AND created_at < '2026-06-16';
Правило простое: держите индексируемый столбец в условии нетронутым — без функций и арифметики слева от оператора. WHERE price * 1.2 > 100 индекс не использует, а равносильное WHERE price > 100 / 1.2 — использует.
Неявные касты типов
Тонкая версия той же проблемы. Если тип столбца и тип значения в условии не совпадают, PostgreSQL вставляет неявное приведение — а оно работает как функция на столбце и может отключить индекс. Классика — числовой столбец и строковый литерал или наоборот.
-- столбец code имеет тип text, а сравниваем с числом
SELECT * FROM products WHERE code = 1024;
-- PostgreSQL приводит code к числу: code::int = 1024 → индекс по code (text) не сработает
Лечение — сравнивать значение того же типа, что и столбец: для текстового code писать code = '1024'. Тот же подвох ловит соединения таблиц: если orders.user_id имеет тип bigint, а users.id — integer, при JOIN возникает каст, и индекс по ключу может проигнорироваться. Согласуйте типы столбцов в схеме.
Низкая селективность: индекс просто невыгоден
Иногда индекс есть, типы совпадают, функций нет — а планировщик всё равно выбирает Seq Scan. И он прав. Селективность — это насколько условие сужает выборку. Если запрос возвращает большую долю таблицы, прочитать её подряд дешевле, чем прыгать по индексу к половине строк.
-- столбец gender принимает всего два значения: условие отбирает ~50% строк
SELECT * FROM users WHERE gender = 'F';
-- Seq Scan — и это оптимально: индекс тут только замедлил бы
Классический пример — булев флаг или столбец с малым числом значений: is_active, gender, status с двумя состояниями. Если же вам важны именно редкие строки (скажем, всего 2% заказов со статусом 'failed'), решение — частичный индекс ровно по ним: он крошечный, и планировщик им воспользуется. Низкая селективность — не повод чинить индекс, а повод понять, что полное сканирование здесь и есть правильный план.
Устаревшая статистика: ANALYZE
Планировщик выбирает план по статистике: сколько строк в таблице, сколько различных значений в столбце, как они распределены. Если данные резко изменились (массовая загрузка, заливка миллионов строк), а статистика осталась старой, планировщик оценивает стоимость по неверным числам и может выбрать Seq Scan там, где индекс был бы выгоден.
-- после большой загрузки данных обновляем статистику таблицы
ANALYZE orders;
-- проверяем: оценочные rows теперь близки к фактическим
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 42;
Признак именно этой проблемы вы уже знаете из урока про EXPLAIN: большое расхождение между оценочными rows и фактическими actual rows. Обычно за статистикой следит фоновый процесс autovacuum, но после массовых изменений и при отладке полезно вызвать ANALYZE вручную — это быстро и часто мгновенно «чинит» выбор плана.
LIKE с ведущим процентом
Обычный B-tree ускоряет LIKE 'abc%' — поиск по началу строки опирается на сортировку дерева. Но LIKE '%abc' и LIKE '%abc%' с ведущим % индекс не использует: непонятно, с какого места дерева начинать — искомое может стоять в любой позиции строки.
CREATE INDEX idx_users_email ON users (email);
SELECT * FROM users WHERE email LIKE 'anna%'; -- индекс работает (префикс)
SELECT * FROM users WHERE email LIKE '%@mail.io'; -- Seq Scan (ведущий %)
Что делать, если нужен поиск по подстроке или по концу строки. Для суффикса — завести индекс по перевёрнутой строке (reverse(email)) и искать префикс в ней. Для произвольной подстроки и нечёткого поиска в PostgreSQL есть расширение pg_trgm и индекс GIN по триграммам — он умеет ускорять даже LIKE '%abc%'. А полноценный поиск по словам стоит делать не через LIKE, а через полнотекстовый поиск с GIN-индексом из первого урока.
OR мешает индексу
Условие через OR по разным столбцам мешает использовать индекс целиком: планировщику трудно объединить два независимых условия одним проходом по индексу, и он часто выбирает Seq Scan.
-- OR по двум разным столбцам — индекс по одному из них может не примениться
SELECT * FROM users WHERE email = '[email protected]' OR phone = '+70000000000';
-- эквивалент через UNION: каждая ветвь использует свой индекс
SELECT * FROM users WHERE email = '[email protected]'
UNION
SELECT * FROM users WHERE phone = '+70000000000';
Перепись через UNION позволяет каждой ветви воспользоваться своим индексом (по email и по phone), а результаты объединить. Иногда планировщик и сам справляется через bitmap-сканирование (объединяет результаты двух индексов битовой картой), но при сложных OR переписать на UNION или вынести условие — надёжный приём.
Как это работает под капотом
За всеми случаями стоит одна и та же логика стоимостного оптимизатора. Индекс применим, только если условие можно превратить в границы поиска по дереву (Index Cond): функция или каст на столбце разрушают это соответствие, ведущий % не даёт начальной границы, OR по разным столбцам не сводится к одному диапазону. Но даже когда индекс применим, планировщик сравнивает его стоимость со стоимостью Seq Scan и выбирает дешевле: при низкой селективности или неверной (устаревшей) статистике оценка склоняется к полному сканированию. Поэтому диагностика всегда двухшаговая: сперва понять, может ли индекс примениться к такому предикату, затем — выгодно ли это по оценке планировщика. Ответ на оба вопроса всегда виден в EXPLAIN.
Частые ошибки
- Оборачивать столбец функцией в условии.
lower(col),date(col),col * 2отключают обычный индекс. Держите столбец «голым» или создайте индекс по выражению. - Сравнивать с литералом чужого типа. Текстовый столбец с числом (или наоборот) рождает неявный каст, который ломает индекс. Сравнивайте значением того же типа и согласуйте типы в схеме.
- Винить планировщик за Seq Scan при низкой селективности. Если условие отбирает большую долю строк, полное сканирование оптимально; для редких значений используйте частичный индекс.
- Забывать про ANALYZE после массовой загрузки. Устаревшая статистика — частая причина внезапно «плохих» планов; обновите её вручную.
- Искать подстроку через LIKE '%...%'. Ведущий
%исключает обычный индекс; для этого нужен GIN по триграммам (pg_trgm) или полнотекстовый поиск.
Итоги
- Функция или арифметика на столбце (
lower(col),col*2) отключают обычный индекс — переписывайте условие или делайте индекс по выражению. - Несовпадение типов столбца и литерала вызывает неявный каст, который тоже ломает индекс; согласуйте типы.
- При низкой селективности (мало различных значений, большая доля строк)
Seq Scanоптимален; для редких строк помогает частичный индекс. - После массовых изменений данных обновляйте статистику командой
ANALYZE— иначе планировщик ошибается. LIKEс ведущим%иORпо разным столбцам индекс не используют; помогаютpg_trgm/полнотекст и переписывание наUNION.- Диагностика всегда через
EXPLAIN: увиделиSeq ScanвместоIndex Scan— проверьте этот список.