Как ускорить медленный 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+ вопросами для собесов.