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


SELECT N + 1通常被称为对象 - 关系映射(ORM)讨论中的一个问题,我理解它必须为对象世界中看起来很简单的事情进行大量数据库查询。

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


1299
2017-09-18 21:30


起源


IMO javalobby.org/java/forums/t20533.html 这种解释更好 - didxga
这是一个很好的链接,很好地解释了解 n + 1个 问题。它还涵盖了解决此问题的解决方案: architects.dzone.com/articles/how-identify-and-resilve-n1 - aces.
有一些有用的帖子谈论这个问题和可能的解决方案。 常见的应用问题及其解决方法:选择N + 1问题, N + 1问题的(银)子弹, 延迟加载 - 急切加载 - cateyes


答案:


假设你有一个集合 Car 对象(数据库行)和每个 Car 有一个集合 Wheel 对象(也是行)。换一种说法, Car  - > Wheel 是1对多的关系。

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

SELECT * FROM Cars;

接着 为每个人 Car

SELECT * FROM Wheel WHERE CarId = ?

换句话说,您有一个选择汽车,然后N个额外选择,其中N是汽车总数。

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

SELECT * FROM Wheel

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

参考: 使用Hibernate的Java持久性,第13章。


768
2017-09-18 21:36



澄清“这是坏事” - 你可以通过1选择获得所有车轮(SELECT * from Wheel;),而不是N + 1。如果N值很大,性能损失可能会非常显着。 - tucuxi
@tucuxi我很惊讶你错了很多赞成票。数据库非常适合索引,对特定CarID的查询返回速度非常快。但是如果你所有的Wheels都是一次,你将不得不在你的应用程序中搜索CarID,它没有编入索引,这个速度较慢。除非您的数据库存在严重的延迟问题,否则n + 1实际上更快 - 是的,我使用各种各样的真实代码对其进行基准测试。 - Ariel
@ariel'正确'的方式是得到 所有 轮子,由CarId(1选择)排序,如果需要比CarId更多的细节,请进行第二次查询 所有 汽车(总共2个查询)。打印出来的东西现在是最佳的,不需要索引或二级存储(您可以迭代结果,无需全部下载)。你对错误的东西进行了基准测试如果您对基准测试仍有信心,您是否介意发布更长的评论(或完整答案)来解释您的实验和结果? - tucuxi
“Hibernate(我不熟悉其他ORM框架)为您提供了几种处理它的方法。”这些方式是? - Tima
@Ariel尝试在不同的计算机上运行数据库和应用程序服务器的基准测试。根据我的经验,往返数据库的成本比查询本身花费更多。所以,是的,查询真的很快,但这是匆匆忙忙的往返旅行。我转换了“WHERE Id = 常量“到”在哪里(常量, 常量,......)“并且从中获得了数量级的增加。 - Hans


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

通过返回table2中每个子行的table1结果,可以获得结果集,其中table2中的子行导致重复。 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,Address =“22 Valley St”的Home实例,然后用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的优点是减少了查询复杂性,并且您可以使用延迟加载,其中子结果集仅在第一次请求时加载。


98
2017-09-18 21:43



n + 1的另一个优点是它更快,因为数据库可以直接从索引返回结果。进行连接然后排序需要一个较慢的临时表。避免n + 1的唯一原因是如果你有很多延迟与你的数据库交谈。 - Ariel
加入和排序可以非常快(因为您将加入索引和可能排序的字段)。你的'n + 1'有多大?您是否认真地认为n + 1问题仅适用于高延迟数据库连接? - tucuxi
@ariel - 你的建议是N + 1是“最快的”是错误的,即使你的基准可能是正确的。怎么可能?看到 en.wikipedia.org/wiki/Anecdotal_evidence,以及我在这个问题的另一个答案中的评论。 - Lee
@Ariel - 我想我明白了:)。我只是想指出你的结果只适用于一组条件。我可以很容易地构建一个显示相反的反例。那有意义吗? - Lee
重申一下,SELECT N + 1问题的核心是:我有600条记录需要检索。在一个查询中获取所有600个查询更快,在600个查询中一次获得1个更快。除非您使用MyISAM和/或您的模式化程度很低/索引不佳(在这种情况下ORM不是问题),正确调整的数据库将在2毫秒内返回600行,同时返回单个行每个约1毫秒。所以我们经常看到N + 1花费数百毫秒,其中连接只需要几个 - Dogs


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

***** 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”(默认)

  • 用于在Product上查询的获取模式是Select

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

  • 缓存第一次没有发挥作用

  • 访问供应商

获取模式为Select Fetch(默认)

// 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选择产品声明
  • N为供应商选择报表

这是N + 1选择问题!


58
2017-12-01 13:35



是否应该为供应商选择1,然后N选择产品? - bencampbell_14


我不能直接评论其他答案,因为我没有足够的声誉。但值得注意的是,问题基本上只会产生,因为从历史上看,许多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的糟糕表现而过去一直在避免加入,那么再试一次最新版本。你可能会感到惊喜。


33
2018-01-08 12:49



将早期版本的MySQL称为关系型DBMS是一个很大的延伸......如果遇到这些问题的人一直在使用真正的数据库,他们就不会遇到这类问题。 ;-) - Craig
有趣的是,随着INNODB引擎的引入和后续优化,许多这些类型的问题在MySQL中得到了解决,但是你仍然会遇到试图推广MYISAM的人,因为他们认为它更快。 - Craig
仅供参考,3个常见之一 JOIN RDBMS中使用的算法称为嵌套循环。它从根本上说是引擎盖下的N + 1选择。唯一的区别是数据库做出了明智的选择,可以根据统计数据和索引使用它,而不是客户端代码明确强制它沿着这条路径。 - Brandon
@Brandon是的!与JOIN提示和INDEX提示非常相似,在所有情况下强制某个执行路径很少会超过数据库。数据库几乎总是非常非常擅长选择获取数据的最佳方法。也许在dbs的早期你需要以一种特殊的方式“扼杀”你的问题来哄骗数据库,但经过几十年的世界级工程,你现在可以通过向数据库询问关系问题并让它获得最佳性能理清如何为您提取和组装数据。 - Dogs
数据库不仅利用索引和统计数据,而且所有操作都是本地I / O,其中大部分操作通常是针对高效缓存而不是磁盘。数据库程序员非常注重优化这些事情。 - Craig


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

for p in person:
    print p.car.colour

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

一个简单而有效的方法是我称之为“fanfolding“,这避免了无意义的想法,即来自关系数据库的查询结果应该映射回构成查询的原始表。

第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

看到 这个网页 执行 fanfolding 对于python。


25
2018-06-09 21:18



我很高兴我偶然发现了你的帖子,因为我以为我疯了。当我发现N + 1问题时,我的直接想法是 - 好吧,为什么不创建一个包含您需要的所有信息的视图,并从该视图中拉出来?你已经验证了我的立场。谢谢你,先生。 - a developer
由于这个问题,我们离开了Django的ORM。咦? Django有 select_related,这是为了解决这个问题 - 实际上,它的文档以类似于你的例子开头 p.car.colour 例。 - Adrian17
这是旧的anwswer,我们有 select_related() 和 prefetch_related() 现在在Django。 - Mariusz Jamro


假设您有公司和员工。公司有许多员工(即员工有一个字段COMPANY_ID)。

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

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


16
2017-09-18 21:33





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

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


13
2017-09-18 21:43