Yii2 ActiveRecord делает слишком много SQL-запросов: как найти N+1
N+1 в Yii2 появляется, когда приложение сначала получает список моделей, а потом в цикле лениво загружает связь для каждой строки. На небольших данных это почти незаметно. На каталоге, заказах или списке пользователей страница начинает делать десятки или сотни SQL-запросов.
Проблема не в ActiveRecord как таковом, а в неявной загрузке связей. Код выглядит аккуратно, но под капотом выполняет лишнюю работу.
Типичный пример N+1
Есть список заказов и связь customer. В шаблоне выводится имя клиента.
$orders = Order::find()
->where(['status' => Order::STATUS_NEW])
->all();
foreach ($orders as $order) {
echo $order->customer->name;
}
Первый запрос получает заказы. Затем для каждого заказа Yii2 отдельно загружает customer. Если заказов 100, получится 101 запрос.
Как увидеть проблему
Самый простой способ — включить Yii debug toolbar на тестовой копии и посмотреть количество SQL-запросов. Для production лучше использовать логирование SQL точечно, потому что полный лог запросов может быть слишком тяжёлым.
Признаки:
- много одинаковых SELECT по одной таблице;
- запросы отличаются только id;
- страница медленная при большом списке;
- в шаблоне много обращений к связям;
- при пагинации на 20 строк запросов мало, на 500 — слишком много.
Использовать with()
Если связь нужна для вывода, её лучше загрузить заранее.
$orders = Order::find()
->with('customer')
->where(['status' => Order::STATUS_NEW])
->all();
foreach ($orders as $order) {
echo $order->customer ? $order->customer->name : '';
}
Yii2 выполнит отдельный запрос для связанных клиентов, но не по одному запросу на каждый заказ. Это обычно уже резко снижает нагрузку.
with() и joinWith()
with() загружает связь отдельным запросом. joinWith() добавляет JOIN к основному запросу и полезен, когда по связанной таблице нужно фильтровать или сортировать.
$orders = Order::find()
->joinWith('customer')
->where(['customer.status' => Customer::STATUS_ACTIVE])
->orderBy(['customer.name' => SORT_ASC])
->all();
Не нужно заменять все with на joinWith. JOIN может утяжелить запрос, особенно при множественных связях. Выбирать нужно под конкретную задачу.
Ограничивать select()
Если для списка нужны только несколько полей, не обязательно тянуть всю модель со всеми колонками. Но важно не забыть поля, которые нужны для связей.
$orders = Order::find()
->select(['id', 'customer_id', 'status', 'created_at'])
->with([
'customer' => function ($query) {
$query->select(['id', 'name']);
},
])
->all();
Если убрать customer_id из select, связь может не сопоставиться корректно. Поэтому select нужно применять внимательно.
asArray()
Для отчётов и выгрузок, где не нужна логика ActiveRecord, можно использовать asArray(). Это снижает накладные расходы на создание объектов.
$rows = Order::find()
->select(['id', 'customer_id', 'status'])
->with('customer')
->asArray()
->all();
Но asArray меняет формат результата. Если дальше код ожидает методы модели, такой переход сломает обработку.
Проверить шаблон
N+1 часто находится не в контроллере, а в view. Например, в таблице заказов выводятся товары, клиент, менеджер, статус, склад и доставка. Каждое обращение может запускать связь.
// потенциально опасно в цикле
$order->items
$order->customer
$order->manager
$order->delivery
Перед оптимизацией списка нужно выписать все связи, которые используются в шаблоне, и загрузить их заранее.
Не загружать лишнее
Противоположная ошибка — подключить слишком много связей “на всякий случай”. Это может сделать запрос ещё тяжелее. Для каждой страницы нужно загружать только то, что реально выводится.
Чек-лист
- Посмотреть количество SQL-запросов на странице.
- Найти повторяющиеся SELECT по одной таблице.
- Проверить обращения к связям в циклах.
- Добавить with() для связей, которые нужны для вывода.
- Использовать joinWith() только при фильтрации или сортировке по связи.
- Ограничить select(), не забывая ключи связей.
- Для отчётов рассмотреть asArray().
- Повторно замерить число запросов и время страницы.
N+1 — одна из самых частых причин медленных списков в Yii2. Хорошая диагностика начинается с количества SQL-запросов, а исправление обычно сводится к явной загрузке нужных связей и отказу от лишней ленивой магии в шаблоне.
Комментарии (0)
Пока нет комментариев. Будьте первым!