你知道吗?1/4的访客如果网站加载时间超过4秒就会放弃访问。虽然可能有多个因素会减慢网站速度,但一个常见因素是效率低下的数据库查询。
Eloquent是一个强大的工具,使得我们在Laravel应用程序中与数据库交互变得容易,但有时候我们忘记了看似方法调用或属性实际上是在内部执行数据库查询,这可能会导致页面加载缓慢或内存使用量过高。以下是一些可以提高Laravel应用程序加载速度的技术。
1. 只选择需要的列
我们优化查询的一种方式是减少从数据库中获取的数据量。当你进行查询时,数据库返回的数据会通过网络发送。数据越多,所需时间越长,但不仅如此,所有这些数据都必须在请求的生命周期中存储在内存中,这可能导致服务器在负载高峰时变慢甚至耗尽内存。
幸运的是,在Laravel中,我们可以指定确切的所需数据。例如,如果我们有一个电子商务网站,并且我们想显示产品列表,我们的控制器可能会像这样
public function index()
{
return view('products', [
'products' => Products::query()->paginate()
]);
}
虽然这样做看似合理(我们甚至对结果进行了分页!),但我们的产品表可能含有大量信息,而我们列表页不需要(正文、分类ID、产品类型等)。您可以只选择所需的列来减少数据量
public function index()
{
return view('products', [
'products' => Products::query()
->select(['id', 'title', 'slug', 'thumbnail'])
->paginate()
]);
}
这会使我们的查询更加高效,这对于产品的表中可能包含相当大的 body
列(文本类型)尤为重要。
这种技术在您需要处理大量记录的页面上效果最佳,所以如果您将其应用于只获取少数数据库记录的页面,可能不会看到内存使用/性能的大幅变化。不过,记住这一点仍然是个好习惯。
2. 注意 N+1 的问题
继续我们的电子商务示例,假设我们需要显示每种产品所属的品牌。让我们假设我们在 Product
模型中有一个品牌关系。
public function brand()
{
return $this->belongsTo(Brand::class);
}
如果我们需要在 blade 模板中访问品牌名称,它可能看起来像这样:
@foreach ($products as $product)
//...
<p>{{ $product->brand->name }}</p>
//...
@endforeach
这看起来没有问题。我过去写过很多这样的代码。然而,这里有一个小问题可能不太明显。虽然我们在控制器中通过一个 SQL 查询加载了所有产品到内存,但是品牌是在 for 循环中逐个从数据库中检索的。如果你的关系没有被加载,Laravel 将执行一个 SQL 查询从数据库中检索它。
这个问题被称为“n+1”,因为它执行 1
个 SQL 查询来获取我们的产品,然后执行 N
个 SQL 查询来获取品牌,其中 N
是产品的数量。
Laravel 提供了一种简单的方法来解决此问题,即使用预加载。预加载意味着我们在使用它们之前希望获取所有相关的模型。在底层,Laravel 将只执行一个 SQL 查询以检索每个相关品牌。所以,我们不是执行 N+1 查询,而是只执行 2 个。
要使用预加载,你需要在查询中调用 with
方法。
public function index()
{
return view('products', [
'products' => Products::query()
->select(['id', 'title', 'slug', 'thumbnail'])
->with('brand')
->paginate()
]);
}
我们甚至可以将之前的技巧与这个技巧结合起来,只选择关系的我们需要的列。我们可以通过添加 :
后跟我们要从关系中选择的列名来实现。
public function index()
{
return view('products', [
'products' => Products::query()
->select(['id', 'title', 'slug', 'thumbnail'])
->with('brand:id,name')
->paginate()
]);
}
这样,Laravel 将确保在预加载关系时只选择 id
和 name
列。
3. 如果您只需要一个记录,不要获取所有记录
假设在我们的店铺中,我们想显示所有用户并显示他们最新下订单的总额,我们可以在 blade 模板中这样做:
@foreach($users as $user)
<p>{{ $user->name }}</p>
<p>{{ $user->orders()->latest()->first()->total }}</p>
@endforeach
在这里,我们正在遍历所有用户,然后查询按 created_at
排序的订单(latest()
方法会处理这个问题),然后我们执行查询以获取第一个结果,即最新创建的订单,然后我们简单地在该模型上访问总额。
然而,你会意识到,这引入了一个 N+1 的问题。但我们知道如何使用预加载来修复它!但是,如果我们要将所有订单而不是最新的订单都存入内存,预加载可能不会按我们想要的做。
在 Laravel 中有几种方法可以解决这个问题。其中一种是在我们的 User
模型中定义一个 hasOne
关系,该关系只检索最新创建的订单。
function lastOrder()
{
return $this->hasOne(Order::class)->latestOfMany();
}
通过使用方法 latestOfMany
,我们告诉 Laravel 我们只需要最新创建的记录,如果你想获取最旧的记录,也可以使用 oldestOfMany
方法。
现在我们有一个新的关系,我们可以像任何其他关系一样预加载它。
public function index()
{
return view('users', [
'users' => Users::query()
->with('lastOrder')
->get();
]);
}
现在,在我们的模板中,我们可以通过访问加载的关系来获取总额。
@foreach($users as $user)
<p>{{ $user->name }}</p>
<p>{{ $user->lastOrder->total }}</p>
@endforeach
4. 使用索引
数据库索引如果深入研究其工作原理可能会相当复杂,然而您不需要深入了解它们就能有效地使用它们。简单来说,索引就是一种数据库在搜索记录时可以查询的查找表,使用索引可以使您的查找查询速度大大提高。
您可以在Laravel中通过在迁移中添加索引来使用索引。比如说,我们预期我们的店铺用户会经常按照名称搜索产品,我们可以在产品标题上添加一个索引查询。
Schema::table('products', function (Blueprint $table) {
$table->index('title');
});
通过创建这个迁移,我们可以加快我们表的速度。但是请注意,这个索引只有在用整个标题或标题的开头搜索产品时才会应用。换句话说,索引将应用于以下查询
Product::where('title', '=', $search);
Product::where('title', 'like', $search . '%');
但是,如果我们尝试使用如下类似查询进行匹配时,则不会应用该索引
Product::where('title', 'like', '%' . $search . '%');
这个问题可以使用全文索引来解决,这篇文章还涉及其他类型的索引,但这些不在本文的讨论范围内。
5. 优化循环关系
假设我们的产品模型如下所示
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)
<li>
<a href="{{ $product->url() }}">{{ $product->name }}</a>
</li>
@endforeach
这里的问题是,我们再次引入了N+1问题,当我们在产品中调用url
方法时,我们对每个产品都要查询一次分类,尽管我们已经有了它!解决这个问题的方法之一是在我们的产品中预加载分类。我们可以通过在load
方法调用中添加.category
来实现这一点
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]);
}
如果您想了解更多关于这个技术,可以阅读 Jonathan Reinink 写的这篇精彩的<竇a rel="nofollow noopener noreferrer" target="_blank" href="https://reinink.ca/articles/optimizing-circular-relationships-in-laravel">文章。
结论
这就是全部内容,我希望您至少从这个文章中学到了一些新知识,并觉得它很有价值,您可以将其用于加快您的网站速度。
如果您觉得这篇文章有价值,请确保在Twitter上关注我,我在那里发布提示和其他Laravel相关内容,您也可以向我提出有关此或其他Laravel主题的任何问题。
driesvints, naser-da, cosmeoes, elkdev 赞同这篇文章