Почему медленный SQL-запрос: причины и решения
Карьерник — квиз-тренажёр в Telegram с 1500+ вопросами для собесов аналитика. SQL, Python, A/B, метрики. Бесплатно.
Короткий список частых причин
- Нет индекса на колонку в WHERE / JOIN
- Seq scan больших таблиц вместо index scan
- Плохой JOIN (cartesian, wrong key)
- DISTINCT / ORDER BY без индекса
- LIKE с префиксом % — не использует индекс
- Функция в WHERE на индексированной колонке
- Устаревшая статистика планировщика
- Слишком много JOIN
- Subquery пересчитывается на каждую строку
- Нет партиционирования на огромных таблицах
Шаг 1. Посмотреть план выполнения
PostgreSQL
EXPLAIN ANALYZE
SELECT * FROM orders WHERE user_id = 42;Смотрите:
- Seq Scan — полный перебор таблицы. Плохо для больших данных.
- Index Scan — поиск по индексу. Хорошо.
- Nested Loop — O(n×m). Может быть медленно.
- Hash Join / Merge Join — обычно хорошо.
- cost / actual time — сколько реально.
MySQL
EXPLAIN SELECT * FROM orders WHERE user_id = 42;Обращайте внимание:
type:ALL(полный скан — плохо),ref/range(ок),const/eq_ref(отлично)rows: сколько строк планирует прочитатьkey: какой индекс использует
Типичные проблемы
1. Нет индекса на WHERE
SELECT * FROM orders WHERE user_id = 42;Без индекса на user_id — seq scan таблицы. Решение:
CREATE INDEX idx_orders_user_id ON orders(user_id);2. Функция на индексированной колонке
-- индекс не используется
SELECT * FROM users WHERE LOWER(email) = 'alice@example.com';
-- решение: functional index
CREATE INDEX idx_users_email_lower ON users(LOWER(email));
-- или нормализовать email до сравнения
SELECT * FROM users WHERE email = 'alice@example.com';3. LIKE с % в начале
-- не использует индекс
SELECT * FROM users WHERE name LIKE '%Alice%';
-- индекс работает
SELECT * FROM users WHERE name LIKE 'Alice%';Для поиска по подстроке — используйте full-text search (tsvector в Postgres, MATCH в MySQL).
4. Implicit type cast
-- user_id INT, но сравниваем со строкой
SELECT * FROM orders WHERE user_id = '42';Cast делается для каждой строки → индекс может не использоваться. Решение — одинаковый тип.
5. OR в WHERE
-- планировщик может не использовать индексы для OR
SELECT * FROM users WHERE email = 'a@b.c' OR phone = '123';
-- переписать через UNION
SELECT * FROM users WHERE email = 'a@b.c'
UNION
SELECT * FROM users WHERE phone = '123';6. IN с большим списком
-- 10 000 значений в IN — медленно
SELECT * FROM users WHERE user_id IN (1, 2, ..., 10000);
-- через temporary table
CREATE TEMP TABLE ids (id INT);
INSERT INTO ids VALUES (1), (2), ...;
SELECT * FROM users JOIN ids ON ids.id = users.user_id;7. Subquery вместо JOIN
-- подзапрос вычисляется для каждой строки
SELECT *,
(SELECT SUM(total) FROM orders WHERE user_id = u.id) AS total
FROM users u;
-- JOIN + GROUP BY работает быстрее
SELECT u.*, o.total
FROM users u
LEFT JOIN (
SELECT user_id, SUM(total) AS total FROM orders GROUP BY user_id
) o ON o.user_id = u.id;8. DISTINCT на большой выборке
-- сканирует + сортирует всё
SELECT DISTINCT user_id FROM events;
-- иногда быстрее через GROUP BY
SELECT user_id FROM events GROUP BY user_id;Или использовать approximate count distinct в DWH:
-- ClickHouse / BigQuery
SELECT uniq(user_id) FROM events; -- approximate9. ORDER BY без индекса
-- сортирует миллион строк
SELECT * FROM orders ORDER BY created_at DESC LIMIT 10;
-- индекс на created_at даст O(log n)
CREATE INDEX idx_orders_created_at ON orders(created_at);10. Устаревшая статистика
Планировщик использует статистику о данных. Если она старая — выбирает неоптимальный план.
-- Postgres
ANALYZE orders;
-- MySQL
ANALYZE TABLE orders;Как искать узкое место
1. Найти медленные запросы
- Postgres:
pg_stat_statements - MySQL: slow query log (
long_query_time)
2. EXPLAIN ANALYZE
Смотрите самый долгий node. Обычно это:
- Seq Scan на большой таблице
- Hash Join с плохим предсказанием
- Sort без индекса
3. Проверить индексы
-- Postgres
SELECT * FROM pg_indexes WHERE tablename = 'orders';
-- MySQL
SHOW INDEXES FROM orders;4. Размер таблиц
Иногда таблица просто ОГРОМНАЯ. Тогда:
- Партиционирование по дате
- Отдельный data warehouse (ClickHouse)
- Материализованные view
Правила оптимизации
1. Сначала замерь
Не оптимизируйте «на глаз». EXPLAIN + замер времени.
2. Индекс на WHERE и JOIN
Каждая колонка в условиях должна быть проиндексирована.
3. Избегать SELECT *
-- медленно, если много колонок
SELECT * FROM orders WHERE user_id = 42;
-- только нужные колонки
SELECT order_id, total, created_at FROM orders WHERE user_id = 42;Особенно важно для columnar-СУБД (ClickHouse, BigQuery).
4. Covering index
Если часто запрашиваете 3 колонки — создайте индекс, включающий все:
-- Postgres
CREATE INDEX idx ON orders(user_id) INCLUDE (total, created_at);Не придётся обращаться к основной таблице → быстрее.
5. Materialized views
Если агрегат считаете часто — предрасчитать:
CREATE MATERIALIZED VIEW monthly_revenue AS
SELECT DATE_TRUNC('month', created_at) AS month, SUM(total) AS rev
FROM orders
GROUP BY 1;
-- обновление
REFRESH MATERIALIZED VIEW monthly_revenue;Связанные темы
- EXPLAIN SQL — шпаргалка
- Индексы SQL — шпаргалка
- Оптимизация SQL — шпаргалка
- EXPLAIN ANALYZE — как читать
FAQ
Первое, что проверить при медленном запросе?
EXPLAIN ANALYZE. Смотрите Seq Scan на больших таблицах и отсутствующие индексы.
Всегда ли индекс ускоряет?
Нет. Для маленьких таблиц или при выборке >20% строк seq scan может быть быстрее.
Как узнать, что индекс не используется?
В плане: если видно Seq Scan, а на колонке есть индекс — значит, не использовался.
Можно ли ускорить JOIN на миллионе строк?
Индекс на JOIN-колонке. Если не помогает — партиционирование, predicate pushdown.
Тренируйте SQL-оптимизацию — откройте тренажёр с 1500+ вопросами для собесов.