“N+1选择问题”在对象关系映射(ORM)讨论中通常被称为一个问题,我理解这与必须为对象世界中看似简单的东西进行大量数据库查询有关。

有人对这个问题有更详细的解释吗?


当前回答

因为这个问题,我们离开了Django的ORM。基本上,如果你尝试

for p in person:
    print p.car.colour

ORM将很高兴地返回所有人(通常作为Person对象的实例),但随后需要为每个Person查询car表。

一种简单且非常有效的方法是我称之为“扇形折叠”的方法,它避免了来自关系数据库的查询结果应该映射回组成查询的原始表的荒谬想法。

步骤1:宽选择

  select * from people_car_colour; # this is a view or sql function

这将返回类似

  p.id | p.name | p.telno | car.id | car.type | car.colour
  -----+--------+---------+--------+----------+-----------
  2    | jones  | 2145    | 77     | ford     | red
  2    | jones  | 2145    | 1012   | toyota   | blue
  16   | ashby  | 124     | 99     | bmw      | yellow

第2步:客观化

将结果吸入通用对象创建器中,并在第三项之后添加一个要拆分的参数。这意味着“jones”对象不会被制作多次。

步骤3:渲染

for p in people:
    print p.car.colour # no more car queries

有关python的扇形折叠的实现,请参阅此网页。

其他回答

假设您有一组Car对象(数据库行),每个Car都有一组Wheel对象(也有行)。换句话说,汽车→ 轮子是一对多的关系。

现在,假设您需要遍历所有汽车,并为每辆汽车打印出车轮列表。天真的O/R实现将执行以下操作:

SELECT * FROM Cars;

然后,对于每辆车:

SELECT * FROM Wheel WHERE CarId = ?

换言之,您可以为汽车选择一个选项,然后再选择N个选项,其中N是汽车的总数。

或者,可以获取所有轮子并在内存中执行查找:

SELECT * FROM Wheel;

这将到数据库的往返次数从N+1减少到2。大多数ORM工具提供了几种防止N+1选择的方法。

参考资料:Java Persistence with Hibernate,第13章。

我不能直接评论其他答案,因为我没有足够的声誉。但值得注意的是,这个问题本质上只会出现,因为从历史上看,很多dbm在处理连接时都非常糟糕(MySQL是一个特别值得注意的例子)。因此,n+1通常比join快得多。然后有一些方法可以改进n+1,但仍然不需要连接,这就是最初的问题所在。

然而,在连接方面,MySQL现在比过去好了很多。当我第一次学习MySQL时,我经常使用联接。然后我发现它们有多慢,并在代码中改用n+1。但是,最近,我又回到了连接,因为MySQL现在在处理它们方面比我刚开始使用它时要好得多。

现在,从性能角度来看,在一组索引正确的表上进行简单联接很少有问题。如果它确实影响了性能,那么使用索引提示通常可以解决这些问题。

MySQL的一个开发团队在这里讨论了这一点:

http://jorgenloland.blogspot.co.uk/2013/02/dbt-3-q3-6-x-performance-in-mysql-5610.html

所以总结是:如果您过去一直在避免连接,因为MySQL的性能糟糕,那么请在最新版本上重试。你可能会感到惊喜。

N+1的推广

N+1问题是一个ORM特有的问题名称,它将可以在服务器上合理执行的循环移动到客户端。通用问题不是ORM特有的,您可以通过任何远程API解决。在本文中,我展示了如果您调用一个API N次而不是仅调用1次,JDBC往返是如何代价高昂的。示例中的区别在于您是否调用Oracle PL/SQL过程:

dbms_output.get_lines(调用一次,接收N个项目)dbms_output.get_line(调用N次,每次接收1项)

它们在逻辑上是等价的,但由于服务器和客户端之间的延迟,您需要在循环中添加N个延迟等待,而不是只等待一次。

ORM案例

事实上,ORM-y N+1问题甚至不是ORM特有的,您也可以通过手动运行自己的查询来实现,例如,当您在PL/SQL中执行以下操作时:

-- This loop is executed once
for parent in (select * from parent) loop

  -- This loop is executed N times
  for child in (select * from child where parent_id = parent.id) loop
    ...
  end loop;
end loop;

使用联接(在本例中)实现这一点会更好:

for rec in (
  select *
  from parent p
  join child c on c.parent_id = p.id
)
loop
  ...
end loop;

现在,循环只执行一次,并且循环的逻辑已经从客户端(PL/SQL)移动到服务器(SQL),这甚至可以以不同的方式对其进行优化,例如,通过运行哈希连接(O(N))而不是嵌套循环连接(带索引的O(N log N))

自动检测N+1个问题

如果您使用的是JDBC,可以在后台使用jOOQ作为JDBC代理来自动检测N+1问题。jOOQ的解析器规范化您的SQL查询,并缓存有关连续执行父查询和子查询的数据。如果您的查询不完全相同,但在语义上是等价的,这甚至可以起作用。

因为这个问题,我们离开了Django的ORM。基本上,如果你尝试

for p in person:
    print p.car.colour

ORM将很高兴地返回所有人(通常作为Person对象的实例),但随后需要为每个Person查询car表。

一种简单且非常有效的方法是我称之为“扇形折叠”的方法,它避免了来自关系数据库的查询结果应该映射回组成查询的原始表的荒谬想法。

步骤1:宽选择

  select * from people_car_colour; # this is a view or sql function

这将返回类似

  p.id | p.name | p.telno | car.id | car.type | car.colour
  -----+--------+---------+--------+----------+-----------
  2    | jones  | 2145    | 77     | ford     | red
  2    | jones  | 2145    | 1012   | toyota   | blue
  16   | ashby  | 124     | 99     | bmw      | yellow

第2步:客观化

将结果吸入通用对象创建器中,并在第三项之后添加一个要拆分的参数。这意味着“jones”对象不会被制作多次。

步骤3:渲染

for p in people:
    print p.car.colour # no more car queries

有关python的扇形折叠的实现,请参阅此网页。

查看Ayende关于以下主题的帖子:打击NHibernate中的选择N+1问题。

基本上,当使用像NHibernate或EntityFramework这样的ORM时,如果您有一对多(主详细信息)关系,并且希望列出每个主记录的所有详细信息,则必须对数据库进行N+1次查询调用,“N”是主记录的数量:1次查询获取所有主记录,N次查询,每个主记录一次,获取每个主记录所有详细信息。

更多数据库查询调用→ 延迟时间更长→ 降低了应用程序/数据库性能。

然而,ORM可以选择避免这个问题,主要是使用JOIN。