什么是N + 1 SELECT查询问题?

在对象关系映射(ORM)讨论中,SELECT N + 1通常被认为是一个问题,而且我明白,为了在对象世界中看起来很简单的事情做大量的数据库查询,有一些事情要做。

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

我不是专家,最好的指南是Java Persistence with Hibernate ,第13章。但我可以试着举个简短的例子。

假设您有一个Car对象(数据库行)的集合,并且每个Car都有一个Wheel对象(数据库行)的集合。 换句话说,Car:Wheel是一对多的关系。

现在,假设您需要遍历所有车辆,并为每个车辆打印车轮清单。 幼稚的O / R实施将执行以下操作:

SELECT * FROM Cars; 

然后对于每辆车:

 SELECT * FROM Wheel WHERE CarId = ? 

换句话说,你有一个汽车select,然后N个额外的select,其中N是汽车总数。

或者,可以让所有的车轮在内存中执行查找:

 SELECT * FROM Wheel 

这减less了从N + 1到2的往返数据库的次数。

大多数ORM工具为您提供了几种防止N + 1select的方法。

 SELECT table1.* , table2.* INNER JOIN table2 ON table2.SomeFkId = table1.SomeId 

这会得到一个结果集,其中table2中的子行通过返回table2中每个子行的table1结果而导致重复。 O / R映射器应根据唯一键字段区分table1实例,然后使用所有table2列来填充子实例。

 SELECT table1.* SELECT table2.* WHERE SomeFkId = # 

N + 1是第一个查询填充主对象的位置,第二个查询填充返回的每个唯一主对象的所有子对象。

考虑:

 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”的单个查询可以返回:

 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的Home,Address =“22 Valley St”的实例,然后用一个查询填充Dave,John和Mike的People实例的Inhabitants数组。

针对上面使用的相同地址的N + 1查询将导致:

 Id Address 1 22 Valley St 

与一个单独的查询一样

 SELECT * FROM Person WHERE HouseId = 1 

并导致一个单独的数据集

 Name HouseId Dave 1 John 1 Mike 1 

最后的结果与上面的单个查询相同。

单选的优点是你可以预先得到所有的数据,这可能是你最终的愿望。 N + 1的优点是查询复杂度降低,您可以使用延迟加载,只在第一次请求时加载子结果集。

与产品具有一对多关系的供应商。 一个供应商有(供应)许多产品。

 ***** 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 | +-----+-----------+--------------------+-------+------------+ 

影响因素:

  • 供应商的懒惰模式设置为“true”(默认)

  • 用于查询产品的提取模式是select

  • 获取模式(默认):访问供应商信息

  • 第一次caching不起作用

  • 供应商被访问

提取模式是select提取(默认)

 // 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=? 

结果:

  • 1为产品select语句
  • N为供应商select报表

这是N + 1select题!

我不能直接评论其他答案,因为我没有足够的声望。 但是值得注意的是,这个问题实质上只是因为在处理联接(MySQL是一个特别值得注意的例子)时,很多dbms在处理连接方面一直很差。 所以n + 1往往比连接速度快得多。 然后有一些方法可以改进n + 1,但仍然不需要join,这就是原来的问题。

但是,现在MySQL比以前的联接要好很多。 当我第一次学习MySQL时,我使用了很多。 然后我发现它们有多慢,而代码中转换为n + 1。 但是,最近,我一直在回头join,因为MySQL现在比我刚开始使用它时好得多。

现在,在性能方面,对正确索引的表集进行简单连接就不是什么问题。 如果它确实给予了性能的提升,那么使用索引提示经常可以解决它们。

这是由MySQL开发团队之一在这里讨论的:

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

所以总结是:如果你因为MySQL的糟糕performance而避免了过去的join,那就试试最新的版本吧。 你可能会感到惊喜。

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

 for p in person: print p.car.colour 

ORM将愉快地返回所有的人(典型地作为Person对象的实例),但是然后将需要为每个Person查询车表。

一个简单且非常有效的方法就是我所说的“ fanfolding ”,它避免了从关系数据库查询结果映射回查询所组成的原始表的荒谬想法。

第1步:宽select

  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:物化

将结果抽取到具有参数的通用对象创build器中,以便在第三项之后进行分割。 这意味着“琼斯”对象不会被制作一次以上。

第3步:渲染

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

看到这个网页的python实现fanfolding

假设你有COMPANY和EMPLOYEE。 公司有很多员工(即员工有一个COMPANY_ID字段)。

在一些O / Rconfiguration中,当你有一个映射的Company对象并且去访问它的Employee对象时,O / R工具将为每个员工做一个select,如果你只是用直接的SQL做事情,你可以select * from employees where company_id = XX 。 因此N(雇员人数)加1(公司)

这是EJB Entity Beans的初始版本的工作方式。 我相信像Hibernate这样的东西已经消除了这一点,但我不太确定。 大多数工具通常包含有关其映射策略的信息。

这是一个很好的问题描述 – http://www.realsolve.co.uk/site/tech/hib-tip-pitfall.php?name=why-lazy

现在,您已经了解了这个问题,通常可以通过在查询中进行联合提取来避免此问题。 这基本上强制延迟加载对象的提取,以便在一个查询中检索数据,而不是n + 1个查询。 希望这可以帮助。

在我看来写在Hibernate陷阱的文章:为什么关系应该是懒惰的是真正的N + 1问题是完全相反的。

如果你需要正确的解释,请参考Hibernate – 第19章:提高性能 – 获取策略

select读取(默认)非常容易受到N + 1select问题的困扰,所以我们可能想要启用连接读取

在主题上检查Ayendepost: 在NHibernate中打击selectN + 1问题

基本上,当使用像NHibernate或EntityFramework这样的ORM时,如果你有一对多(master-detail)关系,并且想要列出每个主logging的所有细节,你必须对数据库,“N”是主logging的数量:1查询获得所有的主logging,和N查询,每个主logging一个,获得每个主logging的所有细节。

更多的数据库查询调用 – >更多的延迟时间 – >应用程序/数据库性能下降。

但是,ORM可以select避免这个问题,主要使用“连接”。

提供的链接有一个非常简单的例子,即n + 1问题。 如果你把它应用到Hibernate,它基本上是在谈论同样的事情。 当你查询一个对象时,实体被加载,但任何关联(除非另有configuration)将被延迟加载。 因此,一个查询的根对象和另一个查询加载每个这些关联。 返回的100个对象意味着一个初始查询,然后是100个额外的查询来获得每个n + 1的关联。

http://pramatr.com/2009/02/05/sql-n-1-selects-explained/

发出返回100个结果的1个查询要比发出100个每个返回1个结果的查询要快得多。

一个百万富翁有N辆车。 你想得到所有(4)车轮。

一(1)个查询加载所有车辆,但是对于每个(N)车辆提交单独的查询以装载车轮。

成本:

假设索引符合公羊。

1 + N查询parsing和计划+索引search和1 + N +(N * 4)板载访问来加载有效载荷。

假设索引不符合公羊。

在最坏的情况下,额外的成本为加载指数1 + N板访问。

概要

瓶颈是板的访问(在硬盘上每秒约70次随机访问)一个急切的joinselect也将访问有效载荷板1 + N +(N * 4)次。 所以如果这些指标符合公羊 – 没有问题,它的速度足够快,因为只涉及公羊操作。

与其他人一样,问题的优雅之处在于,您可能拥有OneToMany列的笛卡尔积,或者您正在进行N + 1个select。 可能是巨大的结果集,或分别与数据库聊天。

我很惊讶,这是没有提到,但是这是我如何解决这个问题… 我做了一个半暂时的IDS表 。 当你有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列,您只需在ids表上执行一个SELECT INNER JOIN子表WHERE batch_id= (反之亦然)。 你只是想确保你的id列的顺序,因为它会使合并结果列更容易(否则你将需要一个HashMap /表的整个结果集可能不是那么糟糕)。

然后你只需定期清理ID表。

如果用户select100个左右不同的项目用于某种批量处理,这也特别有效。 将100个不同的ID放在临时表中。

现在你正在做的查询的数量是OneToMany列的数量。

N + 1select问题是一种痛苦,在unit testing中检测这种情况是有意义的。 我开发了一个小型库来validation由给定的testing方法执行的查询数量,或者只是一个任意的代码块 – JDBC Sniffer

只需在testing类中添加一个特殊的JUnit规则,并在您的testing方法中添加预期数量的查询即可:

 @Rule public final QueryCounter queryCounter = new QueryCounter(); @Expectation(atMost = 3) @Test public void testInvokingDatabase() { // your JDBC or JPA code } 

当您忘记获取关联,然后您需要访问它时,就会发生N + 1查询问题:

 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语句:

 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日志logging和监视,以便您可以发现此问题。

其次,这样的问题最好是被集成testing抓住。 您可以使用自动JUnit声明来validation生成的SQL语句的预期计数 。 db-unit项目已经提供了这个function,而且它是开源的。

当您确定N + 1查询问题时, 您需要使用JOIN FETCH,以便在一个查询中获取子关联,而不是N。 如果您需要获取多个子关联,则最好在初始查询中获取一个集合,使用辅助SQL查询获取第二个集合。

以Matt Solnit为例,假设你将汽车和车轮之间的联系定义为“懒惰”,并且你需要一些“轮子”字段。 这意味着,第一次select后,hibernate将做“从车轮select* car_id =:id”为每辆车。

这使得每个N车select第一个和多个1,这就是为什么它被称为n + 1问题。

为了避免这种情况,请将关联提取为渴望,以便hibernate使用连接加载数据。

但是要注意,如果很多时候你不访问相关的车轮,最好是保持懒惰或改变与标准的获取types。