Как ускорить SQL-запрос

1. EXPLAIN ANALYZE

Любая оптимизация — с диагностики:

EXPLAIN ANALYZE SELECT ... ;

Ищите в плане:

  • Seq Scan на большой таблице → нужен индекс.
  • Sort Method: external merge → сортировка пошла на диск.
  • Nested Loop на N×N → хэш-джоин лучше.

Подробнее.

2. Добавить индекс

Стандартный случай:

-- Было: Seq Scan
SELECT * FROM orders WHERE user_id = 123;

-- Решение
CREATE INDEX idx_orders_user ON orders(user_id);

Ускорение: 100–10000x.

3. Использовать LIMIT

-- Без LIMIT читает и сортирует всю таблицу
SELECT * FROM orders ORDER BY created_at DESC;

-- С LIMIT + индексом на created_at — мгновенно
SELECT * FROM orders ORDER BY created_at DESC LIMIT 100;

4. Избегать функций на индексированных столбцах

-- ❌ Индекс на email не используется
WHERE LOWER(email) = 'ivan@x.com'

-- ✅ Либо без функции
WHERE email = 'ivan@x.com'

-- ✅ Либо функциональный индекс
CREATE INDEX ON users (LOWER(email));

5. Заменить коррелированный подзапрос на JOIN / оконную

-- Медленно
SELECT o.*,
    (SELECT COUNT(*) FROM orders o2 WHERE o2.user_id = o.user_id) AS user_orders
FROM orders o;

-- Быстро
SELECT o.*,
    COUNT(*) OVER (PARTITION BY user_id) AS user_orders
FROM orders o;

Прокачать тему на реальных задачах удобно в боте @kariernik_bot — база вопросов собрана с собеседований в Яндексе, Авито, Ozon, Тинькофф.

6. Фильтр до JOIN / GROUP BY

-- Медленно: JOIN всех, потом фильтр
SELECT * FROM orders o
JOIN users u ON u.user_id = o.user_id
WHERE u.country = 'RU';

-- Быстрее: фильтр в CTE
WITH ru_users AS (
    SELECT * FROM users WHERE country = 'RU'
)
SELECT * FROM orders JOIN ru_users USING (user_id);

7. Partial index для узких фильтров

Если 95% строк имеют status='paid', а ищем status='refunded':

CREATE INDEX idx_refund_orders ON orders(user_id) WHERE status = 'refunded';

Индекс маленький, запрос быстрый.

8. Composite index для AND-фильтров

-- Два отдельных индекса неэффективны
CREATE INDEX idx_a ON orders(user_id);
CREATE INDEX idx_b ON orders(created_at);

-- Composite покрывает WHERE user_id=1 AND created_at>'2026-01-01'
CREATE INDEX idx_user_date ON orders(user_id, created_at);

Порядок: чаще используемый столбец — слева.

9. Materialized View для тяжёлых агрегатов

Если запрос тяжёлый, но меняется редко:

CREATE MATERIALIZED VIEW daily_gmv AS
SELECT created_at::DATE AS day, SUM(amount) AS gmv
FROM orders GROUP BY 1;

-- Обновлять ночью
REFRESH MATERIALIZED VIEW CONCURRENTLY daily_gmv;

Дашборд теперь читает agg-таблицу → мгновенно.

10. Partitioning таблицы

Для очень больших таблиц (100M+):

CREATE TABLE orders (
    id SERIAL, created_at TIMESTAMP, amount NUMERIC
) PARTITION BY RANGE (created_at);

CREATE TABLE orders_2026_q1 PARTITION OF orders
FOR VALUES FROM ('2026-01-01') TO ('2026-04-01');

Запросы с фильтром по дате читают только нужные партиции.

Дополнительно

Избегать SELECT *

-- Читает все столбцы, включая TEXT/JSON
SELECT * FROM big_table;

-- Только нужные
SELECT id, name FROM big_table;

OR → UNION ALL

-- OR мешает оптимизатору
WHERE a = 1 OR b = 2

-- Быстрее
SELECT * FROM t WHERE a = 1
UNION ALL
SELECT * FROM t WHERE b = 2 AND NOT (a = 1);

NOT IN → NOT EXISTS

-- NOT IN с NULL глючит + медленно
WHERE id NOT IN (SELECT x FROM other)

-- NOT EXISTS безопаснее и обычно быстрее
WHERE NOT EXISTS (SELECT 1 FROM other WHERE other.x = id)

VACUUM / ANALYZE

Если статистика устарела — оптимизатор делает плохие планы:

ANALYZE orders;       -- обновить статистику
VACUUM orders;         -- физическая очистка

На собесе такие штуки часто спрашивают. Быстрый способ довести до автоматизма — тренажёр в Telegram с задачами из реальных интервью.

Что проверять при медленном запросе

  1. Run EXPLAIN ANALYZE.
  2. Есть ли Seq Scan на большой таблице?
  3. Index Scan vs Index Only Scan?
  4. Сортировка в памяти или на диске?
  5. Estimated rows ≈ actual rows?
  6. Nested Loop vs Hash Join?

Порядок оптимизации

  1. Индексы (быстро, большой эффект).
  2. Переписать запрос (фильтр до JOIN, оконные вместо subquery).
  3. Materialized view (если часто выполняется).
  4. Partitioning (для очень больших).
  5. Schema changes (только в крайнем случае).

Типичные ошибки

1. Оптимизировать без замера

«Кажется, будет быстрее» — не работает. Замеряйте.

2. Создавать слишком много индексов

Каждый индекс замедляет INSERT. 5+ индексов на таблицу — проверяйте, все ли нужны.

3. Игнорировать partitions

На OLAP (ClickHouse) — partition key критичен.

Читайте также

FAQ

EXPLAIN или EXPLAIN ANALYZE?

ANALYZE реально запускает запрос и даёт real timings. Для SELECT — используйте ANALYZE. Для DML — осторожно.

Сколько должен работать запрос?

Для дашборда — до 2–5 секунд. Для ETL — минуты-часы OK.

CTE замедляют запрос?

В PostgreSQL ≥13 — обычно нет. В более старых — CTE всегда материализовалось, что могло замедлять.

Когда partitioning нужен?

Когда таблица >100M строк и запросы часто фильтруют по одной колонке (обычно дата).