【问题标题】:Why is the n+1 selects pattern slow?为什么 n+1 选择模式很慢?
【发布时间】:2014-12-02 10:26:08
【问题描述】:

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

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

SELECT * FROM Cars;

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

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

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

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


编辑:我刚刚通过黑客新闻找到了这篇相关文章:Following a Select Statement Through Postgres Internals. - HN discussion thread

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

【问题讨论】:

  • 因为一条语句检索 n+1 行几乎总是比 n+1 条语句快。解析 SQL、准备执行计划和返回数据都会产生开销(取决于 DBMS,解析阶段可能是相当大的开销)。此外:你会读一份蛋糕的食谱,然后去超市买面粉,回家读食谱,回去买鸡蛋,回家读食谱,回去买黄油,......
  • “因为一条语句检索 n+1 行几乎总是比 n+1 条语句快。” - 我看到了,但令我惊讶的是,人们用 Ruby 或 Python 而不是 C 编写程序实际上需要更长的时间。我发现的解决方案有时看起来比原始代码复杂得多。重新蛋糕食谱:但如果配料就在另一个房间里,我可能不会太在意去两次或三次;)。
  • 重新查询解析开销:鉴于我们已经在任何地方使用准备好的语句,这难道不是一个开发良好的库应该解决的问题吗?或者这是否会破坏公共接口,我们又回到了 IPC 开销。 (@a_horse_with_no_name)
  • “一个完善的库应该解决的问题” - 例如,Python caches compiled regular expressions by default
  • 进程间通信慢。

标签: sql orm select-n-plus-1


【解决方案1】:

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

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

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

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

【讨论】:

    【解决方案2】:

    将数据库放在本地机器上可以减少问题;但是,大多数应用程序和数据库将位于不同的机器上,每次往返至少需要几毫秒。

    数据库还需要对每个单独的查询进行大量锁定和闩锁检查。 meriton 已经提到了上下文切换。如果您不使用周边事务,它还必须为每个查询构建隐式事务。一些查询解析开销仍然存在,即使是参数化、准备好的查询或字符串相等(带参数)记住的查询。

    如果数据库被填满,与一开始几乎空的数据库相比,查询时间可能会增加。

    如果您的数据库要被其他应用程序使用,您可能会对其进行锤击:即使您的应用程序工作正常,其他应用程序也可能会变慢甚至出现越来越多的故障,例如超时和死锁。

    另外,请考虑拥有两个以上级别的数据。想象三个级别:博客、条目、评论,有 100 个博客,每个博客有 10 个条目,每个条目有 10 个 cmets(平均)。这是 SELECT 1+N+(NxM) 的情况。检索博客条目需要 100 次查询,获取所有 cmets 需要另外 1000 次。一些更复杂的数据,你会遇到10000s甚至100000s。

    当然,糟糕的编程可能会在某些情况下和在某种程度上起作用。如果数据库总是在同一台机器上,没有其他人使用它,而且汽车的数量永远不会超过 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 的最简单选择是连接,缺点是每个车排乘以轮数,并且多个子项/孙项可能最终以连接结果的巨大笛卡尔积而告终。

    【讨论】:

      【解决方案3】:

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

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

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

      因此,有时最好针对数据库速度不快的情况进行计划。

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

      【讨论】:

      • 很抱歉,您并没有真正回答我的问题。如果每个请求有明显的开销,那么它会加起来很大n,这很清楚。我还看到,对于许多应用程序,您希望将来证明您的代码在非本地数据库上足够快。但是,在某些情况下,这将是不必要的,例如考虑 sqlite DB。您是否暗示在这些情况下(使用正确的数据库)n+1 选择模式并不慢
      • 我试图解释这个问题没有非黑即白的答案。 n 总是比 1 慢,但在某些情况下,n 可能仍然足够快。这取决于。嵌入式 SQLite 数据库是 n 可能会保持足够小的情况之一。
      • 查看编辑以澄清问题。作为一个现实生活中的例子,考虑 mpd,一个音乐播放器,它有一个包含数千个条目的本地音乐文件数据库。在我的树莓派上搜索歌曲时的多秒延迟证明 n 是“大”并且访问模式效率低下。从某种意义上说,这里的世界很黑。 (虽然,使用 mpd 搜索速度慢的实际原因是缺少索引,但你明白了。)
      【解决方案4】:

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

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

      为什么 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 查询”案例会将数据库中的所有内容读取到一个线性缓冲区,然后将其发送给将线性读取它的客户端。数据传输过程中没有随机访问。

      “N+1 查询”的情况将分配和取消分配 RAM N 次,这(由于各种原因)可能不是 RAM 的同一物理位。

      其他各种原因

      网络子系统只需要读取一两个 TCP 头,而不是 N。

      您的数据库只需要解析一个查询而不是 N 个。

      当您加入多用户时,“局部/顺序访问”在 N+1 情况下会变得更加分散,但在 1 查询情况下仍然很好。

      CPU 使用的许多其他技巧(例如分支预测)在紧密循环中效果更好。

      见:http://blogs.msdn.com/b/oldnewthing/archive/2014/06/13/10533875.aspx

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2011-03-30
        • 2021-10-31
        • 2010-10-21
        • 1970-01-01
        • 2021-04-20
        • 1970-01-01
        • 2015-03-14
        相关资源
        最近更新 更多