“N+1选择问题”在对象关系映射(ORM)讨论中通常被称为一个问题,我理解这与必须为对象世界中看似简单的东西进行大量数据库查询有关。
有人对这个问题有更详细的解释吗?
“N+1选择问题”在对象关系映射(ORM)讨论中通常被称为一个问题,我理解这与必须为对象世界中看似简单的东西进行大量数据库查询有关。
有人对这个问题有更详细的解释吗?
当前回答
正如其他人更优雅地指出的那样,问题是您要么拥有OneToMany列的笛卡尔积,要么正在进行N+1选择。无论是可能的巨大结果集,还是与数据库的聊天。
我很惊讶没有提到这一点,但我是如何解决这个问题的。。。我制作了一个半临时ID表。当您有IN()条款限制时,我也会这样做。
这并不适用于所有情况(可能甚至不适用于大多数情况),但如果您有很多子对象,使得笛卡儿乘积无法控制(即大量的OneToMany列,结果的数量将是列的乘积),并且它更像是一个批处理作业,那么它就特别适用。
首先,将父对象ID作为批处理插入到ID表中。batch_id是我们在应用程序中生成并保存的东西。
INSERT INTO temp_ids
(product_id, batch_id)
(SELECT p.product_id, ?
FROM product p ORDER BY p.product_id
LIMIT ? OFFSET ?);
现在,对于每个OneToMany列,您只需在id表INNER上执行SELECT,然后使用WHERE batch_id=(反之亦然)将子表JOIN。您只需要确保按id列排序,因为这将使合并结果列更容易(否则,您将需要一个HashMap/Table用于整个结果集,这可能不会那么糟糕)。
然后,只需定期清理ids表。
如果用户选择例如100个左右不同的项目进行某种批量处理,这也特别有效。将100个不同的ID放入临时表中。
现在,您正在执行的查询数量是OneToMany列的数量。
其他回答
提供的链接有一个非常简单的n+1问题示例。如果你将它应用于Hibernate,它基本上是在谈论相同的事情。查询对象时,实体将被加载,但任何关联(除非另有配置)都将被延迟加载。因此,一个查询用于根对象,另一个查询加载每个根对象的关联。返回的100个对象意味着一个初始查询,然后是100个附加查询,以获得每个对象的关联,n+1。
http://pramatr.com/2009/02/05/sql-n-1-selects-explained/
因为这个问题,我们离开了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的扇形折叠的实现,请参阅此网页。
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查询,并缓存有关连续执行父查询和子查询的数据。如果您的查询不完全相同,但在语义上是等价的,这甚至可以起作用。
这是对问题的一个很好的描述
现在您了解了这个问题,通常可以通过在查询中执行连接获取来避免。这基本上强制获取延迟加载的对象,因此数据在一个查询中而不是n+1个查询中检索。希望这有帮助。
在我看来,《Hibernate陷阱:为什么关系应该懒惰》中的文章与真正的N+1问题完全相反。
如果您需要正确的解释,请参阅Hibernate-第19章:提高性能-获取策略
选择提取(默认值)为极易受到N+1选择的影响问题,因此我们可能希望启用联接获取