Как оптимизировать SQL-запросы?
Оптимизация SQL-запросов — процесс улучшения производительности запросов, основанный на измерениях (EXPLAIN ANALYZE), а не на догадках. Ниже приведён практический чеклист.
Сводный чеклист
| Приём | Когда применять |
|---|---|
| Создать индекс | Медленный WHERE, JOIN, ORDER BY |
Убрать SELECT * |
Всегда |
| Устранить N+1 | Циклические запросы в коде |
| Keyset pagination | Глубокая пагинация (OFFSET > 1000) |
EXISTS вместо IN |
Подзапрос возвращает много строк |
Избегать функций в WHERE |
Функция на индексированном столбце |
| Пул соединений | Всегда |
EXPLAIN ANALYZE |
Любой медленный запрос |
| Денормализация | Чтение >> запись |
| Materialized View | Тяжёлые агрегатные запросы |
1. Используйте индексы с умом
Пример
-- Плохо: функция на индексированном столбце — индекс не используется
SELECT * FROM users WHERE UPPER(email) = 'USER@EXAMPLE.COM';
-- Хорошо: функциональный индекс
CREATE INDEX idx_users_email_upper ON users(UPPER(email));
-- Ещё лучше: хранить данные в нужном формате
SELECT * FROM users WHERE email = 'user@example.com';
2. Избегайте SELECT *
Пример
-- Плохо: читаются все столбцы, даже ненужные
SELECT * FROM orders WHERE status = 'pending';
-- Хорошо: только нужные столбцы (может использовать Index Only Scan)
SELECT id, amount, created_at FROM orders WHERE status = 'pending';
3. Устраните проблему N+1
Пример
// Плохо (N+1): один запрос на список + N запросов на детали
List<User> users = query("SELECT * FROM users");
for (User u : users) {
List<Order> orders = query("SELECT * FROM orders WHERE user_id = ?", u.id);
}
// Хорошо: один запрос с JOIN
query("SELECT u.*, o.* FROM users u LEFT JOIN orders o ON o.user_id = u.id");
4. Используйте keyset pagination вместо OFFSET
Пример
-- Медленно на глубоких страницах (СУБД читает и отбрасывает 10000 строк)
SELECT * FROM products ORDER BY id LIMIT 20 OFFSET 10000;
-- Быстро: keyset pagination (курсорная пагинация)
SELECT * FROM products WHERE id > 10020 ORDER BY id LIMIT 20;
5. Предпочитайте EXISTS вместо IN для подзапросов
Пример
-- EXISTS останавливается при первом совпадении
SELECT * FROM users u
WHERE EXISTS (
SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.amount > 1000
);
6. Не применяйте функции к индексированным столбцам
Пример
-- Плохо: индекс на created_at не используется
SELECT * FROM orders WHERE EXTRACT(YEAR FROM created_at) = 2026;
-- Хорошо: диапазонный запрос, индекс используется
SELECT * FROM orders
WHERE created_at >= '2026-01-01' AND created_at < '2027-01-01';
7. Используйте пул соединений
Создание TCP-соединения к PostgreSQL — дорогая операция (~100-200ms). Используйте пул: PgBouncer (внешний пулер), HikariCP (в Java-приложениях), или встроенный пул Spring Boot.
8. Материализованные представления
Пример с Materialized View
CREATE MATERIALIZED VIEW mv_daily_stats AS
SELECT
DATE(created_at) AS day,
COUNT(*) AS order_count,
SUM(amount) AS total_amount
FROM orders
GROUP BY DATE(created_at);
CREATE INDEX idx_mv_daily_stats_day ON mv_daily_stats(day);
-- Использование (мгновенный ответ)
SELECT * FROM mv_daily_stats WHERE day >= '2026-01-01';
-- Обновление данных (CONCURRENTLY — без блокировки чтения)
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_daily_stats;
9. Денормализация как компромисс
Денормализация ускоряет чтение за счёт хранения избыточных данных, но усложняет обновление и может привести к аномалиям. Используйте, когда чтение значительно преобладает над записью.
Важное
- Оптимизация должна быть основана на измерениях, а не на догадках
- Не оптимизируйте преждевременно — сначала убедитесь, что запрос является узким местом
- Регулярно запускайте
ANALYZEдля актуальной статистики pg_stat_statements— расширение для отслеживания самых медленных и частых запросов- При высокой нагрузке рассмотрите read replicas для разделения чтения и записи
Частые ошибки
- Создают индексы «на всякий случай» без проверки их использования
- Используют
OFFSETдля глубокой пагинации — крайне медленно - Кешируют в приложении то, что лучше кешировать на уровне СУБД (materialized views)
- Оптимизируют запрос на тестовых данных (100 строк), а на production (10 млн строк) план совершенно другой
На собеседовании: не перечисляйте все приёмы — опишите системный подход: измерить (
EXPLAIN ANALYZE) -> найти узкое место -> применить конкретный приём. Частая ошибка — начать с «добавить индекс» без понимания, где именно проблема.