我对数据库缺乏经验,刚刚阅读了"n+1 selects issue"。我的后续问题:假设数据库与我的程序驻留在同一台机器上,缓存在RAM中并正确编制索引,为什么n + 1查询模式会变慢?
作为一个例子,让我们从接受的答案中获取代码:
SELECT * FROM Cars;
/* for each car */
SELECT * FROM Wheel WHERE CarId = ?
使用我的数据库缓存的心理模型,每个SELECT * FROM Wheel WHERE CarId = ?
查询都需要:
get()
)CarId
(另一个散列图get()
)由于内部存储器结构,即使我们将其乘以一个小的常数因子以获得额外的开销,它仍然应该是不明显的快速。进程间通信是瓶颈吗?
修改:我刚通过黑客新闻发现了这篇相关文章:Following a Select Statement Through Postgres Internals. - HN discussion thread。
编辑2 :为了澄清,我做假设N
很大。一个非平凡的开销会加起来明显延迟,是的。对于上述设置,我首先要求为什么开销是非常重要的。
答案 0 :(得分:5)
在您描述的场景中,避免n + 1选择不太重要是正确的。如果数据库在远程机器上,则通信等待时间> 1ms是常见的,即cpu将花费数百万个时钟周期等待网络。
如果我们在同一台机器上,通信延迟要小几个数量级,但与另一个进程的同步通信必然涉及一个上下文切换,其通常成本> 1。 0.01 ms(source),即数万个时钟周期。
此外,ORM工具和数据库每个查询都会有一些开销。
总而言之,如果数据库是本地的,那么避免n + 1选择就不那么重要了,但是如果n很大,那么仍然很重要。
答案 1 :(得分:3)
假设数据库与我的程序位于同一台机器上
从不假设这一点。考虑这样的特殊情况绝不是一个好主意。您的数据很可能会增长,您需要将数据库放在另一台服务器上。或者您需要冗余,这涉及(您猜对了)另一台服务器。或者为了安全起见,您可能不希望您的应用服务器与数据库位于同一个框中。
为什么n + 1查询模式会变慢?
你不认为这很慢,因为你的表现心理模型可能都错了。
1)内存非常缓慢。每次需要从RAM读取内容时,你的CPU就会浪费大约200-400个CPU周期。 CPU有很多技巧可以隐藏它(缓存,流水线,超线程)
2)从RAM读取不是“随机访问”。它就像一个硬盘驱动器:顺序读取速度更快。 请参阅这篇文章,了解如何以正确的顺序访问RAM的速度提高76.6%http://lwn.net/Articles/255364/(如果您想知道实际上有多么可怕的RAM,请阅读整篇文章。)
CPU缓存
在“N + 1查询”的情况下,每个N的“循环”包括许多兆字节的代码(在客户端和服务器上)在每次迭代时交换进出缓存,加上上下文切换(通常会转储缓存)反正)。
“1查询”案例可能涉及服务器上的单个紧密循环(查找并复制每一行),然后是客户端上的单个紧密循环(读取每一行)。如果这些循环足够小,它们可以从缓存中运行10-100倍。
RAM顺序访问
“1 query”案例将读取从DB到一个线性缓冲区的所有内容,将其发送给将线性读取的客户端。数据传输期间无随机访问。
“N + 1查询”案例将分配和解除分配RAM N次,这(由于各种原因)可能与RAM的物理位不同。
其他各种原因
网络子系统只需读取一个或两个TCP标头,而不是N.
您的数据库只需要解析一个查询而不是N.
当您投入多用户时,“局部性/顺序访问”在N + 1情况下变得更加分散,但在1查询的情况下保持相当好。
CPU使用的许多其他技巧(例如分支预测)在紧密循环中效果更好。
请参阅:http://blogs.msdn.com/b/oldnewthing/archive/2014/06/13/10533875.aspx
答案 2 :(得分:1)
将数据库放在本地计算机上可以减少问题;但是,大多数应用程序和数据库都在不同的机器上,每次往返至少需要几毫秒。
数据库还需要对每个查询进行大量锁定和锁定检查。 meriton已经提到了上下文切换。如果您不使用周围的事务,它还必须为每个查询构建隐式事务。一些查询解析开销仍然存在,即使使用参数化,准备好的查询或通过字符串相等(带参数)记住的查询。
如果数据库被填满,查询时间可能会增加,而开头几乎是空数据库。
如果您的数据库要被其他应用程序使用,您可能会对其进行攻击:即使您的应用程序正常运行,其他应用程序也可能会变慢甚至出现越来越多的失败,例如超时和死锁。
另外,请考虑拥有两个以上的数据级别。想象一下三个级别:博客,条目,评论,100个博客,每个博目有10个条目和10条评论(平均)。这是 SELECT 1 + N +(NxM)情况。它将需要100个查询来检索博客条目,并需要另外1000个来获取所有评论。一些更复杂的数据,你将遇到10000甚至100000。
当然,糟糕的编程在某些情况下可能会有所作为。如果数据库总是在同一台机器上,那么没有其他人使用它,并且汽车的数量永远不会超过100,即使是非常不理想的程序也可能就足够了。但要注意这些先决条件中的任何一个都会发生变化:重构整个事情会比你在开始时做正确的事情花费更多的时间。而且,您可能会首先尝试其他一些解决方法:一些IF条款,内存缓存等等,它们在开始时会有所帮助,但会使代码更加混乱。最后,你可能会被困在一个"永远不会碰到正在运行的系统"位置,系统性能变得越来越不可接受,但重构风险太大,而且比更改正确的代码要复杂得多。
另外,一个好的ORM为您提供了N + 1的方法:(N)Hibernate,例如,允许您指定批量大小(将许多SELECT * FROM Wheels WHERE CarId=?
个查询合并为一个SELECT * FROM Wheels WHERE CarId IN (?, ?, ..., ?)
)或者使用子选择(如:SELECT * FROM Wheels WHERE CarId IN (SELECT Id FROM Cars)
)。
避免N + 1的最简单的选择是连接,缺点是每个车行乘以车轮的数量,并且多个子/孙项目可能最终成为连接结果的巨大笛卡尔积。< / p>
答案 3 :(得分:0)
即使数据库位于同一台计算机上,缓存在RAM中并正确编制索引,仍然存在开销。此开销的大小取决于您正在使用的DBMS,运行的计算机,用户数量,DBMS的配置(隔离级别......)等。
检索N行时,您可以选择支付此费用一次或N次。如果N足够大,即使很小的成本也会变得明显。
有一天,有人可能想要将数据库放在单独的计算机上或使用不同的dbms。这在商业世界中经常发生(符合一些ISO标准,降低成本,改变供应商,......)
因此,有时计划数据库不闪电的情况很好。
所有这些在很大程度上取决于软件的用途。避免“选择n + 1问题”并不总是必要的,这只是一个经验法则,以避免常见的陷阱。