当前位置:  开发笔记 > 编程语言 > 正文

ORM(对象关系映射)中的"N + 1选择问题"是什么?

如何解决《ORM(对象关系映射)中的"N+1选择问题"是什么?》经验,为你挑选了15个好方法。

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

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



1> Matt Solnit..:

假设您有一组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选择的方法.

参考:Java Persistence with Hibernate,第13章.


@tucuxi我很惊讶你错了很多赞成票.数据库非常适合索引,对特定CarID执行查询会非常快速地返回.但是如果你所有的Wheels都是一次,你将不得不在你的应用程序中搜索CarID,它没有被编入索引,这个速度较慢.除非你有很长的延迟问题到达你的数据库,否则n + 1实际上更快 - 是的,我用各种各样的真实代码对它进行基准测试.
澄清"这是坏事" - 你可以通过1选择(`SELECT*from Wheel;`)而不是N + 1来获得所有轮子.如果N值很大,性能损失可能会非常显着.
"Hibernate(我不熟悉其他ORM框架)为您提供了几种处理它的方法." 这些方式是?
@ariel'正确'的方法是获得*all*轮子,按CarId(1选择)排序,如果需要比CarId更多的细节,则对*all*cars进行第二次查询(总共2次查询).打印出来的东西现在是最佳的,不需要索引或二级存储(您可以迭代结果,无需全部下载).你对错误的东西进行了基准测试 如果您对基准仍有信心,您是否介意发布更长的评论(或完整答案)来解释您的实验和结果?
@Ariel尝试在不同的计算机上运行数据库和应用程序服务器的基准测试.根据我的经验,往返数据库的开销比查询本身花费更多.所以,是的,查询真的很快,但这是匆匆忙忙的往返旅行.我已经将"WHERE Id =*const*"转换为"WHERE Id IN(*const*,*const*,...)"并从中获得了数量级的增加.
`选择汽车.*,轮*,从汽车内部联接车轮上Cars.CarID = Wheels.CardID为了通过Cars.CardID,Wheels.WheelID`是获取此信息假设你的表是正确索引的最有效的方式.如果需要限制,请在其中加入WHERE子句.所有现代ORM值得使用这样的查询.如果有多个选择所比单个join快那么有什么问题你的数据库设计,期.
@ariel - 说天真的方法是"正确的"是不对的.如果说某些情况下天真的方法可以表现得更好,那就更准确了.一般来说,1个查询优于n.请参阅big-o表示法.虽然O(1)是比O(N)更好的算法,但是存在恒定时间方法不是最快的实际情况.
@Hans我在回复中多次说过.也许你撇去并错过了它.如果您的查询有延迟,则情况会发生变化.但至少对于典型的web-dev工作,数据库通常位于同一台机器上.
@tucuxi我对加入轮子和汽车进行了基准测试(并在应用程序代码中进行了重复数据删除),这确实比较慢 - 慢得多.将它们作为两个查询并通过第一个查询对第二个查询进行排序要好得多.这个页面上的其他答案并没有说这样做,但他们谈论加入.如果第一个查询只得到行的一个子集,你需要更复杂的查询第二个限制,你有多少数据获取所以它不一定是最好的方式,但它可以,那就需要进行检查的具体情况.
@Basic你在开玩笑吗?你想要返回所有轮子??? 如果您只需要其中一些怎么办?
@ Lee-Slalom它可能是1个数据库查询 - 但是你必须使用应用程序中的数据,而你的应用程序比数据库慢.因此,您对O()的分析过于简单 - 您只需要包含数据库中的工作,而忽略应用程序必须执行的操作.
有许多影响因素决定了连接(一个选择)是否快于两个或N + 1选择.我认为在正确的设置中,连接应该是最快的,并且具有N + 1选择的版本应该是最慢的.但是,YMMV.
然后......`在哪里Car.Id IN(选择来自汽车的地方......)`?我并不是说你应该总是将最好的操作卸载到应用程序的Db.我说有时候这种权衡是值得的.
@Stephen Byrne:不一定.使用Joins,您最常发送冗余数据(另请参阅笛卡尔积的x-Joins).如果`Cars.*`包含大量数据,那么两个单一查询可以更快并节省资源.
你为什么不说`select * from carid in(id1,id2 ...)的轮子
@mickeymoon是因为当您使用延迟加载来访问子集合时,SELECT是由NHibernate生成的,而NHibernate不会像这样生成SELECT。

2> cfeduke..:
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,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的优点是减少了查询复杂性,并且您可以使用延迟加载,其中子结果集仅在第一次请求时加载.


加入和排序可以非常快(因为您将加入索引和可能排序的字段).你的'n + 1'有多大?您是否认真地认为n + 1问题仅适用于高延迟数据库连接?
重申一下,SELECT N + 1问题的核心是:我有600条记录需要检索.在一个查询中获取所有600个查询更快,在600个查询中一次获得1个更快.除非您使用的是MyISAM和/或您的模式化程度很低/索引不佳(在这种情况下ORM不是问题),正确调整的数据库将在2 ms内返回600行,同时返回单个行每个约1毫秒.因此,我们经常看到N + 1花费数百毫秒,其中连接仅需要几个
@ariel - 你的建议是N + 1是"最快的"是错误的,即使你的基准可能是正确的.怎么可能?请参阅http://en.wikipedia.org/wiki/Anecdotal_evidence,以及我在此问题的其他答案中的评论.
@Ariel - 我想我明白了:) 我只是想指出你的结果只适用于一组条件.我可以很容易地构建一个显示相反的反例.那有意义吗?
n + 1的另一个优点是它更快,因为数据库可以直接从索引返回结果.执行连接然后排序需要临时表,这比较慢.避免n + 1的唯一原因是你有很多延迟与你的数据库交谈.
@Ariel再看看执行计划.如果连接需要永久但很多个人选择很快,那么您的数据库可能缺少索引.这与ORM无关,也与糟糕的数据库设计无关.此外,前端序列化将比等待池连接,通过网络以及让数据库执行数百次而不是一次执行查询便宜得多.在现实世界中,前端服务器水平扩展,而rdbms服务器则没有,我可以购买20台功能强大的前端服务器,以支付我的数据库成本.

3> 小智..:

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

***** 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选择问题!


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

4> 小智..:

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

5> rorycl..:

由于这个问题,我们离开了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 的fanfolding的实现,请参阅此网页.


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

6> davetron5000..:

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

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

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



7> Joe Dean..:

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

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



8> Vlad Mihalce..:

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

List 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日志记录和监视,以便您可以发现此问题.

其次,这种问题最好是通过集成测试来捕获.您可以使用自动JUnit断言来验证生成的SQL语句的预期计数.在DB-单元项目已经提供了这个功能,并且它是开源的.

当你确定了N + 1查询问题,你需要使用一个连接抓取,这样子关联是在一个查询中获取,代替N.如果需要获取多个子关联,最好在初始查询中获取一个集合,而在第二个SQL查询中获取第二个集合.


我有[一篇文章](https://vladmihalcea.com/2017/09/13/the-best-way-to-fix-the-hibernate-hhh000104-firstresultmaxresults-specified-with-collection-fetch-applying-in -memory-warning-message /)也是如此.

9> Nathan..:

查看关于主题的Ayende帖子:在NHibernate中解决选择N + 1问题

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

更多数据库查询调用 - >更多延迟时间 - >降低应用程序/数据库性能.

但是,ORM可以选择避免这个问题,主要是使用"连接".


连接(通常)不是一个好的解决方案,因为它们可能会产生笛卡尔积,这意味着结果行的数量是根表结果的数量乘以每个子表中的结果数.在多个层次级别上尤其糟糕.选择20个"博客",每个帖子上有100个"帖子",每个帖子上有10个"评论",将产生20000个结果行.NHibernate有一些变通方法,比如"批量大小"(在父ID上选择带子句的子项)或"子选择".

10> 小智..:

在我看来,用Hibernate陷阱写的文章:为什么关系应该是懒惰的,与真正的N + 1问题正好相反.

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

选择提取(默认)极易受到N + 1选择问题的影响,因此我们可能希望启用连接提取


select-size是select select,在一个select语句中为多个父项选择子对象.子选择可能是另一种选择.如果您有多个层次结构级别并且创建了笛卡尔积,则联接可能会非常糟糕.
我读了休眠页面.它没有说明*N + 1选择问题*实际上*是*.但它说你可以使用连接来修复它.

11> 小智..:

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

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



12> jj_..:

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



13> 小智..:

一位百万富翁有N辆车.你想得到所有(4)轮子.

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

成本:

假设索引符合ram.

1 + N查询解析和规划+索引搜索和1 + N +(N*4)板访问以加载有效载荷.

假设索引不适合ram.

最坏情况下的额外成本1 + N板加载索引.

摘要

瓶颈是板块访问(在硬盘上每秒大约70次随机访问)一个热切的连接选择也将访问板1 + N +(N*4)次有效载荷.因此,如果索引适合ram - 没问题,它的速度足够快,因为只涉及ram操作.



14> bedrin..:

N + 1选择问题很痛苦,在单元测试中检测这种情况是有意义的.我开发了一个小型库,用于验证给定测试方法或任意代码块执行的查询数量 - JDBC Sniffer

只需在测试类中添加一个特殊的JUnit规则,并在测试方法上放置具有预期查询数量的注释:

@Rule
public final QueryCounter queryCounter = new QueryCounter();

@Expectation(atMost = 3)
@Test
public void testInvokingDatabase() {
    // your JDBC or JPA code
}



15> Adam Gent..:

正如其他人所说的更优雅的问题是,您要么拥有OneToMany列的笛卡尔积,要么您正在进行N + 1选择.无论是可能的巨大结果集还是分别与数据库聊天.

我很惊讶这没有被提及,但这是我如何解决这个问题... 我做了一个半临时的id表.当你有IN ()条款限制时我也会这样做.

这并不适用于所有情况(可能甚至不是大多数情况)但如果你有很多子对象使得笛卡尔积会失控(即很多OneToMany列的结果数量将是列的乘法)和更像批处理的作业.

首先,将父对象ID作为批处理插入到ids表中.这个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列,你只是做一个SELECT对IDS进行表INNER JOIN荷兰国际集团与子表WHERE batch_id=(反之亦然).您只是想确保按id列进行排序,因为它会使合并结果列更容易(否则您将需要一个HashMap/Table用于整个结果集,这可能不是那么糟糕).

然后你只需要定期清理ids表.

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

现在,您正在执行的查询数量取决于OneToMany列的数量.

推荐阅读
爱唱歌的郭少文_
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有