18条优化Laravel 数据库查询的建议(1-3)
如果你的应用运行缓慢,使用了很多数据库查询,以下的性能优化秘诀可以改进你应用的加载时间。
1. 查询大量数据集
这一条主要聚焦于处理大量数据集的时候改进应用的内存使用情况。
如果你的应用需要处理大量的记录集,你可以把他们分成不同的子集查询、分组处理,而不是一次性查询所有数据。
从 posts 表中查询很多结果,通常我们的做法是:
$posts = Post::all(); // when using eloquent
$posts = DB::table('posts')->get(); // when using query builder
foreach ($posts as $post){
// Process posts
}
以上的例子查询和处理 posts 中中的所有记录,万一表中有 100 万条数据呢?内存很快就会用光。
为了避免大量数据集处理的问题,我们可以按照以下方式查询子集、处理结果。
选项1:使用 chunk
// when using eloquent
$posts = Post::chunk(100, function($posts){
foreach ($posts as $post){
// Process posts
}
});
// when using query builder
$posts = DB::table('posts')->chunk(100, function ($posts){
foreach ($posts as $post){
// Process posts
}
});
以上示例先从 posts 表中查询处理 100 条记录,然后再查询另外 100 条,再处理他们。如此往复迭代,直到所有数据都处理完。
此方式会创建更多的数据库查询,但也会更有效率地使用内存。通常,大量数据的处理应该放在后台。因此,当处理大量数据时,在后台使用更多的查询以避免内存耗尽。
选项 2:使用 cursor
// when using eloquent
foreach (Post::cursor() as $post){
// Process a single post
}
// when using query builder
foreach (DB::table('posts')->cursor() as $post){
// Process a single post
}
以上示例会发起单条数据库查询,从表中查询所有记录,一条条进入 Eloquent 模型。该方法只会发起一条数据库查询来查询所有记录。但是,使用 PHP 生成器可以优化内存使用。
什么时候使用这个?
尽管这会在应用级别上极大优化了内存使用,由于查询了表中的所有记录,数据库中的内存使用依然很高。
如果你的应用拥有的内存较少,数据库实例有较多的内存时,可以使用 cursor。如果你的数据库没有足够的内存,最好还是使用 chunks。
选项3:使用 chunkById
// when using eloquent
$posts = Post::chunkById(100, function($posts){
foreach ($posts as $post){
// Process posts
}
});
// when using query builder
$posts = DB::table('posts')->chunkById(100, function ($posts){
foreach ($posts as $post){
// Process posts
}
});
chunk 和 chunkById 最大的不同在于 chunk 查询是基于 offset 和 limit,而 chunkById 查询数据库基于 id 字段。Id 字段通常是整型,大部分情况下是自增的。
chunk
select * from posts offset 0 limit 100
select * from posts offset 101 limit 100
chunkById
select * from posts order by id asc limit 100
select * from posts where id > 100 order by id asc limit 100
通常使用 limt 和 offset 较慢,我们应该尽量避免使用。此文详细解释了使用 offset 的问题。chunkById 使用了类型为整型的 id 字段,查询使用 where 子句,查询会快很多。
什么时候使用 chunkById?
- 如果你的数据库表中使用了主键自增字段。
2. 只选择你需要的字段
通常我们会这样从数据库表中查询结果:
$posts = Post::find(1); //When using eloquent
$posts = DB::table('posts')->where('id','=',1)->first(); //When using query builder
以上代码会生成以下查询语句:
select * from posts where id = 1 limit 1
如你使见,查询语句使用了 select *
。这意味着会从数据表中查询所有字段。
如果我们确实需要所有字段数据,这样做没问题。但是如果我们只需要指定的字段(id、title), 可以只查询这些字段。
$posts = Post::select(['id','title'])->find(1); //When using eloquent
$posts = DB::table('posts')->where('id','=',1)->select(['id','title'])->first(); //When using query builder
以上代码会生成查询语句:
select id,title from posts where id = 1 limit 1
3. 当你确切需要 1 个或 2 个字段的时候,使用 pluck
此技巧主要关注的是,从数据库中获取数据后消耗的时间,不会影响真实的查询时间。
查询特定字段,我们通常会这么做:
$posts = Post::select(['title','slug'])->get(); //When using eloquent
$posts = DB::table('posts')->select(['title','slug'])->get(); //When using query builder
当以上代码执行的时候,幕后会做以下动作:
- 在数据库中执行
selet title, slug from post
查询 - 为每条数据创建一个 Post 模型对象(对于 query Builder 而言, 创建 PHP 标准对象)
- 用Post模型创建新的 collection
- 返回 Collection
现在, 我们可以这样获取结果:
foreach ($posts as $post){
// $post is a Post model or php standard object
$post->title;
$post->slug;
}
以上方式会有一个额外的开销,即为每行记录创建一个 Post 模型,之后还会创建一个 Collection 对象。如果你确实需要 Post 模型实例而非数据,这样做是最好。
但如果你只需要这两个值,你可以这样:
$posts = Post::pluck('title', 'slug'); //When using eloquent
$posts = DB::table('posts')->pluck('title','slug'); //When using query builder
执行以上代码,幕后会产生以下动作:
- 在数据库中执行
selet title, slug from post
查询 - 创建一个数组,以 slug 为数组索引,title 为数组值。
- 返回数组(数组格式: [ slug => title, slug => title ])
现在可以这样获取结果
foreach ($posts as $slug => $title){
// $title is the title of a post
// $slug is the slug of a post
}
如果你只想查询单个字段,你可以:
$posts = Post::pluck('title'); //When using eloquent
$posts = DB::table('posts')->pluck('title'); //When using query builder
foreach ($posts as $title){
// $title is the title of a post
}
以上方面不用为每一行记录创建一个 Post 对象,因此减少了查询的内存占用和时间消耗。
我只建议使用在新的代码中使用此方法。个人感觉没必要花时间按照此方式跑回去重构之前的代码。除非你已有的代码处理大量的数据或者你有空闲时间。