Когда БД может использовать SEQUENTIAL SCAN вместо индекса?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
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 когда ожидаешь индекс — проанализируй:
- Есть ли индекс?
\d+ users - Актуальна ли статистика?
ANALYZE users; - Selectivity хорошая? Проверь
n_distinctвpg_stats - 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 используется когда:
- Нет индекса
- Индекс есть, но условие его не может использовать
- Optimizer решил, что SEQUENTIAL SCAN дешевле (правильное решение для больших выборок)
- Статистика устарела
Для оптимизации: используй EXPLAIN ANALYZE, создавай индексы на часто фильтруемых колонках, и не забывай ANALYZE после загрузки данных.