Медленные 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, те же параметры, примерно тот же объём данных.

  1. Зафиксировать исходное время страницы.
  2. Найти самый медленный SQL.
  3. Проверить EXPLAIN.
  4. Исправить запрос, связь или индекс.
  5. Снова проверить время и количество запросов.
  6. Посмотреть, не ухудшилась ли запись данных.

Медленные SQL-запросы редко исправляются одним универсальным приёмом. Иногда помогает индекс, иногда eager loading, иногда пересмотр фильтра или простое ограничение количества строк. Главное — не оптимизировать вслепую.

Комментарии (0)

Пока нет комментариев. Будьте первым!