Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Транзакции и скорость запросов

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 — они показывают список запросов страницы и время каждого. Обычно причина медленной страницы — не «тяжёлый» запрос, а сотня одинаковых лёгких.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.