Как ускорить 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 с задачами из реальных интервью.
Что проверять при медленном запросе
- Run
EXPLAIN ANALYZE. - Есть ли Seq Scan на большой таблице?
- Index Scan vs Index Only Scan?
- Сортировка в памяти или на диске?
- Estimated rows ≈ actual rows?
- Nested Loop vs Hash Join?
Порядок оптимизации
- Индексы (быстро, большой эффект).
- Переписать запрос (фильтр до JOIN, оконные вместо subquery).
- Materialized view (если часто выполняется).
- Partitioning (для очень больших).
- 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 строк и запросы часто фильтруют по одной колонке (обычно дата).