5

我对数据库相当缺乏经验,并且刚刚阅读了有关“n+1 选择问题”的信息。我的后续问题:假设数据库与我的程序位于同一台机器上,缓存在 RAM 中并正确索引,为什么 n+1 查询模式很慢?

作为一个例子,让我们从接受的答案中获取代码:

SELECT * FROM Cars;

/* for each car */
SELECT * FROM Wheel WHERE CarId = ?

根据我对数据库缓存的心智模型,每个SELECT * FROM Wheel WHERE CarId = ?查询都应该需要:

  • 1 次查找以到达“Wheel”表(一个 hashmap get()
  • 1 次查找以到达具有指定CarId(另一个 hashmap get())的 k 个轮子列表
  • k 查找以获取每个匹配车轮的车轮行(k 指针取消引用)

即使我们将它乘以一个小的常数因子以获得额外的开销,因为内部内存结构,它仍然应该快得不明显。进程间通信是瓶颈吗?


编辑:我刚刚通过 Hacker News 找到了这篇相关文章:通过 Postgres Internals 遵循 Select Statement。-HN讨论线程

编辑2:为了澄清,我确实假设N很大。那么,一个不平凡的开销将增加一个明显的延迟,是的。我在问为什么对于上述设置,开销首先不是微不足道的。

4

4 回答 4

5

您是正确的,在您描述的场景中避免 n+1 选择不太重要。如果数据库在远程机器上,> 1ms 的通信延迟很常见,即cpu 将花费数百万个时钟周期等待网络。

如果我们在同一台机器上,通信延迟会小几个数量级,但与另一个进程的同步通信必然会涉及到上下文切换,这通常花费> 0.01 ms(来源),也就是数万个时钟周期。

此外,ORM 工具和数据库每次查询都会有一些开销。

总而言之,如果数据库是本地的,那么避免 n+1 次选择就没有那么重要了,但如果 n 很大,那么仍然很重要。

于 2014-10-14T09:08:57.460 回答
3

假设数据库与我的程序位于同一台机器上

永远不要假设这一点。考虑这样的特殊情况绝不是一个好主意。您的数据很可能会增长,您需要将数据库放在另一台服务器上。或者您将需要冗余,这涉及(您猜对了)另一台服务器。或者为了安全起见,您可能不希望您的应用服务器与数据库位于同一个盒子上。

为什么 n+1 查询模式很慢?

你不认为这很慢,因为你的表现心理模型可能全错了。

1) RAM 非常慢。您的 CPU 每次需要从 RAM 中读取某些内容时,都会浪费大约 200-400 个 CPU 周期。CPU 有很多技巧可以隐藏这一点(缓存、流水线、超线程)

2) 从 RAM 读取不是“随机访问”。它就像一个硬盘驱动器:顺序读取速度更快。请参阅这篇关于如何以正确的顺序访问 RAM 的速度提高 76.6%的文章http://lwn.net/Articles/255364/ (如果您想知道 RAM 的实际复杂程度,请阅读整篇文章。)

CPU缓存

在您的“N+1 查询”情况下,每个 N 的“循环”包括在每次迭代时交换进出缓存的许多兆字节的代码(在客户端和服务器上),以及上下文切换(通常无论如何都会转储缓存)。

“1 查询”情况可能涉及服务器上的单个紧密循环(查找和复制每一行),然后是客户端上的单个紧密循环(读取每一行)。如果这些循环足够小,它们可以从缓存中以 10-100 倍的速度运行。

RAM 顺序存取

“1 查询”案例将从 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

于 2014-10-18T23:51:25.907 回答
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 的最简单选择是连接,其缺点是每个车排乘以轮数,并且多个子项/孙项可能以连接结果的巨大笛卡尔积而告终。

于 2014-10-14T10:01:14.290 回答
0

即使数据库在同一台机器上,缓存在 RAM 中并正确索引,仍然存在开销。这种开销的大小取决于您使用的 DBMS、运行它的机器、用户数量、DBMS 的配置(隔离级别……)等等。

在检索 N 行时,您可以选择支付此费用一次或 N 次。如果 N 足够大,即使是很小的成本也会变得引人注目。

有一天,有人可能想将数据库放在另一台机器上或使用不同的 dbms。这在商业世界中经常发生(为了符合某些 ISO 标准、降低成本、更换供应商……)

因此,有时最好为数据库不是闪电般快速的情况进行计划。

所有这些在很大程度上取决于软件的用途。避免“选择 n+1 问题”并不总是必要的,这只是一个经验法则,可以避免常见的陷阱。

于 2014-10-17T21:10:45.647 回答