Транзакции и скорость запросов
Eloquent удобен, но скрывает запросы: одна строка шаблона может выполнить сотню обращений к базе. Этот урок — о том, как замечать такие места, обрабатывать большие таблицы и выполнять несколько запросов как одно целое.
Проблема N+1
$posts = Post::all();
foreach ($posts as $post) {
echo $post->author->name; // запрос на каждую статью
}Сто статей — сто один запрос. Связь загружают заранее:
$posts = Post::with('author')->get(); // два запросаЗапретить ленивую загрузку
Чтобы такие места не оставались незамеченными, включите проверку — при разработке ленивая загрузка будет вызывать ошибку:
use Illuminate\Database\Eloquent\Model;
public function boot(): void
{
Model::preventLazyLoading(!app()->isProduction());
}Подсчёт связанных записей
Чтобы показать число комментариев, сами комментарии загружать не нужно:
$posts = Post::withCount('comments')->get();
foreach ($posts as $post) {
echo $post->comments_count;
}Считает база, а не PHP
Post::all()->count(); // загружает все записи в память
Post::count(); // один запрос SELECT COUNT(*)
Post::where('user_id', 5)->exists(); // вместо count() > 0
Post::sum('views');
Post::max('created_at');Только нужные столбцы
$titles = Post::select('id', 'title')->get();
$emails = User::pluck('email');Длинный текст статьи не нужен в списке заголовков.
Большие таблицы
Post::all() для миллиона записей исчерпает память. Записи обрабатывают частями:
User::chunkById(500, function ($users) {
foreach ($users as $user) {
// обработка
}
});
foreach (User::lazy() as $user) {
// по одной модели, запросы идут порциями
}Транзакции
Несколько запросов, которые должны выполниться вместе или не выполниться вовсе, оборачивают в транзакцию:
use Illuminate\Support\Facades\DB;
DB::transaction(function () use ($from, $to, $amount) {
$from->decrement('balance', $amount);
$to->increment('balance', $amount);
Transfer::create(['from_id' => $from->id, 'to_id' => $to->id, 'amount' => $amount]);
});Если внутри возникнет исключение, все изменения отменятся.
Блокировка строки
Два одновременных списания могут прочитать один и тот же баланс. lockForUpdate() заставляет второй запрос подождать:
DB::transaction(function () use ($id, $amount) {
$account = Account::lockForUpdate()->find($id);
if ($account->balance < $amount) {
throw new InsufficientFunds();
}
$account->decrement('balance', $amount);
});Индексы
Столбцы, по которым ищут и сортируют, должны иметь индекс:
$table->index('user_id');
$table->index(['status', 'created_at']);Метод foreignId()->constrained() создаёт индекс сам.
Частая ошибка
Оптимизировать наугад бесполезно. Сначала посмотрите, какие запросы выполняются: установите Laravel Debugbar или Telescope — они показывают список запросов страницы и время каждого. Обычно причина медленной страницы — не «тяжёлый» запрос, а сотня одинаковых лёгких.