"N + 1选择问题"通常被称为对象关系映射(ORM)讨论中的一个问题,我理解它必须为对象中看起来很简单的事情做出大量的数据库查询。 世界。
有没有人对这个问题有更详细的解释?
-
IMO javalobby.org/java/forums/t20533.html这个解释更好
-
对于理解n + 1问题,这是一个很好的解释。 它还涵盖了解决此问题的解决方案:architects.dzone.com/articles/how-identify-and-resilve-n1
-
有一些有用的帖子谈论这个问题和可能的解决方案。 常见的应用问题及其解决方法:选择N + 1问题,N + 1问题的(银)子弹,延迟加载 - 急切加载
-
对于寻找这个问题的解决方案的每个人,我找到了一个描述它的帖子。stackoverflow.com/questions/32453989/
-
考虑到答案,这不应该被称为1 + N问题吗? 由于这似乎是一个术语,我不是,特别是,要求OP。
假设您有一组Car对象(数据库行),每个Car都有一个Wheel对象(也是行)的集合。换句话说,Car - > Wheel是1对多的关系。
现在,假设您需要遍历所有车辆,并为每个车辆打印出车轮列表。天真的O / R实现将执行以下操作:
然后为每个Car:
1
| SELECT * FROM Wheel WHERE CarId = ? |
换句话说,您有一个选择汽车,然后N个额外选择,其中N是汽车总数。
或者,可以获得所有轮子并在内存中执行查找:
这减少了从N + 1到2的数据库往返次数。
大多数ORM工具为您提供了几种防止N + 1选择的方法。
参考:Java Persistence with Hibernate,第13章。
-
为了澄清"这是坏事" - 你可以用1选择(SELECT * from Wheel;)而不是N + 1来获得所有轮子。如果N值很大,性能损失可能会非常显着。
-
@tucuxi我很惊讶你错了很多赞成票。数据库非常适合索引,对特定CarID执行查询会非常快速地返回。但是如果你所有的Wheels都是一次,你将不得不在你的应用程序中搜索CarID,它没有被编入索引,这个速度较慢。除非你有很长的延迟问题到达你的数据库,否则n + 1实际上更快 - 是的,我用各种各样的真实代码对它进行基准测试。
-
@ariel'正确'的方法是获得所有车轮,按CarId(1选择)排序,如果需要比CarId更多的细节,则对所有车辆进行第二次查询(总共2次查询)。打印出来的东西现在是最佳的,不需要索引或二级存储(您可以迭代结果,无需全部下载)。你对错误的东西进行了基准测试如果您对基准仍有信心,您是否介意发布更长的评论(或完整答案)来解释您的实验和结果?
-
@tucuxi我对加入轮子和汽车进行了基准测试(并在应用程序代码中进行了重复数据删除),这确实比较慢 - 慢得多。将它们作为两个查询并通过第一个查询对第二个查询进行排序要好得多。这个页面上的其他答案并没有说这样做,但他们谈论加入。如果第一个查询只获得行的子集,则需要对第二个查询进行更复杂的查询以限制获得的数据量,因此它不一定是最好的方式,尽管可能是,但需要检查具体情况。
-
@Ariel你说"数据库非常适合索引,对特定CarID进行查询会非常快速地返回。"所以我要做的是让所有车轮按CarID(1选择)排序,然后在Java代码中循环通过我获得的(唯一的)N CarID,并为每个新的CarID发出一个具有该特定CarID的Car的Select查询(根据你的速度非常快)。所以基本上我得到N + 1选择查询的总结果但非常快。
-
@Ariel(续...)如果我说得对,你建议的是原始问题中的天真O / R实现。所以你首先获得所有的汽车(1选择),然后为每辆汽车你在Wheel表上发出一个Select查询,所以这不是那么快(因为你不是简单地通过WheelID获取一个轮子,你取一个轮子通过其CarID)。所以你得到1 + N选择查询,但N选择很慢。总之,我会说(基于你报告的基准)关于执行的时间:我的N + 1选择<天真N + 1选择
-
@rapt除了N选择不慢,它可以比连接加上临时表中的排序更快。有关原因的详细信息,请参阅我对cfeduke答案的回复。
-
我认为这个问题是通过延迟初始化解决的,对吧?
-
"Hibernate(我不熟悉其他ORM框架)为您提供了几种处理它的方法。"这些方式是?
-
@ariel - 说天真的方法是"正确的"是不对的。如果说某些情况下天真的方法可以表现得更好,那就更准确了。一般来说,1个查询优于n。请参阅big-o表示法。虽然O(1)是比O(N)更好的算法,但是存在恒定时间方法不是最快的实际情况。
-
@ Lee-Slalom它可能是1个数据库查询 - 但是你必须使用应用程序中的数据,而你的应用程序比数据库慢。因此,您对O()的分析过于简单 - 您只需要包含数据库中的工作,而忽略应用程序必须执行的操作。
-
@Ariel尝试在不同的计算机上运行数据库和应用程序服务器的基准测试。根据我的经验,往返数据库的开销比查询本身花费更多。所以,是的,查询真的很快,但这是匆匆忙忙的往返旅行。我已经将"WHERE Id = const"转换为"WHERE Id IN(const,const,...)"并从中获得了数量级的增加。
-
@Hans我在回复中多次说过。也许你撇去并错过了它。如果您的查询有延迟,则情况会发生变化。但至少对于典型的web-dev工作,数据库通常位于同一台机器上。
-
有许多影响因素决定了连接(一个选择)是否快于两个或N + 1选择。我认为在正确的设置中,连接应该是最快的,并且具有N + 1选择的版本应该是最慢的。但是,YMMV。
-
@Ariel - 或者让Db返回按CarId排序的所有轮子 - 现在你已经有了一个排序列表,在你的应用程序中可以快速访问,只需要一个Db rountrip。数据开销更少,查询更少,您的应用程序所做的唯一额外工作就是在列表中找到carId的第一个/最后一个实例。在另一个主题上,我不知道你工作的网站有多大,但我很少在与网站相同的(虚拟)机器上使用Db ...
-
@Basic你在开玩笑吗?你想要返回所有轮子???如果您只需要其中一些怎么办?
-
然后...... WHERE Car.Id IN (SELECT Id FROM Cars WHERE...)?我并不是说你应该总是将最好的操作卸载到应用程序的Db。我说有时候这种权衡是值得的。
-
一个很好的权衡策略是在选择汽车时保持对整个结果集的引用(例如,1页结果) - 然后使用以逗号分隔的所有汽车ID列表来执行后续SELECT FROM wheels w WHERE w.car_id IN (...)结果集。无论您的数据库延迟是高还是低,两次往返都比JOIN更经济 - 即使总响应时间略高于一次JOIN查询,整体CPU时间也会更低,并且应该在生产系统上平均更快许多并发用户。 JOIN作为优化(以节省CPU)是一个神话。
-
假设您的表被正确编入索引,Select Cars.*, Wheels.* from Cars inner join Wheels on Cars.CarID=Wheels.CardID order by Cars.CardID,Wheels.WheelID是检索此信息的最有效方法。如果需要限制,请在其中加入WHERE子句。所有现代ORM值得使用这样的查询。如果多个SELECTS比单个JOIN快,那么您的数据库设计会出现问题。
-
@Stephen Byrne:不一定。使用Joins,您最常发送冗余数据(另请参阅笛卡尔积的x-Joins)。如果Cars.*包含大量数据,则两个单个查询可以更快并节省资源。
-
@ sl3dg3如果您正在使用,请使用。*,但这仅用于说明目的。实际上,ORM将只显式选择映射列...
-
对不起,我一定是错的。但是,你不能加入一个单一的查询来获得所有车轮。在这种情况下,N + 1问题出在哪里?
-
@Tima超级老帖但似乎没有答案。所以...在Nhibernate中你应该创建一个搜索条件并将其设置为eager fetch。 session.CreateCriteria.SetFetchMode("Wheels",FetchMode.Eager))。您也可以使用映射器Map(x => x.Wheel).Not.LazyLoad();
-
我认为,如果您使用ScaleArc之类的东西进行负载平衡,连接池管理和数据包级缓存,那么我会提倡n + 1而不是针对特定目的的智能编写查询的唯一实例。老实说,在我的大多数经验中,n + 1的拥护者找到了积极的边缘情况来证明更简单和更简洁的UnitOfWork / Repository模式。这不是过早的优化。但过于简单会让你陷入困境。如果你在任何紧密循环(没有中间技术)的情况下懒惰加载,你做错了。故事结局。
-
为什么你不能说select * from wheel where carId in (id1, id2...)
-
@mickeymoon是因为当你使用延迟加载来访问子集合时,SELECT是由NHibernate生成的,而NHibernate不会生成像这样的SELECT
-
@Ariel +1用于实际考虑到DB的距离。根据我的经验,当DB在同一台机器上时,延迟加载几乎总是更快。
-
a)为什么每个人都在谈论这个问题,好像这是两次往返查询?它最终将成为许多往返查询,通常是在Web服务器坐下并等待数据以便它可以返回到客户端时,b)我认为来自Web服务器的延迟加载并不"总是更快"。因为你正在占用等待数据库结果的有限资源(网络连接,内存),c)数据库只会在相同的机器上用于相对较小的站点。一旦尝试扩展,数据库和Web服务器就会分开。
-
汽车示例不能简化为单个查询吗? SELECT * from Cars INNER JOIN Wheels ON Cars.id = Wheels.CarID ORDER BY Cars.ID然后结果可以在单个for循环中迭代,每次Cars.ID更改时输出一个新Car。
-
@RyanGriggs是的,这是这个问题的已知解决方案之一。通过一些配置,你可以让Hibernate这样做。我相信其他ORM也有同等效力。
-
@tucuxi"你可以迭代结果"的程度取决于你正在使用的DBMS。 MySQL每个连接只允许一个活动迭代器;其余部分必须通过与DBMS的连接下载。
-
@Hans WHERE Id IN (const, const, ...)需要以下两种方法之一:DBMS客??户端库中的表值参数,或允许在运算符IN右侧转义字符串的代码复审策略,作为所有查询的一般规则的例外参数化。
-
这可以动态构造而不必担心字符串转义:WHERE Id IN(@ Id1,@ Id2,@ Id3)
-
那么在同一个实体上更新查询呢?如果我在单个事务中更新同一实体的一组对象,我可以做什么,orm层发出多个更新但我想要一个更新语句工作。那可行吗?怎么样?
-
@Tima对于JPA,你可以使用EntityGraph(docs.oracle.com/javaee/7/tutorial/)。
1 2 3 4
| SELECT
table1.*
, table2.*
INNER JOIN table2 ON table2.SomeFkId = table1.SomeId |
这会得到一个结果集,其中table2中的子行通过返回table2中每个子行的table1结果而导致重复。 O / R映射器应根据唯一键字段区分table1实例,然后使用所有table2列填充子实例。
1 2 3
| SELECT table1.*
SELECT table2.* WHERE SomeFkId = # |
N + 1是第一个查询填充主对象的位置,第二个查询填充返回的每个唯一主对象的所有子对象。
考虑:
1 2 3 4 5 6 7 8 9 10 11 12
| class House
{
int Id { get; set; }
string Address { get; set; }
Person[] Inhabitants { get; set; }
}
class Person
{
string Name { get; set; }
int HouseId { get; set; }
} |
和具有类似结构的表格。地址"22 Valley St"的单个查询可能会返回:
1 2 3 4
| Id Address Name HouseId
1 22 Valley St Dave 1
1 22 Valley St John 1
1 22 Valley St Mike 1 |
O / RM应该填充ID = 1,Address ="22 Valley St"的Home实例,然后用Dave,John和Mike的People实例填充Inhabitants数组,只需一个查询。
对上面使用的相同地址的N + 1查询将导致:
1 2
| Id Address
1 22 Valley St |
用一个单独的查询
1
| SELECT * FROM Person WHERE HouseId = 1 |
并产生一个单独的数据集
1 2 3 4
| Name HouseId
Dave 1
John 1
Mike 1 |
并且最终结果与单个查询的上述相同。
单一选择的优点是您可以预先获得所有数据,这可能是您最终想要的。 N + 1的优点是减少了查询复杂性,并且您可以使用延迟加载,其中子结果集仅在第一次请求时加载。
-
n + 1的另一个优点是它更快,因为数据库可以直接从索引返回结果。执行连接然后排序需要临时表,这比较慢。避免n + 1的唯一原因是你有很多延迟与你的数据库交谈。
-
加入和排序可以非常快(因为您将加入索引和可能排序的字段)。你的'n + 1'有多大?您是否认真地认为n + 1问题仅适用于高延迟数据库连接?
-
@tucuxi加入和排序很慢,因为你需要对来自两个不同表的列进行排序,而MySQL至少不支持这样的索引,所以它在临??时表中对它进行排序 - 这很慢。 (某些数据库确实有来自两个表的复合索引,这将有很大帮助。)
-
@tucuxi你也最终不必要地反复检索table1中的数据,如果你从中获取大量数据,你会浪费带宽,更糟糕的是你的临时表太大了,必须在磁盘上排序,然后做一个查询比n + 1慢几百万倍。在我的情况下,我从table1获取了大量数据,而table2有很多行,但实际上我在temp表上的磁盘空间用尽的数据非常少,因为笛卡儿产量巨大。切换到n + 1完全解决了这个问题。
-
@tucuxi在另一个评论中,你告诉我在一个查询中从table2获取所有数据。在这种情况下,table2非常大,我只使用数据的子集,只是数据与从table1获得的数据相匹配。但对于table1,我有一个非常大而复杂的where条件来获取我需要的数据。复制table2查询的位置会很慢。我可以提前收集table1中的所有ID,然后使用IN()发送它们,但是如果有很多ID,我会超过限制到查询的长度。
-
@ariel - 你的建议是N + 1是"最快的"是错误的,即使你的基准可能是正确的。怎么可能?请参阅en.wikipedia.org/wiki/Anecdotal_evidence,以及我对此问题的其他答案的评论。
-
@ Lee-Slalom这不是轶事 - 它是一个包括所有内容的分析,而不仅仅是关注数据库。我在评论中对此进行了解释,希望你理解它。
-
@Ariel - 我想我明白了:)我只是想指出你的结果只适用于一组条件。我可以很容易地构建一个显示相反的反例。那有意义吗?
-
@ Lee-Slalom这就是我想告诉你的:我的结果大部分时间都适用,而不仅仅是在一种情况下。数据库在排序时更快,然后您的应用程序就是。如果您没有在数据库中执行N + 1,那么您最终会在您的应用中执行此操作。每个人总是专注于不给数据库做很多工作,并忘记前端应用程序必须做多少工作。您应该避免N + 1的唯一时间是数据库查询是否有很多延迟,或者是否需要很长时间才能进行查询。另一个时候,如果数据库时间很宝贵,但你有很多应用服务器。
-
@Ariel通过使用临时id表stackoverflow.com/a/12927312/318174,我能够获得比N + 1和SELECT *笛卡尔产品风格更好的性能。我同意N + 1适用于大多数情况,但有时会有更好的选择,具体取决于您的应用程序和主机语言的性能(Java尽管人们说与PHP相比非常快)。
-
@AdamGent是的,这可以工作。关于它的一个好处是你可以索引临时表,只为索引支付一次,然后快速查询。 in子句本身不是索引的(并且您不能始终索引它比较的列)。索引条款的"另一面"有时可能很快。
-
@Ariel我完全同意你的看法。应用程序必须执行的"前端"工作才能对数据进行排序。我认为您应该尝试限制数据库查询,因为IMO您的应用程序和数据库应该是群集的,并且可能(并且大部分时间应该)不存在于同一台计算机上。随后这意味着每个查询都是网络呼叫(TCP设置拆除等)。所以恕我直言,你应该在一次通话中将工作卸载到数据库,告诉它你希望如何排序。数据库中没有黑魔法,它们仍然必须进行页面替换并以与应用程序相同的方式处理数据。
-
数据库中出现的"黑魔法"比许多人倾向于赞美的还要多。数据库服务器是功能强大的系统,由失去睡眠的工程师设计和工作,优化了您永远不会有时间并且可能永远不会开始在您的中间层应用程序代码中解决的微小性能问题。关于尝试从数据库服务器上卸载的大部分内容都很愚蠢。在您的应用程序中涉及的所有代码堆栈中,数据库服务器是真正设计用于执行和扩展的数据库服务器。
-
@Ariel在实践中我发现这个建议实际上是错误的建议。我强烈建议您查看其中一些"加入查询"的执行计划。数据库可以很好地处理连接,如果正确使用外键关系,连接将非常快速地执行。比单个N + 1查询的总和快得多。反复排队连接,运行查询和序列化结果将比连接更昂贵,并且在N值较大(如1000)的情况下,我们说的数量级更加昂贵。
-
我真的想强调的是,对于超过一小部分的N值,你真的会伤害N + 1的性能。即使有一个巨大的InnoDB缓冲池(想想数百场演出),重复访问数据库的时间和时间再次被证明是许多系统中的巨大瓶颈。 N + 1很有意义的时候是你有大量的实体/子项,否则它们将被包含在你的默认提取(join)中,但实际上只需要获取它们中的几个。如果你要取出所有这些,N + 1将会扼杀你的表现。将N + 1视为延迟加载。
-
@Dogs所有这些评论的要点是,与普通教条"Never N + 1"不同,实际上有时N + 1更快!是的,有时它会慢一些。它主要取决于您从父表中获取的数据量(即在返回的数据中重复多次)。你还必须记住,前端必须"解包"数据,并且它必须做N次!因此,您必须比较执行单个数据库查询和大量前端工作,以及多个数据库查询,但前端工作很少。不仅要查看数据库时间,还要检查前端时间。
-
@Ariel前端将以同样的方式完成同样的工作。你不是通过601个小查询而不是1个大查询来保存前端的任何东西。通常,序列化的"前端"(您应该更具体)的成本将是在数据库上连续运行许多查询的成本的一小部分。 N + 1更快的时间仅在您没有抓住所有孩子的时候。如果你有600个孩子并且你正在取3,那么选择fetch是可行的方法。如果您要获取所有600,那么加入fetch将会超过选择获取光年数
-
@Dogs这根本不是真的。假设您需要显示关于父级的10个字段,然后重复10次子字段。如果你有10个父母,那就是100行。这些字段中的900个是相同的重复(10个父字段)。这是一次性能损失。然后你的前端需要遍历所有行,并检查"这是否与以前一样父母?"相比之下,你可以做11个查询 - 所有父母一个,然后每个孩子10个。然后是前端排序,所有数据可能最终都在哈希表中,这对于前端处理更加有用。
-
@Dogs如果你有子子孩子,复制会变得更糟。数据库排序是一个更大的问题。当你有子子(即一个连接)时,你只能将1个列编入索引进行排序,其余列的排序方式很慢。我有一次尝试正确编写查询 - 1个查询,获取所有内容。我以50GB结束了!临时表作为数据库努力排序所有数据。然后我将其更改为N + 1并且它是即时的,尽管发出了大量查询,这是因为每个查询都很快并且可以使用索引排序列直接读取数据。
-
@Ariel再看看执行计划。如果连接需要永久但很多个人选择很快,那么您的数据库可能缺少索引。这与ORM无关,也与糟糕的数据库设计无关。此外,前端序列化将比等待池连接,通过网络以及让数据库执行数百次而不是一次执行查询便宜得多。在现实世界中,前端服务器水平扩展,而rdbms服务器则没有,我可以购买20台功能强大的前端服务器来支付我的数据库费用。
-
重申一下,SELECT N + 1问题的核心是:我有600条记录需要检索。在一个查询中获取所有600个查询更快,在600个查询中一次获得1个更快。除非您使用的是MyISAM和/或您的模式化程度很低/索引不佳(在这种情况下ORM不是问题),正确调整的数据库将在2 ms内返回600行,同时返回单个行每个约1毫秒。因此,我们经常看到N + 1花费数百毫秒,其中连接仅需要几个
与产品具有一对多关系的供应商。一个供应商拥有(供应)许多产品。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| ***** Table: Supplier *****
+-----+-------------------+
| ID | NAME |
+-----+-------------------+
| 1 | Supplier Name 1 |
| 2 | Supplier Name 2 |
| 3 | Supplier Name 3 |
| 4 | Supplier Name 4 |
+-----+-------------------+
***** Table: Product *****
+-----+-----------+--------------------+-------+------------+
| ID | NAME | DESCRIPTION | PRICE | SUPPLIERID |
+-----+-----------+--------------------+-------+------------+
|1 | Product 1 | Name for Product 1 | 2.0 | 1 |
|2 | Product 2 | Name for Product 2 | 22.0 | 1 |
|3 | Product 3 | Name for Product 3 | 30.0 | 2 |
|4 | Product 4 | Name for Product 4 | 7.0 | 3 |
+-----+-----------+--------------------+-------+------------+ |
影响因素:
获取模式为Select Fetch(默认)
1 2 3 4 5 6 7 8 9 10
| // It takes Select fetch mode as a default
Query query = session.createQuery("from Product p");
List list = query.list();
// Supplier is being accessed
displayProductsListWithSupplierName(results);
select ... various field names ... from PRODUCT
select ... various field names ... from SUPPLIER where SUPPLIER.id=?
select ... various field names ... from SUPPLIER where SUPPLIER.id=?
select ... various field names ... from SUPPLIER where SUPPLIER.id=? |
结果:
这是N + 1选择问题!
-
是否应该为供应商选择1,然后N选择产品?
-
@bencampbell_是的,最初我感觉一样。但随后以他的例子,它是许多供应商的一种产品。
我不能直接评论其他答案,因为我没有足够的声誉。但值得注意的是,问题基本上只会产生,因为从历史上看,许多dbms在处理连接方面都很差(MySQL是一个特别值得注意的例子)。所以n + 1通常比连接快得多。然后有一些方法可以改进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的糟糕表现而过去一直在避免加入,那么再试一次最新版本。你可能会感到惊喜。
-
将早期版本的MySQL称为关系型DBMS是一个很大的延伸......如果遇到这些问题的人一直在使用真正的数据库,他们就不会遇到这类问题。 ;-)
-
有趣的是,随着INNODB引擎的引入和后续优化,许多这些类型的问题在MySQL中得到了解决,但是你仍然会遇到试图推广MYISAM的人,因为他们认为它更快。
-
仅供参考,RDBMS中使用的3种常见JOIN算法之一称为嵌套循环。它从根本上说是引擎盖下的N + 1选择。唯一的区别是数据库做出了明智的选择,可以根据统计数据和索引使用它,而不是客户端代码明确地强制它沿着这条路径。
-
@Brandon是的!很像JOIN提示和INDEX提示,在所有情况下强制某个执行路径很少会超过数据库。数据库几乎总是非常非常擅长选择获取数据的最佳方法。也许在dbs的早期阶段,你需要以一种特殊的方式"扼杀"你的问题来哄骗数据库,但经过几十年的世界级工程,你现在可以通过询问你的数据库关系问题并让它获得最佳性能理清如何为您提取和组装数据。
-
数据库不仅利用索引和统计数据,而且所有操作都是本地I / O,其中大部分操作通常是针对高效缓存而不是磁盘。数据库程序员非常注重优化这些事情。
由于这个问题,我们离开了Django的ORM。基本上,如果你尝试做
1 2
| for p in person:
print p.car.colour |
ORM将很乐意返回所有人(通常作为Person对象的实例),但随后它将需要查询每个Person的car表。
一种简单而有效的方法是我称之为"粉丝折叠",它避免了一种荒谬的想法,即来自关系数据库的查询结果应该映射回构成查询的原始表。
第1步:广泛选择
1
| select * from people_car_colour; # this is a view or sql function |
这将返回类似的东西
1 2 3 4 5
| 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步:渲染
1 2
| for p in people:
print p.car.colour # no more car queries |
有关python的fanfolding的实现,请参阅此网页。
-
我很高兴我偶然发现了你的帖子,因为我以为我疯了。当我发现N + 1问题时,我的直接想法是 - 好吧,为什么不创建一个包含所需信息的视图,并从该视图中拉出来?你已经验证了我的立场。谢谢你,先生。
-
由于这个问题,我们离开了Django的ORM。咦? Django有select_related,这是为了解决这个问题 - 实际上,它的文档以类似于p.car.colour示例的示例开头。
-
这是一个旧的anwswer,我们现在在Django中有select_related()和prefetch_related()。
-
凉。但是select_related()和朋友似乎没有进行任何明显有用的连接推断,例如LEFT OUTER JOIN。问题不是接口问题,而是与对象和关系数据可映射的奇怪想法有关的问题....在我看来。
假设您有公司和员工。公司有许多员工(即员工有一个字段COMPANY_ID)。
在一些O / R配置中,当你有一个映射的Company对象并且去访问它的Employee对象时,O / R工具会为每个员工做一个选择,如果你只是在直接SQL中做事,你可以。因此N(员工人数)加1(公司)
这就是EJB Entity Beans的初始版本的工作方式。我相信像Hibernate这样的东西已经废除了这个,但我不太大多数工具通常都包含有关其映射策略的信息。
这是对问题的一个很好的描述 - https://web.archive.org/web/20160310145416/http://www.realsolve.co.uk/site/tech/hib-tip-pitfall.php?name=why-懒
现在您已经了解了这个问题,通常可以通过在查询中进行连接提取来避免它。这基本上强制获取延迟加载的对象,因此在一个查询中检索数据而不是n + 1个查询。希望这可以帮助。
查看关于主题的Ayende帖子:在NHibernate中解决选择N + 1问题
基本上,当使用像NHibernate或EntityFramework这样的ORM时,如果你有一对多(主 - 细节)关系,并希望列出每个主记录的所有细节,你必须对N + 1查询调用数据库,"N"是主记录的数量:1个查询获取所有主记录,N个查询(每个主记录一个)获取每个主记录的所有详细信息。
更多数据库查询调用 - >更多延迟时间 - >降低应用程序/数据库性能。
但是,ORM可以选择避免这个问题,主要是使用"连接"。
-
连接(通常)不是一个好的解决方案,因为它们可能会产生笛卡尔积,这意味着结果行的数量是根表结果的数量乘以每个子表中的结果数。在多个层次级别上尤其糟糕。选择20个"博客",每个帖子上有100个"帖子",每个帖子上有10个"评论",将产生20000个结果行。 NHibernate有一些变通方法,比如"批量大小"(在父ID上选择带子句的子项)或"子选择"。
当您忘记获取关联然后需要访问它时,会发生N + 1查询问题:
1 2 3 4 5 6 7 8 9 10 11 12
| List<PostComment> comments = entityManager.createQuery(
"select pc" +
"from PostComment pc" +
"where pc.review = :review", PostComment.class)
.setParameter("review", review)
.getResultList();
LOGGER.info("Loaded {} comments", comments.size());
for(PostComment comment : comments) {
LOGGER.info("The post title is '{}'", comment.getPost().getTitle());
} |
这会生成以下SQL语句:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| SELECT pc.id AS id1_1_, pc.post_id AS post_id3_1_, pc.review AS review2_1_
FROM post_comment pc
WHERE pc.review = 'Excellent!'
INFO - Loaded 3 comments
SELECT pc.id AS id1_0_0_, pc.title AS title2_0_0_
FROM post pc
WHERE pc.id = 1
INFO - The post title is 'Post nr. 1'
SELECT pc.id AS id1_0_0_, pc.title AS title2_0_0_
FROM post pc
WHERE pc.id = 2
INFO - The post title is 'Post nr. 2'
SELECT pc.id AS id1_0_0_, pc.title AS title2_0_0_
FROM post pc
WHERE pc.id = 3
INFO - The post title is 'Post nr. 3' |
首先,Hibernate执行JPQL查询,并获取PostComment实体列表。
然后,对于每个PostComment,关联的post属性用于生成包含post标题的日志消息。
因为post关联未初始化,所以Hibernate必须使用辅助查询获取post实体,并且
对于N PostComment个实体,将执行N个更多查询(因此N + 1查询问题)。
首先,您需要正确的SQL日志记录和监视,以便您可以发现此问题。
其次,这种问题最好是通过集成测试来捕获。您可以使用自动JUnit断言来验证生成的SQL语句的预期计数。 db-unit项目已经提供了这个功能,它是开源的。
当您确定N + 1查询问题时,需要使用JOIN FETCH以便在一个查询中提取子关联,而不是N.如果需要获取多个子关联,最好在初始查询中获取一个集合第二个带有辅助SQL查询。
-
但现在你有分页问题。 如果你有10辆汽车,每辆车有4个轮子你想要每页5辆车分页。 所以你基本上有SELECT cars, wheels FROM cars JOIN wheels LIMIT 0, 5。 但你得到的是2辆5轮车(第一辆车全部4轮,第二辆车只有1轮),因为LIMIT将限制整个结果集,而不仅仅是根条款。
-
我也有一篇文章。
-
谢谢你的文章。 我会读它。 通过快速滚动 - 我看到该解决方案是Window Function,但它们在MariaDB中相当新 - 所以问题在旧版本中仍然存在。:)
在我看来,用Hibernate陷阱写的文章:为什么关系应该是懒惰的,与真正的N + 1问题正好相反。
如果您需要正确的解释,请参阅Hibernate - 第19章:提高性能 - 获取策略
Select fetching (the default) is
extremely vulnerable to N+1 selects
problems, so we might want to enable
join fetching
-
我读了休眠页面。它没有说明N + 1实际选择的问题是什么。但它说你可以使用连接来修复它。
-
select-size是select select,在一个select语句中为多个父项选择子对象。子选择可能是另一种选择。如果您有多个层次结构级别并且创建了笛卡尔积,则联接可能会非常糟糕。
提供的链接有一个非常简单的n + 1问题示例。如果你将它应用于Hibernate,它基本上是在谈论同样的事情。查询对象时,将加载实体,但任何关联(除非另外配置)都将延迟加载。因此,一个查询根对象,另一个查询加载每个对象的关联。返回100个对象意味着一个初始查询,然后100个额外的查询以获得每个n + 1的关联。
http://pramatr.com/2009/02/05/sql-n-1-selects-explained/
一位百万富翁有N辆车。你想得到所有(4)轮子。
一(1)个查询加载所有汽车,但是对于每个(N)汽车,提交单独的查询以加载车轮。
成本:
假设索引符合ram。
1 + N查询解析和规划+索引搜索和1 + N +(N * 4)板访问以加载有效载荷。
假设索引不适合ram。
最坏情况下的额外成本1 + N板加载索引。
摘要
瓶颈是板通道(在硬盘上每秒约70次随机访问)
对于有效载荷,急切的连接选择也将访问板1 + N +(N * 4)次。
因此,如果索引适合ram - 没问题,它的速度足够快,因为只涉及ram操作。
发出1个查询返回100个结果比发出100个查询每个返回1个结果要快得多。
N + 1选择问题很痛苦,在单元测试中检测这种情况是有意义的。
我开发了一个小型库,用于验证给定测试方法或任意代码块执行的查询数量 - JDBC Sniffer
只需在测试类中添加一个特殊的JUnit规则,并在测试方法上放置具有预期查询数量的注释:
1 2 3 4 5 6 7 8
| @Rule
public final QueryCounter queryCounter = new QueryCounter();
@Expectation(atMost = 3)
@Test
public void testInvokingDatabase() {
// your JDBC or JPA code
} |
正如其他人所说的更优雅的问题是,您要么拥有OneToMany列的笛卡尔积,要么您正在进行N + 1选择。无论是可能的巨大结果集还是分别与数据库聊天。
我很惊讶这没有被提及,但这是我如何解决这个问题...我做了一个半临时的id表。当你有IN ()子句限制时,我也会这样做。
这并不适用于所有情况(可能甚至不是大多数情况)但如果你有很多子对象使得笛卡尔积会失控(即大量的OneToMany列)结果数量,它的效果特别好将是一个列的乘法)和它更多的批处理作业。
首先,将父对象ID作为批处理插入到ids表中。
这个batch_id是我们在应用程序中生成并保留的内容。
1 2 3 4 5
| INSERT INTO temp_ids
(product_id, batch_id)
(SELECT p.product_id, ?
FROM product p ORDER BY p.product_id
LIMIT ? OFFSET ?); |
现在,对于每个OneToMany列,只需在子表INNER JOIN上使用WHERE batch_id=执行SELECT(反之亦然)。您只是想确保按id列进行排序,因为它会使合并结果列更容易(否则您将需要一个HashMap / Table用于整个结果集,这可能不是那么糟糕)。
然后你只需要定期清理ids表。
如果用户为某种批量处理选择说100个左右的不同项目,这也特别有效。将100个不同的ID放在临时表中。
现在,您正在执行的查询数量取决于OneToMany列的数量。
以Matt Solnit为例,假设您将Car和Wheels之间的关联定义为LAZY并且您需要一些Wheels字段。这意味着在第一次选择之后,休眠将执行"从车轮中选择* car_id =:id"FOR FOR EACH Car。
这使得第一个选择和每个N车选择更多1,这就是为什么它被称为n + 1问题。
为避免这种情况,请将关联提取视为急切,以便hibernate通过连接加载数据。
但是注意,如果很多次你没有访问相关的车轮,最好保持LAZY或用Criteria更改提取类型。
-
同样,连接不是一个好的解决方案,特别是当可以加载超过2个层次结构级别时。改为检查"subselect"或"batch-size";最后一个将通过"in"子句中的父ID加载子项,例如"select ... from wheel where car_id in(1,3,4,6,7,8,11,13)"。