Проблема N+1 и EXPLAIN QUERY PLAN
Проблема N+1 запросов и идея EXPLAIN QUERY PLAN — как понять, что делает СУБД.
N+1 — антипаттерн, когда вместо одного запроса с JOIN приложение делает 1 запрос за списком и ещё N запросов по одному на каждый элемент.
Откуда берётся N+1
Типичный сценарий в коде с ORM: получили список из N пользователей (1 запрос), а потом в цикле для каждого тянем его заказы (ещё N запросов). Итого N+1 обращений к базе вместо одного. На 1000 пользователей это 1001 запрос — медленно из-за сетевых задержек на каждый.
# Псевдокод приложения — так делать НЕ надо:
users = db.query("SELECT * FROM users") # 1 запрос
for u in users:
# по запросу на каждого пользователя — это и есть +N
orders = db.query("SELECT * FROM orders WHERE user_id = ?", u.id)
Лечение — заменить цикл одним запросом с JOIN (или загрузить все заказы одним WHERE user_id IN (...) и сгруппировать в коде). В ORM это называют «eager loading» / JOIN FETCH.
CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);
INSERT INTO users (name) VALUES ('Аня'), ('Борис');
CREATE TABLE orders (id INTEGER PRIMARY KEY, user_id INTEGER, amount INTEGER);
INSERT INTO orders (user_id, amount) VALUES (1,500), (1,300), (2,900);
-- Один запрос вместо N+1: сразу пользователи с их заказами
SELECT u.name, o.amount
FROM users u
JOIN orders o ON o.user_id = u.id
ORDER BY u.name, o.amount;
Вывод:
Аня|300 Аня|500 Борис|900
Один JOIN вернул все нужные данные за одно обращение к базе. Это и есть решение N+1: переносим работу из цикла приложения в один SQL-запрос.
EXPLAIN QUERY PLAN: что делает СУБД
Чтобы понять, использует ли запрос индекс или сканирует всю таблицу, есть команда EXPLAIN (в SQLite — EXPLAIN QUERY PLAN). Она не выполняет запрос, а показывает план: как СУБД собирается его исполнять.
Например, до индекса план показывает SCAN TABLE users (полный скан), а после CREATE INDEX — SEARCH TABLE users USING INDEX (поиск по индексу). Конкретный текст плана зависит от версии СУБД, поэтому показываем его как иллюстрацию, без запуска:
EXPLAIN QUERY PLAN
SELECT email FROM users WHERE city = 'Москва';
-- Без индекса:
-- SCAN users
-- После CREATE INDEX idx_users_city ON users(city):
-- SEARCH users USING INDEX idx_users_city (city=?)
Ключевые слова в плане: SCAN — читается вся таблица (часто плохо на больших данных), SEARCH ... USING INDEX — точечный поиск по индексу (хорошо). Анализ плана — главный инструмент оптимизации: сначала смотрим план, потом решаем, какой индекс добавить.
Итог
- N+1 — это 1 запрос за списком плюс по запросу на каждый элемент; лечится одним
JOINилиIN. EXPLAIN QUERY PLANпоказывает план без выполнения запроса.SCAN= полный проход (часто медленно),SEARCH ... USING INDEX= быстрый поиск по индексу.