[EagerLoad] leftJoin 和 with 哪个更有用?[N+1 问题]


我是系统开发部的榎木,最近我的前辈和上司都跟我说
“任何没写过原始查询语句的人都应该认真对待这件事”。 (顺便说一句,我现在负责新毕业生的MySQL培训,所以我对此非常担忧。)

N+1 问题

(暂且不谈我的故事)假设
建立了以下关系。

class Bolg { public function comments(): HasMany { return $this->hasMany(Comment::class); } }

获取博尔格模型

$blogs = Bolg::query()->first();

此处发出以下查询:

"select * from `blogs`"

通过继续循环,我们可以得到与每个 Blog 模型关联的所有 Comment 模型。

$comments = $blogs->map(function ($blog) { return $blogs->comments->first(); });

当然,由于这是一个循环,以下查询将执行与章节数相同的次数。

"select * from `blogs` where `blogs`.`blog_id` = ? and `blogs`.`blog_id` is not null"

初始查询(用于检索所有记录)会在发出 N 个查询后执行一次。
这就是 1+N 或 N+1 问题。

问题在于,自然而然地,随着章节数的增加,查询次数也会增加,这无疑是个问题。

加入并避免 N+1

现在我们已经讲到重点了。

如上所述,每次都发出查询效率极低,因此应该使用 (left)join 或 with 语句。
(顺便一提,在某些情况下,每次都进行检索可能更好,所以要谨慎选择。)
这些方法都可以用来避免发送大量查询。
但是,它们各自的作用有所不同。

什么是连接?

该连接是内连接,它只
保留表中共有的数据,并剔除其余数据。

leftJoin 执行左外连接
,返回左表中的所有行以及右表中所有匹配的行。这样,
您就可以获得符合连接条件的数据,同时保留左表中的所有数据。

这些连接是 SQL 的一个特性。
它们可以在一个查询中连接多个表。
就查询而言,它们看起来像这样:

0 => array:3[ "query" => "select * from `blogs` inner join `comments` on `blogs`.`id` = `blog_id`" "bindings" => [] "time" => 1.5 ],

仅仅增加了一个查询!
这真是巨大的进步!

那么,和……在一起呢?

with 查询
运行两次,这比 join 更糟糕,但比 N+1 循环有了巨大的改进。

顺便一提,`with` 函数的参数字符串是模型文件中关系定义方法的名称。
(它本质上与相关模型的名称相同,但如果您没有将其定义为方法,则无法使用 `with` 函数,因此请务必小心。)

$blogAndComments= Blog::query() ->with('comments') ->first();

查看查询语句,我们可以看到使用 whereIn 可以一次性检索所有数据。

0 => array:3[ "query" => "select * from `blogs`" "bindings" => [] "time" => 0.5 ], 1 => array:3[ "query" => "select * from `blogs` where `blogs`.`blogt_id` in (?)" "bindings" => [] "time" => 1.5 ],

各自的特点和用途

我们采用两种方法获得了该模型,现在让我们来看看每种方法的特点。

●(左)JOIN
函数根据连接条件合并模型。JOIN
是一个 SQL 函数,用于一次性从多个表中检索数据。
检索时,只会创建符合连接条件的数据量
在本例中,您可以将最终创建的数据量视为与 Comment 模型中的数据量相同。
如果连接多个表,则可能会生成无效数据。

然而,能够一次性获取所有数据是很有吸引力的,
当不需要将其作为模型或需要简单、扁平的数据时,它是理想的选择。

● 它以
关联数组的形式检索模型,而无需将它们合并。
预先加载ORM 中的,它解决了 N+1 问题。

在本例中,与 Bolg 模型关联的 Comment 模型将存在于“relations”属性下。
模型可以嵌套,并按照其父子关系的层级顺序检索,因此模型不会合并。
相关的模型和数据会一次性全部检索,从而消除冗余数据。
当您希望将检索到的数据直接作为模型处理时,此功能也很有用。

顺便说一下,可以使用 with 来检索关系,只要它们持续存在,例如 (ModelA.ModelB.ModelC.),但由于嵌套意味着所有传递的模型都将作为外壳包含在内,因此最终需要使用 unset 或类似方法将其删除。

总结与题外话

现在所有信息都已公布,我将对其进行总结,然后在结论之前再补充一些题外话。

概括

你觉得怎么样?
信息量很大,所以我把它整理成了一个表格。

我不知道哪个才真正有用!
虽然结果大致都是从多个表中检索数据,但它们的具体操作却截然不同。

因此,总的结论是:“根据所需的数据格式更改采集方法。”
最简单的判断方法是确定
您需要的是原始模型还是可以接受合并模型或者,您可以根据哪种形式更便于后续处理,选择嵌套形式或平面形式

此外,我认为应该
根据要检索的数据总量来调整因为对于加载关系来说,嵌套越深,速度就越慢,相比之下,连接操作速度更慢。
换句话说,这取决于具体情况。

附注(EagerLoad)

我想写一下关于 EagerLoad 的内容。
你可能已经明白,它的基本含义是不希望每次都执行 SQL 语句。

这有点枯燥,所以让我详细解释一下。
“EagerLoad 仅适用于动态属性”这就是它的含义。
(也许这说不通)
首先,我们来谈谈动态属性和关系方法之间的区别。
换句话说,这和

$blog= 新建博客;$blog->评论;

这就是区别所在。

$blog = new Blog; $blog->comments();

->comments 是一个动态属性,它返回一个集合,并且
具有“延迟加载”属性,这意味着相关数据仅在访问时加载,
而 ->comments() 返回一个相关对象。

你不能将带有动态属性的查询构建器链接在一起,但

你可以像这样链接它们: ->comments()->where('create_user_id', 1)

因此,最好将其作为关联对象检索。
这里,请记住 EagerLoad 的属性,例如 with。

是的,您之前已经发出了查询
由于数据已被检索(在关系属性中),因此
当您编写 $blog->comment->create_user_id 时,查询将不会执行。

这意味着“除非属性是动态的,否则不能使用 EagerLoad”
,或者换句话说,
“如果使用 EagerLoad,后续处理也必须是动态属性,否则将毫无意义!!”这是一个自相矛盾的情况。

那么,在检索数据时,如何应用条件、限制和搜索呢?!

$blog = Bolg::with(['comments' => function ($query) { $query->where('create_user_id', 1); }])->first(); $comments = $blog->comments;

通过这种方式,您可以将 EagerLoad 与条件、约束和搜索结合使用。
另外,如果您有一些经常使用的条件,可以将它们转换为模型文件中的方法,这样可以提高代码的可读性,也更方便使用。

结束

我希望这篇文章能让访问世界各地的数据库变得更容易一些。

如果您觉得这篇文章有用,请点击【点赞】!
6
加载中...
6票,平均分:1.00/16
865
X Facebook Hatena书签 口袋

这篇文章的作者

关于作者

榎木

我什么游戏都玩,包括第一人称射击游戏、角色扮演游戏、大型多人在线游戏和建造类游戏。