← Назад к статьям
Производительность и базы данных

Дело не в том, что ваша база данных медленная — медленный ваш запрос: основы оптимизации MySQL в Laravel

Практическое руководство по оптимизации MySQL в Laravel: исправление N+1 запросов, правильные индексы, отказ от циклов и превращение страницы с 3 секундами в 150 мс без апгрейда железа.

Доступные языки
Дело не в том, что ваша база данных медленная — медленный ваш запрос: основы оптимизации MySQL в Laravel
Рекомендуемая статья ↗

Дело не в том, что ваша база данных медленная — медленный ваш запрос: основы оптимизации MySQL в Laravel, которые должен знать каждый разработчик

Каждый Laravel-разработчик через это проходил. Страница, которая загружалась за 200 мс, внезапно начинает загружаться за 3 секунды. Первый инстинкт — обвинить базу данных: «MySQL медленная», «нам нужен более мощный сервер», «давайте перейдём на PostgreSQL». Но вот неудобная правда — ваша база данных почти никогда не является проблемой. Проблема в ваших запросах.

MySQL — один из самых проверенных в бою программных продуктов в мире. Он справляется с миллионами строк и тысячами одновременных соединений без малейшего напряжения. Чего он не может — так это волшебным образом исправить плохо написанные запросы, отсутствующие индексы и пресловутую проблему N+1.

В этой статье мы разберём основные техники оптимизации MySQL, которые должен знать каждый Laravel-разработчик — те, что превращают страницу с загрузкой в 3 секунды в страницу с загрузкой за 150 мс без апгрейда ни одной детали железа.

1. Проблема N+1: тихий убийца Laravel

Это самая распространённая причина медленных Laravel-приложений, и она почти всегда невидима, пока вы специально не начнёте её искать.

Рассмотрим этот невинно выглядящий код:

<?php $posts = Post::all(); foreach ($posts as $post) { echo $post->author->name; }

Выглядит нормально, да? Кроме того, что это генерирует N+1 запросов. Один запрос для получения всех постов, затем один дополнительный запрос для каждого поста, чтобы получить его автора. При 100 постах это 101 запрос. При 1000 постах это 1001 запрос.

Решение — жадная загрузка (eager loading):

<?php $posts = Post::with('author')->get(); foreach ($posts as $post) { echo $post->author->name; }

Теперь это всего 2 запроса, независимо от того, сколько у вас постов.

Как обнаружить N+1 запросы

У Laravel есть отличные инструменты для этого:

  • Laravel Debugbar — показывает каждый запрос и подсвечивает дубликаты.
  • Laravel Telescope — полноценный компаньон для отладки с профилированием запросов.
  • DB::listen() — логируйте каждый запрос вручную.
<?php DB::listen(function ($query) { Log::info($query->sql, $query->bindings); });

Вы также можете включить строгий режим в разработке, чтобы выбрасывать исключения при ленивой загрузке:

<?php // В AppServiceProvider::boot() Model::preventLazyLoading(!app()->isProduction());

Это заставляет вас ловить проблемы N+1 во время разработки, а не в продакшене.

2. Индексы: самая большая победа

Если вы сделаете только одну вещь после прочтения этой статьи, пусть это будет следующая: добавьте индексы на ваши внешние ключи и часто запрашиваемые столбцы.

Laravel делает это простым с помощью миграций:

<?php Schema::table('posts', function (Blueprint $table) { $table->index('user_id'); $table->index('status'); $table->index(['status', 'created_at']); // составной индекс });

Запрос, который сканирует 1 000 000 строк, может сократиться до сканирования 100 строк с правильным индексом. Это ускорение в 10 000 раз от одной строки кода.

Как узнать, какие индексы вам нужны

Используйте EXPLAIN, чтобы увидеть, что делает MySQL:

<?php $posts = Post::where('status', 'published') ->where('user_id', 42) ->orderBy('created_at', 'desc') ->get(); // Посмотреть план выполнения DB::select('EXPLAIN ' . $posts->toSql(), $posts->getBindings());

Ищите эти тревожные признаки:

  • type: ALL — полное сканирование таблицы, индекс не используется.
  • rows: 500000 — MySQL ожидает сканирования полумиллиона строк.
  • Extra: Using filesort — сортировка в памяти или на диске вместо использования индекса.
  • Extra: Using temporary — создание временной таблицы, обычно для GROUP BY.

Решение? Добавьте индекс, соответствующий шаблону запроса — обычно столбцы в WHERE, затем столбцы в ORDER BY.

3. Выбирайте только то, что вам нужно

Ещё одна классическая ошибка:

<?php $users = User::all(); // получает каждый столбец для каждого пользователя

Если вам нужны только имена и email, скажите об этом:

<?php $users = User::select('id', 'name', 'email')->get();

Это важнее, чем вы думаете. Получение больших столбцов TEXT или JSON, которые вы не используете, может умножить как использование памяти, так и время запроса. А в Laravel каждая Eloquent-модель имеет накладные расходы — создание сотен полных моделей, когда вам нужно всего несколько полей, расточительно.

Для действительно данных только для чтения рассмотрите возможность полностью пропустить Eloquent:

<?php $stats = DB::table('orders') ->select('status', DB::raw('COUNT(*) as total')) ->groupBy('status') ->get();

Query Builder значительно быстрее Eloquent, когда вам не нужны модели.

4. Используйте chunk() и cursor() для больших наборов данных

Загрузка миллиона строк в память обрушит ваше приложение — даже если сам запрос быстрый. Laravel предоставляет два инструмента для потоковой обработки больших наборов данных:

<?php // chunk() — загружает пакетами User::chunk(1000, function ($users) { foreach ($users as $user) { // обработка } }); // cursor() — использует генератор, по одной строке за раз foreach (User::cursor() as $user) { // обработка }

cursor() использует PHP-генераторы и эффективен по памяти, но держит одно соединение с базой данных открытым на всю итерацию. chunk() выполняет несколько запросов, что часто безопаснее в продакшене.

5. Избегайте запросов в циклах

Если вы ловите себя на написании такого:

<?php foreach ($userIds as $id) { $user = User::find($id); // ... }

Остановитесь. Замените это одним запросом:

<?php $users = User::whereIn('id', $userIds)->get();

Это относится и к обновлениям, и к удалениям:

<?php // Плохо foreach ($userIds as $id) { User::where('id', $id)->update(['active' => false]); } // Хорошо User::whereIn('id', $userIds)->update(['active' => false]);

Вторая версия — это один запрос вместо сотен — часто разница между 500 мс и 5 мс.

6. Будьте осторожны с whereHas и withCount

whereHas() удобен, но может генерировать дорогие коррелированные подзапросы:

<?php $users = User::whereHas('orders', function ($q) { $q->where('total', '>', 1000); })->get();

Для простых проверок существования предпочитайте whereIn с подзапросом:

<?php $userIds = Order::where('total', '>', 1000) ->distinct() ->pluck('user_id'); $users = User::whereIn('id', $userIds)->get();

Аналогично, withCount() выполняет подзапрос для каждой связи. Это нормально для пары связей, но если вы считаете пять связей в одном запросе, рассмотрите реструктуризацию.

7. Кэшируйте агрессивно

Иногда самый быстрый запрос — тот, который вы не выполняете вовсе. Слой кэша Laravel — один из самых недоиспользуемых инструментов производительности в фреймворке.

<?php $topPosts = Cache::remember('top_posts', 3600, function () { return Post::orderByDesc('views')->limit(10)->get(); });

Для данных, которые редко меняются — категорий, настроек, статических списков — кэширование это по сути бесплатная производительность. Используйте Redis или Memcached в продакшене и не забывайте инвалидировать ключи при изменении данных.

8. Анализируйте медленные запросы в продакшене

У MySQL есть встроенный инструмент для поиска медленных запросов:

-- Включить лог медленных запросов SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 1; SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';

После дня в продакшене просмотрите лог. Вы найдёте настоящих виновников — запросы, о которых вы бы никогда не подумали, что они медленные, выполняющиеся тысячи раз в минуту.

В Laravel вы также можете логировать медленные запросы для каждого запроса через service provider:

<?php // В AppServiceProvider::boot() if (app()->isProduction()) { DB::listen(function ($query) { if ($query->time > 1000) { // мс Log::warning('Slow query', [ 'sql' => $query->sql, 'time' => $query->time, ]); } }); }

9. Используйте правильные типы данных

Мелкие детали важнее, чем вы думаете:

  • Используйте INT UNSIGNED для ID, а не VARCHAR.
  • Используйте DECIMAL для денег, никогда FLOAT.
  • Используйте ENUM или TINYINT для статусов, а не VARCHAR(255).
  • Используйте CHAR(36) для UUID, а не VARCHAR(255).
  • Предпочитайте DATETIME или TIMESTAMP строковым датам.

Меньшие столбцы означают меньшие индексы, что означает более быстрые поиски. Это накапливается.

10. Оптимизируйте связи и ограничения

Миграции Laravel делают простым определение внешних ключей:

<?php Schema::table('posts', function (Blueprint $table) { $table->foreignId('user_id')->constrained()->cascadeOnDelete(); });

Внешние ключи не только сохраняют целостность данных — они также автоматически создают индекс, что часто именно то, что вам нужно для производительности JOIN.

Практический рабочий процесс отладки

Когда страница медленная, следуйте этой последовательности:

  1. Установите Laravel Debugbar или Telescope. Посмотрите, сколько запросов выполняется.
  2. Ищите паттерны N+1. Добавьте with() везде, где связь доступна в цикле.
  3. Проверьте количество запросов. Страница обычно должна выполнять менее 20 запросов. Более 100 — красный флаг.
  4. Запустите EXPLAIN на самых медленных запросах. Ищите полные сканирования таблиц.
  5. Добавьте индексы. Сопоставьте индекс со столбцами, используемыми в WHERE и ORDER BY.
  6. Кэшируйте то, что не меняется. Особенно агрегаты и конфигурационные данные.
  7. Измерьте заново. Повторяйте, пока страница не станет быстрой.

Заключение

В девяти случаях из десяти «база данных медленная» на самом деле означает «запросы медленные». Прежде чем тянуться за более мощным сервером, другим движком базы данных или управляемым облачным сервисом, потратьте день на оптимизацию своих запросов. Вы почти всегда обнаружите, что победы уже там — ждут в коде.

Жадная загрузка, правильные индексы, избегание циклов и кэширование решат большинство проблем производительности в Laravel-приложениях. Ни одна из этих техник не требует новой инфраструктуры. Они требуют только осознанности.

Начните с самой медленной страницы. Запустите лог запросов. Исправьте N+1. Добавьте отсутствующий индекс. Измерьте снова. Затем повторите — и наблюдайте, как ваша база данных волшебным образом «становится быстрее», не меняя в ней ни единой вещи.

Технологии и темы

Теги статьи

Нет статей, соответствующих этим фильтрам.

Есть проект или идея для обсуждения?

Давайте обсудим ↗