Медленные SQL-запросы в Yii2: как искать причину
Когда страница на Yii2 открывается медленно, не всегда нужно сразу ставить кэш или увеличивать сервер. Часто причина находится в нескольких SQL-запросах: нет индекса, выборка тянет слишком много строк, в цикле запускается N+1 или сортировка идёт по тяжёлому выражению.
Правильный порядок здесь простой: сначала найти медленный запрос, потом понять, почему он медленный, и только после этого менять код или структуру таблицы.
Найти проблемную страницу и сценарий
Сначала стоит уточнить, что именно тормозит. “Каталог медленный” — слишком широкое описание. Нужно понять конкретный URL, параметры фильтра, количество данных и роль пользователя.
- одна страница или весь раздел;
- тормозит только с фильтром или всегда;
- есть ли разница между гостем и администратором;
- сколько строк выводится;
- что изменилось перед появлением проблемы.
После этого можно делать замер. Если включён Yii debug toolbar на тестовом окружении, там удобно посмотреть количество запросов и время каждого запроса.
Логирование SQL в Yii2
На рабочем сайте debug toolbar обычно выключен, поэтому можно временно включить логирование нужной категории на тестовом стенде или аккуратно в production на короткое время.
'log' => [
'targets' => [
[
'class' => 'yii\log\FileTarget',
'levels' => ['profile'],
'categories' => ['yii\db\Command::query'],
'logFile' => '@runtime/logs/sql.log',
],
],
],
Логи SQL быстро растут, поэтому такую настройку не стоит оставлять надолго. Для разовой диагностики достаточно повторить медленный сценарий и вернуть конфиг обратно.
N+1 в ActiveRecord
Одна из частых причин — запросы внутри цикла. Например, список товаров загружен одним запросом, а категория для каждого товара тянется отдельно.
$products = Product::find()
->where(['status' => 'A'])
->all();
foreach ($products as $product) {
echo $product->category->name;
}
Если товаров сто, можно получить сто дополнительных запросов. Лучше заранее загрузить связь:
$products = Product::find()
->with('category')
->where(['status' => 'A'])
->all();
Иногда нужен joinWith(), если по связанной таблице идёт фильтрация или сортировка.
$products = Product::find()
->joinWith('category')
->where(['products.status' => 'A'])
->andWhere(['categories.active' => 1])
->all();
EXPLAIN вместо догадок
Когда найден конкретный SQL, следующий шаг — EXPLAIN. Он показывает, как база собирается выполнять запрос: какие индексы использует, сколько строк просматривает, есть ли filesort и temporary.
EXPLAIN SELECT *
FROM products
WHERE status = 'A'
ORDER BY created_at DESC
LIMIT 50;
Если запрос просматривает сотни тысяч строк ради 50 результатов, стоит проверить индексы. Но индекс должен соответствовать реальному условию и сортировке.
CREATE INDEX idx_products_status_created_at
ON products (status, created_at);
Не нужно добавлять индексы на все поля подряд. Каждый индекс ускоряет часть чтения, но замедляет запись и занимает место.
Выбирать только нужные поля
ActiveRecord по умолчанию выбирает все колонки. Для больших таблиц или списков это может быть лишним, особенно если есть тяжёлые текстовые поля.
$rows = Product::find()
->select(['id', 'name', 'slug', 'price'])
->where(['status' => 'A'])
->asArray()
->all();
asArray() полезен, когда не нужны методы модели и связи как объекты. Это не универсальная оптимизация, но для простых списков снижает накладные расходы.
Пагинация и лимиты
Страница, которая выводит сразу тысячи строк, будет медленной даже с хорошими индексами. Для публичных списков и админок нужна пагинация или пакетная обработка.
$dataProvider = new ActiveDataProvider([
'query' => Product::find()->where(['status' => 'A']),
'pagination' => [
'pageSize' => 50,
],
]);
Для консольной обработки больших таблиц лучше использовать batch() или each(), а не all().
foreach (Product::find()->batch(500) as $products) {
foreach ($products as $product) {
// обработка
}
}
Проверить результат после правки
После изменения запроса или индекса нужно повторить замер. Важно сравнивать одинаковый сценарий: тот же URL, те же параметры, примерно тот же объём данных.
- Зафиксировать исходное время страницы.
- Найти самый медленный SQL.
- Проверить EXPLAIN.
- Исправить запрос, связь или индекс.
- Снова проверить время и количество запросов.
- Посмотреть, не ухудшилась ли запись данных.
Медленные SQL-запросы редко исправляются одним универсальным приёмом. Иногда помогает индекс, иногда eager loading, иногда пересмотр фильтра или простое ограничение количества строк. Главное — не оптимизировать вслепую.
Комментарии (0)
Пока нет комментариев. Будьте первым!