
Знаете ли вы, что каждый четвертый посетитель не желает ждать и покидает сайт, если его загрузка занимает более 4 секунд? Хотя на скорость работы сайта влияет много факторов, одним из самых распространённых являются неэффективные запросы к базе данных.
Eloquent Laravel - отличный и удобный инструмент, который упрощает взаимодействие с базой данных в наших приложениях Laravel, но иногда мы забываем, что то что не первый взгляд выглядит как обычный вызов метода или свойства, под капотом запускает запрос к базе данных, и может привести к медленной загрузке страницы или высокому, нерациональному использованию памяти. Здесь я приведу несколько способов, которые могут избежать многих проблем и улучшить время загрузки вашего приложения на Laravel.
Выберите только те столбцы базы данных, которые вам нужны
Один из способов оптимизировать запрос - уменьшить объем извлекаемых из базы данных. Когда мы выполняем запрос, данные, возвращаемые базой данных, предаются по сети. Чем больше объем данных, тем больше времени это занимает. Но это еще не все, все эти данные должны обрабатываться и храниться в памяти в течение всего срока действия запроса, и это может привести к замедлению работы нашего сервера и при больших нагрузках нехватке памяти.
К счастью, в Laravel мы можем выбрать и получить только те данные, которые нам необходимы. Предположим, что у нас есть сайт интернет-магазина, и мы хотим отобразить список товаров. Наш контроллер будет выглядеть примерно так:
public function index()
{
return view('products', [
'products' => Products::query()->paginate()
]);
}
На первый взгляд это может показаться разумным, но в таблице наших продуктов может быть много информации, которая не будет отображаться в списке товаров и в данном случае нам ненужна. К примеру: детальное описание товара, идентификатор и название категории, дополнительные изображения и характеристики товаров, атак же много другой не используемой на странице списка товаров информации. Но используя метод select(), мы можем уменьшить объем данных, выбрав только те столбцы, поля базы данных, которые нам нужны:
2 код
public function index()
{
return view('products', [
'products' => Products::query()
->select(['id', 'title', 'slug', 'thumbnail'])
->paginate()
]);
}
Это сделает наш запрос более эффективным. Особенно это важно, поскольку таблица продуктов предположительно будет содержать столбец с детальным описанием товара типа TEXT, который может быть довольно большим.
Этот метод наиболее эффективен на страницах, где нам нужно работать с большим количеством записей базы данных, к примеру списка товаров, новостей, постов блога и т.д.
Избегайте проблем N+1 запросов к базе данных
Продолжая наш пример с интернет-магазином, предположим, что нам нужно отобразить бренд товара, которому принадлежит каждый продукт. Представим, что в нашей базе данных таблицы ‘products’ и ‘brands’ связаны, имеют отношения:
public function brand()
{
return $this->belongsTo(Brand::class);
}
Если нам нужно получить доступ к названию бренда в нашем blade шаблоне, это будет выглядеть примерно так:
@foreach ($products as $product)
//...
{{ $product->brand->name }}
//...
@endforeach
Опять же, на первый взгляд это выглядит нормально. И многие разработчики именно так и делают. Однако здесь есть небольшая проблема, которая для таких разработчиков не очевидна. Мы получаем все наши продукты, используя всего один SQL-запрос в нашем контроллере, но бренды извлекаются из базы данных один за другим в нашем @foreach – цикле, то есть на каждой итерации цикла осуществляется еще один запрос к базе данных. Если отношение не загружено с первым, основным запросом, Laravel выполнит SQL-запрос для извлечения его из базы данных.
Эта проблема называется "n + 1", и называется она так потому, что мы выполняем один SQL-запрос для получения нашего продукта, а затем n количество SQL-запросов для получения брендов, в нашем случае n – соответствует количеству продуктов.
Laravel предлагает простой способ устранить эту проблему с помощью жадной загрузки. Жадная загрузка (Eager load) означает, что мы получим все связанные модели одновременно с первым запросом. Под капотом Laravel выполнит только один SQL-запрос для получения каждого связанного бренда. Таким образом, вместо N + 1 запросов, будет сделано только два обращения к базе данных.
Чтобы использовать жадную загрузку, в построении запроса достаточно использовать метод with().
public function index()
{
return view('products', [
'products' => Products::query()
->select(['id', 'title', 'slug', 'thumbnail'])
->with('brand')
->paginate()
]);
}
Конечно, полный набор данных связанных моделей так же может быть избыточным, так как в нашем примере нам достаточно получить только название бренда и его ID.
В таком случае нам следует объединить предыдущий способ и этот, выбрав только нужные нам столбцы из наших отношений. Мы можем сделать это следующим образом:
public function index()
{
return view('products', [
'products' => Products::query()
->select(['id', 'title', 'slug', 'thumbnail'])
->with('brand:id,name')
->paginate()
]);
}
Таким образом, Laravel позаботится о том, чтобы выбрать только поля id и name при получении данных.
Не извлекайте все записи, если вам нужна только одна
Допустим, нам необходимо отобразить всех пользователей нашего магазина и показать общее количество последних сделанных ими заказов. Мы могли бы сделать это в нашем blade - шаблоне.
@foreach($users as $user)
{{ $user->name }}
{{ $user->orders()->latest()->first()->total }}
@endforeach
Тут мы просто перебираем всех наших пользователей, затем делаем запрос для получения заказов, сортируем по времени: created_at (это выполняется методом latest()), а затем выполняем запрос, извлекающий только первый результат, то есть последний созданный заказ, и только потом получаем доступ к итогу по этой модели.
Однако, возможно вы заметили, что это так же создает проблему N + 1. Но мы уже знаем, как это исправить с помощью жадной загрузки! Вот только в данном случае, обычная жадная загрузка методом with не будет лучшим решением, так как нам необходимо извлекать не все заказы, а только самый последний.
В Laravel есть несколько способов исправить это. Один из них заключается в определении hasOne отношения в нашей User модели, которое извлекает только один, последний созданный заказ:
function lastOrder()
{
return $this->hasOne(Order::class)->latestOfMany();
}
Методом latestOfMany, мы получаем только самую последнюю созданную запись. Также, мы можем использовать этот oldestOfMany метод, если вам нужна самая старая запись.
Теперь, когда у нас созданы новые отношения, мы можем получать только необходимые данные, как и при любых других отношениях.
public function index()
{
return view('users', [
'users' => Users::query()
->with('lastOrder')
->get();
]);
}
Проблема решена, и теперь в шаблоне, с помощью созданных отношений мы можем получить только необходимые данные.
@foreach($users as $user)
{{ $user->name }}
{{ $user->lastOrder->total }}
@endforeach
Используйте индексы
Дать полное понимание индексов базы данных и принципов их работы в рамках данной статьи сложно, и если вы хотите знать как они работают, Вам следует изучит это самостоятельно. Однако, в данном случае, чтобы их использовать, нам не нужно знать о них слишком много.
Проще говоря, индексы - это своего рода справочная информация, к которой база данных может обращаться при поиске записи, а их использование может значительно ускорить поисковые запросы.
Мы можем использовать индексы в Laravel, добавив их в свои миграции. Допустим, нам необходимо сделать поиск по сайту, и мы ожидаем, что пользователи нашего магазина будут искать товары по их названию - в таком случае, нам следует добавить индексы к названию товаров:
Schema::table('products', function (Blueprint $table) {
$table->index('title');
});
Создав эту миграцию, мы можем значительно ускорить поиск по полю ‘title’ нашей таблицы. Однако стоит иметь в виду, что этот индекс будет применяться только в том случае, если в запросе используется полное название или его начало. Другими словами, индекс будет применен, если мы будем выполнять запросы, подобные этому:
Product::where('title', '=', $search);
Product::where('title', 'like', $search . '%');
Но это не сработает, если мы попытаемся сопоставить, используя подобный запрос, подобный этому:
Product::where('title', 'like', '%' . $search . '%');
Эта проблема решается с помощью полнотекстового поиска, но эта уже тема другой статьи.
Оптимизация циклических взаимосвязей
Предположим, что наша модель продукта выглядит следующим образом,
class Product extends Model
{
public function category()
{
return $this->belongsTo(Category::class);
}
public function url()
{
return URL::route('product', [
'category' => $this->category->slug,
'product' => $this->slug,
]);
}
}
В этой модели у нас определена функция, которая использует связанный slug категории для генерации URL-адреса, простой, но полезный метод в нашей модели. Однако давайте предположим, что мы хотим показать все товары для данной категории, тогда наш контроллер будет выглядеть примерно так:
public function show(Category $category)
{
$category->load('products'); // eager load the products
return view('categories.show', ['category' => $category]);
}
Теперь предположим, что мы хотим показать URL-адрес продукта в нашем представлении, вызвав url метод, который мы определили в модели продукта:
@foreach($category->products as $product)
{{ $product->name }}
@endforeach
И как видите, мы снова получили проблему N + 1. В данном случае, когда мы вызываем url метод, мы делаем запрос для получения данных категории каждого продукта, даже если у нас это уже есть эти данные! Один из способов решения этой проблемы – загрузка данных категории в наших продуктах. Мы можем это сделать, добавив .category в при вызове load метода:
public function show(Category $category)
{
$category->load('products.category'); // eager load the products
return view('categories.show', ['category' => $category]);
}
Теперь проблема N + 1 решена, однако решение все еще не идеально, так как мы выполняем два SQL-запроса для получения одной и той же категории, один раз, когда модель вводится в show метод в контроллере и другой, когда мы вызываем load метод для получения данных этой же категории. К счастью, есть способ избежать этого, назначив связь напрямую, используя setRelation способ.
public function show(Category $category)
{
$category->products->each->setRelation('category', $category);
return view('categories.show', ['category' => $category]);
}
Заключение
Все, я надеюсь, вы узнали сто-то новое и полезное в этой статье, что сможете использовать чтобы избежать проблемы N + 1 запросов, оптимизировать и ускорить работу сайта на Laravel.
Комментарии
Гость
Регистрация Войти