Как ускорить медленный SQL-запрос
Карьерник — квиз-тренажёр в Telegram с 1500+ вопросами для собесов аналитика. SQL, Python, A/B, метрики. Бесплатно.
Зачем это знать
Запрос аналитика, который висит 10 минут — нормальное дело в startup. Но когда это dashboard для CEO или ETL на проде — нужно уметь ускорять. Без знания оптимизации middle-аналитик ограничен в scale.
На собесах middle+ часто спрашивают: «дан медленный запрос, как оптимизируешь?». Без пошагового ответа — сразу downgrade до junior.
Шаг 1: EXPLAIN
Всегда начинать с explain plan:
EXPLAIN ANALYZE SELECT ... ;Смотрите:
- Seq Scan → нет индекса на filter
- Nested Loop с большими rows → нужен hash join
- Sort с big cost → нужен индекс по ORDER BY
- Rows estimated vs actual → стата устарела,
ANALYZE table
Шаг 2: Индексы
Добавить индекс по WHERE
CREATE INDEX idx_orders_user_id ON orders(user_id);Композитный индекс
Для фильтра по 2 полям:
CREATE INDEX idx_orders_user_created ON orders(user_id, created_at);Порядок колонок важен (left-to-right).
Covering index
Если нужны только несколько полей — INCLUDE избыточный доступ к таблице:
CREATE INDEX idx_orders_covering
ON orders(user_id) INCLUDE (total, created_at);Шаг 3: Убрать function(col) из WHERE
-- Плохо (индекс не работает)
WHERE DATE(created_at) = '2026-04-22'
-- Хорошо
WHERE created_at >= '2026-04-22' AND created_at < '2026-04-23'Функция на колонке → индекс не используется.
Шаг 4: IN vs EXISTS vs JOIN
Большой подзапрос в IN → медленно. Замените на JOIN:
-- Медленно
SELECT * FROM users WHERE id IN (SELECT user_id FROM orders);
-- Быстрее
SELECT DISTINCT u.* FROM users u JOIN orders o ON u.id = o.user_id;
-- Ещё быстрее
SELECT * FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);Шаг 5: Избавиться от SELECT *
Вернуть только нужные колонки. Меньше IO → быстрее.
Шаг 6: Ограничить по времени
WHERE created_at >= NOW() - INTERVAL '30 days'Часто 30 дней данных достаточно. Проверьте — не нужна ли вся история.
Шаг 7: Материализация
Если запрос часто выполняется — материализуйте:
CREATE MATERIALIZED VIEW daily_users AS
SELECT DATE(created_at) AS day, COUNT(*) FROM users GROUP BY 1;Обновление по расписанию.
Шаг 8: Партиционирование
Для большой таблицы (> 100M rows) — разбить по времени:
CREATE TABLE orders_2026_04 PARTITION OF orders
FOR VALUES FROM ('2026-04-01') TO ('2026-05-01');Запрос по месяцу — скан только нужной partition.
Шаг 9: Проверить статистику
Если explain estimates врут:
ANALYZE orders; -- пересчитать статыПланировщик полагается на свежие статы.
Шаг 10: Rewrite JOINs
Переставьте JOIN-ы: сначала самые селективные фильтры, чтобы уменьшить intermediate result.
Что НЕ делать
Hints как первый инструмент
Force index — last resort. Сначала понять, почему планировщик не выбрал сам.
Over-indexing
Каждый индекс замедляет INSERT/UPDATE. Только нужные.
Убивать DISTINCT
DISTINCT часто нужен. Замените GROUP BY или EXISTS — если логически эквивалентно.
На собесе
«Запрос выполняется 5 минут, как ускоришь?».
Алгоритм: EXPLAIN → identify bottleneck → индекс / переписать / материализация. Озвучить 3-4 шага, не один.
Связанные темы
FAQ
Индексы всегда помогают?
Нет. На маленькой таблице Seq Scan быстрее.
CTE замедляет или ускоряет?
В Postgres 12+ CTE inline (ускоряет). До 12 — материализовались (может замедлять).
Когда использовать партиционирование?
100M rows или > 50GB таблица. Меньше — не нужно.
Тренируйте SQL — откройте тренажёр с 1500+ вопросами для собесов.