编程

18条优化Laravel 数据库查询的建议(1-3)

1670 2021-12-25 00:09:05

如果你的应用运行缓慢,使用了很多数据库查询,以下的性能优化秘诀可以改进你应用的加载时间。

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 对象,因此减少了查询的内存占用和时间消耗。

我只建议使用在新的代码中使用此方法。个人感觉没必要花时间按照此方式跑回去重构之前的代码。除非你已有的代码处理大量的数据或者你有空闲时间。