← Назад к вопросам

Когда БД может использовать SEQUENTIAL SCAN вместо индекса?

2.7 Senior🔥 81 комментариев
#Базы данных (SQL)

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

SEQUENTIAL SCAN vs INDEX SCAN в PostgreSQL

Постгрес использует SEQUENTIAL SCAN вместо индекса в ряде ситуаций. Понимание этих причин критично для оптимизации запросов и избежания неожиданного замораживания production.

Основные причины использования SEQUENTIAL SCAN

1. Выборка большой части таблицы

Если query optimizer подсчитает, что нужно прочитать более 10-20% строк таблицы, он выберет SEQUENTIAL SCAN вместо индекса. Это логично: читать индекс + переходить по указателям дороже, чем просто прочитать всю таблицу.

-- ❌ Для таблицы с 1 млн записей это может быть SEQUENTIAL SCAN
SELECT * FROM users WHERE status IN ('active', 'pending', 'inactive');

-- ✅ Если нужна малая часть, скорее всего INDEX SCAN
SELECT * FROM users WHERE status = 'active';

2. Дешевле читать последовательно

Диск оптимизирован для последовательного чтения. Произвольные seek'и к индексу + случайные чтения страниц таблицы могут быть медленнее, чем линейное сканирование.

-- PostgreSQL выбирает SEQUENTIAL SCAN, так как результат большой
EXPLAIN SELECT * FROM orders WHERE user_id > 100;
-- Seq Scan on orders  (cost=0.00..50000.00 rows=500000)

3. Отсутствие индекса

Очевидная причина: если нет индекса на колонке, осталось только SEQUENTIAL SCAN:

-- ❌ Нет индекса на email
SELECT * FROM users WHERE email = 'test@example.com';

-- ✅ Создаём индекс
CREATE INDEX idx_users_email ON users(email);

4. Условие не позволяет использовать индекс

Операции, которые не используют индекс:

-- ❌ LIKE с % в начале
SELECT * FROM products WHERE name LIKE '%phone%';
-- SEQUENTIAL SCAN, т.к. индекс B-tree работает только с префиксами

-- ✅ LIKE с % в конце
SELECT * FROM products WHERE name LIKE 'phone%';
-- Может использовать индекс

-- ❌ Функции на индексированной колонке
SELECT * FROM users WHERE UPPER(name) = 'JOHN';
-- SEQUENTIAL SCAN, т.к. индекс на name, а не на UPPER(name)

-- ✅ Функциональный индекс
CREATE INDEX idx_users_name_upper ON users(UPPER(name));

-- ❌ Арифметические операции
SELECT * FROM products WHERE price * 1.2 > 100;
-- SEQUENTIAL SCAN

-- ✅ Переформатируй условие
SELECT * FROM products WHERE price > 100 / 1.2;

5. Статистика устарела

PostgreSQL полагается на ANALYZE для подсчёта статистики. Если её не обновлять, optimizer может неправильно оценить selectivity:

-- Обновляем статистику
ANALYZE users;

-- Или для всей БД
ANALYZE;

-- Проверяем статистику
SELECT * FROM pg_stats WHERE tablename = 'users';

6. Индекс неэффективен для условия

-- Если индекс на (user_id, created_at), а запрос по (created_at)
-- PostgreSQL может выбрать SEQUENTIAL SCAN, т.к. индекс отсортирован по user_id
SELECT * FROM orders WHERE created_at > '2024-01-01';

Как проверить план выполнения

EXPLAIN ANALYZE SELECT * FROM users WHERE id = 1;

Результат:

Seq Scan on users  (cost=0.00..35.50 rows=1 width=32)
  Filter: (id = 1)
  Planning Time: 0.032 ms
  Execution Time: 0.125 ms

Eсли видишь Seq Scan когда ожидаешь индекс — проанализируй:

  1. Есть ли индекс? \d+ users
  2. Актуальна ли статистика? ANALYZE users;
  3. Selectivity хорошая? Проверь n_distinct в pg_stats
  4. Podmнока написана правильно? (не функции, не OR, не приведение типов)

Cost параметры

Оптимизер выбирает SEQUENTIAL SCAN, если cost меньше. Можно настроить:

SET random_page_cost = 1.0;  -- На SSD вместо 4.0
SET seq_page_cost = 0.1;      -- Снижает цену последовательного чтения

-- Проверяем текущие значения
SHOW random_page_cost;
SHOW seq_page_cost;

Практические примеры

1. Большой IN список:

-- ❌ SEQUENTIAL SCAN (слишком много значений)
SELECT * FROM users WHERE id IN (1, 2, 3, 4, 5, ..., 50000);

-- ✅ Используй JOIN с temp таблицей или подзапрос
WITH ids AS (
  SELECT UNNEST(ARRAY[1, 2, 3, ...]) AS id
)
SELECT u.* FROM users u JOIN ids ON u.id = ids.id;

2. Полнотекстовый поиск:

-- ❌ SEQUENTIAL SCAN для нечёткого поиска
SELECT * FROM articles WHERE content LIKE '%javascript%';

-- ✅ Используй полнотекстовый индекс
CREATE INDEX idx_articles_content_fts ON articles USING gin(to_tsvector('english', content));
SELECT * FROM articles WHERE to_tsvector('english', content) @@ to_tsquery('javascript');

3. Условие с OR:

-- ❌ SEQUENTIAL SCAN если слагаемые несопместимы
SELECT * FROM orders WHERE status = 'completed' OR user_id = 5;

-- ✅ UNION быстрее
SELECT * FROM orders WHERE status = 'completed'
UNION
SELECT * FROM orders WHERE user_id = 5;

Когда SEQUENTIAL SCAN — это нормально

  • Таблица маленькая (< 10000 строк)
  • Нужна вся таблица (без WHERE)
  • Первый запуск (ещё нет статистики, но ANALYZE должен был создаться при миграции)

Итого

SEQUENTIAL SCAN используется когда:

  1. Нет индекса
  2. Индекс есть, но условие его не может использовать
  3. Optimizer решил, что SEQUENTIAL SCAN дешевле (правильное решение для больших выборок)
  4. Статистика устарела

Для оптимизации: используй EXPLAIN ANALYZE, создавай индексы на часто фильтруемых колонках, и не забывай ANALYZE после загрузки данных.