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


1596

在对象关系映射(ORM)讨论中,通常将“ N + 1选择问题”表示为问题,并且我了解到它与必须对对象中看起来很简单的内容进行大量数据库查询有关。世界。

有人对此问题有更详细的解释吗?


2
这是一个很好的链接,其中包含对理解n + 1问题的很好的解释。它还涵盖了解决此问题的解决方案:architects.dzone.com/articles/how-identify-and-resilve-n1
ace。


对于寻求该问题解决方案的每个人,我都找到了一篇描述它的文章。stackoverflow.com/questions/32453989/...
damndemon

2
考虑到答案,这不应该称为1 + N问题吗?因为这似乎是一个术语,所以我并不是要问OP。
user1418717'2

Answers:


1014

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

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

SELECT * FROM Cars;

然后每个Car

SELECT * FROM Wheel WHERE CarId = ?

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

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

SELECT * FROM Wheel

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

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


139
为了弄清楚“这很糟糕”,您可以选择1来选择所有车轮(SELECT * from Wheel;),而不是N + 1。如果N较大,则性能影响可能非常显着。
tucuxi

211
@tucuxi我很惊讶您为错了这么多支持。数据库非常适合索引,对特定的CarID进行查询将很快返回。但是,如果所有的Wheels都只有一次,则必须在应用程序中搜索CarID(未编制索引),这会比较慢。除非您遇到主要的延迟问题,否则到达数据库的速度实际上要快n +1-是的,我用各种各样的真实代码对它进行了基准测试。
Ariel

73
@ariel“正确”的方法是按照CarId的顺序获取所有车轮(1个选择),如果需要的细节比CarId更多,请对所有汽车进行第二次查询(总共2个查询)。现在,将内容打印出来是最佳的,并且不需要索引或二级存储(您可以遍历结果,而无需全部下载)。您基准测试错误。如果您仍然对基准测试充满信心,您是否愿意发表更长的评论(或完整的答案)来说明您的实验和结果?
tucuxi 2011年

92
“休眠(我不熟悉其他ORM框架)为您提供了几种处理它的方法。” 这些是吗?
蒂玛2012年

58
@Ariel尝试在单独的计算机上对数据库和应用程序服务器运行基准测试。以我的经验,往返数据库的开销比查询本身更多。是的,查询的速度确实很快,但这是往返的麻烦。我已经将“ WHERE Id = const ” 转换为“ WHERE Id IN(constconst,...)”,并且得到了数量级的增加。
汉斯(Hans)

110
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的优点是降低了查询复杂度,您可以使用延迟加载,其中仅在第一次请求时才加载子结果集。


4
n + 1的另一个优点是它更快,因为数据库可以直接从索引返回结果。进行联接然后排序需要一个临时表,该表比较慢。避免n + 1的唯一原因是,如果您与数据库的交谈有很多延迟。
阿里埃勒(Ariel)

17
联接和排序的速度可能非常快(因为您将要在索引和可能排序的字段上联接)。您的“ n + 1”有多大?您是否真的相信n + 1问题仅适用于高延迟数据库连接?
tucuxi 2011年

9
@ariel-即使基准可能是正确的,您关于N + 1是“最快”的建议也是错误的。那怎么可能?参见en.wikipedia.org/wiki/Anecdotal_evidence,以及我在对该问题的其他答案中的评论。
whitneyland

7
@Ariel-我想我很好:)。我只是想指出您的结果仅适用于一组条件。我可以很容易地构造一个相反的例子。那有意义吗?
whitneyland

13
重申一下,SELECT N + 1问题的核心是:我要检索600条记录。在一个查询中获得全部600个查询还是在600个查询中一次获取1个查询更快?除非您使用的是MyISAM,并且/或者您的规范化/索引编制架构不佳(在这种情况下,ORM都不是问题),否则,经过适当调优的db将在2 ms内返回600行,而在其中返回单个行每个大约1毫秒。所以我们经常看到N + 1个回吐几百毫秒,其中一个连接的只需要一对夫妇

64

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

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

  • 用于查询产品的提取模式为“选择”

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

  • 缓存第一次不起作用

  • 供应商被访问

提取模式为选择提取(默认)

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


3
是否应该为供应商选择1个,然后为产品选择N个?
bencampbell_14 '18

@bencampbell_是的,最初我也有同样的感觉。但是以他的例子为例,它是许多供应商的一种产品。
Mohd Faizan Khan

38

我无法对其他答案直接发表评论,因为我的声誉不够。但是值得注意的是,这个问题的出现本质上是因为,从历史上看,很多dbms在处理联接时都非常差(MySQL是一个特别值得注意的例子)。因此,n + 1通常比联接要快得多。然后有一些方法可以在n + 1上进行改进,但是仍然不需要连接,这就是原始问题所在。

但是,MySQL现在比联接时要好得多。当我第一次学习MySQL时,我经常使用join。然后,我发现它们有多慢,并在代码中改为n + 1。但是,最近,我一直在转而加入联接,因为与现在开始使用MySQL相比,MySQL现在在处理它们方面要好得多。

如今,就性能而言,在索引正确的表集上进行简单联接已很少成为问题。如果确实对性能产生了影响,那么使用索引提示通常可以解决它们。

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

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

因此,摘要是:如果过去由于MySQL的糟糕性能而避免使用联接,请重试最新版本。您可能会感到惊喜。


7
将MySQL的早期版本称为关系DBMS相当困难...如果遇到那些问题的人一直在使用真实的数据库,那么他们就不会遇到那些问题。;-)
Craig

2
有趣的是,通过引入和随后优化INNODB引擎,MySQL中解决了许多这类问题,但是您仍然会遇到尝试推广MYISAM的人们,因为他们认为MYISAM更快。
Craig

5
仅供参考,JOINRDBMS中使用的三种常见算法之一称为嵌套循环。从根本上讲,它是一个N + 1选择。唯一的区别是,数据库管理人员明智地选择了基于统计信息和索引来使用它,而不是通过客户端代码来强制将其沿着该路径分类。
布兰登

2
@布兰登是的!就像JOIN提示和INDEX提示一样,在所有情况下都强制执行某个执行路径很少会击败数据库。数据库几乎总是非常非常擅长选择最佳方法来获取数据。也许在数据库开发的初期,您需要以一种特殊的方式来“解决”您的问题,以哄骗数据库,但是经过数十年的世界一流的工程设计,现在您可以通过向数据库提出一个关系问题并让其解决问题来获得最佳性能。弄清楚如何为您获取和组合该数据。

3
数据库不仅使用索引和统计信息,而且所有操作都是本地I / O,其中许多操作通常针对高效缓存而不是磁盘。数据库程序员将大量精力投入到优化这类事情上。
Craig

27

由于这个问题,我们放弃了Django中的ORM。基本上,如果你尝试去做

for p in person:
    print p.car.colour

ORM会很高兴地返回所有人(通常作为Person对象的实例),但是随后它将需要查询每个Person的汽车表。

一种简单有效的方法就是我所说的“ 扇形折叠 ”,它避免了一个荒谬的想法,即关系数据库的查询结果应映射回组成查询的原始表。

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

将结果吸到具有参数的通用对象创建器中,该参数在第三项之后拆分。这意味着“琼斯”对象将不会被制作多次。

第三步:渲染

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

请参阅此网页获取用于python 的折叠功能的实现。


10
我很高兴我偶然发现了你的帖子,因为我以为我快疯了。当我发现N + 1问题时,我立即想到-为什么您不创建一个包含所需所有信息的视图,并从该视图中提取信息呢?您已经验证了我的职位。谢谢你,先生。
开发人员

14
由于这个问题,我们放弃了Django中的ORM。??Django has select_related旨在解决此问题-实际上,其docs以与您的示例相似的示例开头p.car.colour
阿德里安

8
这是一个老anwswer,我们已经select_related()prefetch_related()在Django现在。
Mariusz Jamro

1
凉。但是select_related()和朋友似乎并没有对联接进行任何明显有用的外推,如LEFT OUTER JOIN。问题不是接口问题,而是与对象和关系数据可映射的奇怪想法有关的问题。
rorycl

26

由于这是一个非常常见的问题,因此我写了 这篇文章,此答案基于该文章

什么是N + 1查询问题

当数据访问框架执行N条其他SQL语句以获取执行主SQL查询时可能已检索到的相同数据时,就会发生N + 1查询问题。

N的值越大,将执行的查询越多,对性能的影响越大。而且,与慢查询日志可以帮助您查找慢查询的日志不同,N + 1问题不会被发现,因为每个附加查询的运行速度都足够快,不会触发慢查询日志。

问题是执行大量其他查询,这些查询总体上需要花费足够的时间来减慢响应时间。

让我们考虑一下,我们具有以下post和post_comments数据库表,它们构成了一对多的表关系

<code> post </ code>和<code> post_comments </ code>表

我们将创建以下4 post行:

INSERT INTO post (title, id)
VALUES ('High-Performance Java Persistence - Part 1', 1)

INSERT INTO post (title, id)
VALUES ('High-Performance Java Persistence - Part 2', 2)

INSERT INTO post (title, id)
VALUES ('High-Performance Java Persistence - Part 3', 3)

INSERT INTO post (title, id)
VALUES ('High-Performance Java Persistence - Part 4', 4)

并且,我们还将创建4 post_comment个子记录:

INSERT INTO post_comment (post_id, review, id)
VALUES (1, 'Excellent book to understand Java Persistence', 1)

INSERT INTO post_comment (post_id, review, id)
VALUES (2, 'Must-read for Java developers', 2)

INSERT INTO post_comment (post_id, review, id)
VALUES (3, 'Five Stars', 3)

INSERT INTO post_comment (post_id, review, id)
VALUES (4, 'A great reference book', 4)

普通SQL的N + 1查询问题

如果选择post_comments使用此SQL查询:

List<Tuple> comments = entityManager.createNativeQuery("""
    SELECT
        pc.id AS id,
        pc.review AS review,
        pc.post_id AS postId
    FROM post_comment pc
    """, Tuple.class)
.getResultList();

并且,稍后,您决定post title为每个获取相关联post_comment

for (Tuple comment : comments) {
    String review = (String) comment.get("review");
    Long postId = ((Number) comment.get("postId")).longValue();

    String postTitle = (String) entityManager.createNativeQuery("""
        SELECT
            p.title
        FROM post p
        WHERE p.id = :postId
        """)
    .setParameter("postId", postId)
    .getSingleResult();

    LOGGER.info(
        "The Post '{}' got this review '{}'",
        postTitle,
        review
    );
}

您将触发N + 1查询问题,因为您执行了5(1 + 4),而不是一个SQL查询:

SELECT
    pc.id AS id,
    pc.review AS review,
    pc.post_id AS postId
FROM post_comment pc

SELECT p.title FROM post p WHERE p.id = 1
-- The Post 'High-Performance Java Persistence - Part 1' got this review
-- 'Excellent book to understand Java Persistence'

SELECT p.title FROM post p WHERE p.id = 2
-- The Post 'High-Performance Java Persistence - Part 2' got this review
-- 'Must-read for Java developers'

SELECT p.title FROM post p WHERE p.id = 3
-- The Post 'High-Performance Java Persistence - Part 3' got this review
-- 'Five Stars'

SELECT p.title FROM post p WHERE p.id = 4
-- The Post 'High-Performance Java Persistence - Part 4' got this review
-- 'A great reference book'

解决N + 1查询问题非常容易。您需要做的就是提取原始SQL查询中所需的所有数据,如下所示:

List<Tuple> comments = entityManager.createNativeQuery("""
    SELECT
        pc.id AS id,
        pc.review AS review,
        p.title AS postTitle
    FROM post_comment pc
    JOIN post p ON pc.post_id = p.id
    """, Tuple.class)
.getResultList();

for (Tuple comment : comments) {
    String review = (String) comment.get("review");
    String postTitle = (String) comment.get("postTitle");

    LOGGER.info(
        "The Post '{}' got this review '{}'",
        postTitle,
        review
    );
}

这次,仅执行一个SQL查询来获取我们进一步感兴趣使用的所有数据。

JPA和Hibernate的N + 1查询问题

使用JPA和Hibernate时,有几种触发N + 1查询问题的方法,因此了解如何避免这种情况非常重要。

对于下一个示例,请考虑我们将postand post_comments表映射到以下实体:

<code> Post </ code>和<code> PostComment </ code>实体

JPA映射如下所示:

@Entity(name = "Post")
@Table(name = "post")
public class Post {

    @Id
    private Long id;

    private String title;

    //Getters and setters omitted for brevity
}

@Entity(name = "PostComment")
@Table(name = "post_comment")
public class PostComment {

    @Id
    private Long id;

    @ManyToOne
    private Post post;

    private String review;

    //Getters and setters omitted for brevity
}

FetchType.EAGER

FetchType.EAGER对您的JPA关联使用隐式或显式使用是一个坏主意,因为您将获取所需的更多数据。此外,该FetchType.EAGER策略还容易出现N + 1查询问题。

不幸的是,@ManyToOneand @OneToOne关联FetchType.EAGER默认使用,因此,如果您的映射如下所示:

@ManyToOne
private Post post;

您正在使用该FetchType.EAGER策略,并且每次通过JPQL或Criteria API查询JOIN FETCH加载某些PostComment实体时都忘记使用该策略:

List<PostComment> comments = entityManager
.createQuery("""
    select pc
    from PostComment pc
    """, PostComment.class)
.getResultList();

您将触发N + 1查询问题:

SELECT 
    pc.id AS id1_1_, 
    pc.post_id AS post_id3_1_, 
    pc.review AS review2_1_ 
FROM 
    post_comment pc

SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 1
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 2
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 3
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 4

请注意,执行了其他SELECT语句,因为在post返回Listof 之前必须先获取关联PostComment实体。

与调用的find方法时使用的默认提取计划不同EnrityManager,JPQL或Criteria API查询定义了一个显式计划,Hibernate无法通过自动注入JOIN FETCH来更改该计划。因此,您需要手动进行操作。

如果您根本不需要该post关联,则使用时会很不幸,FetchType.EAGER因为无法避免获取该关联。这就是为什么最好使用FetchType.LAZY默认值的原因。

但是,如果您想使用post关联,则可以使用A JOIN FETCH+ 1查询问题:

List<PostComment> comments = entityManager.createQuery("""
    select pc
    from PostComment pc
    join fetch pc.post p
    """, PostComment.class)
.getResultList();

for(PostComment comment : comments) {
    LOGGER.info(
        "The Post '{}' got this review '{}'", 
        comment.getPost().getTitle(), 
        comment.getReview()
    );
}

这次,Hibernate将执行一个SQL语句:

SELECT 
    pc.id as id1_1_0_, 
    pc.post_id as post_id3_1_0_, 
    pc.review as review2_1_0_, 
    p.id as id1_0_1_, 
    p.title as title2_0_1_ 
FROM 
    post_comment pc 
INNER JOIN 
    post p ON pc.post_id = p.id

-- The Post 'High-Performance Java Persistence - Part 1' got this review 
-- 'Excellent book to understand Java Persistence'

-- The Post 'High-Performance Java Persistence - Part 2' got this review 
-- 'Must-read for Java developers'

-- The Post 'High-Performance Java Persistence - Part 3' got this review 
-- 'Five Stars'

-- The Post 'High-Performance Java Persistence - Part 4' got this review 
-- 'A great reference book'

有关为什么应避免FetchType.EAGER获取策略的更多详细信息,也请参阅本文

FetchType.LAZY

即使您切换为FetchType.LAZY对所有关联都显式使用,仍然可以遇到N + 1问题。

这次,post关联映射如下:

@ManyToOne(fetch = FetchType.LAZY)
private Post post;

现在,当您获取PostComment实体时:

List<PostComment> comments = entityManager
.createQuery("""
    select pc
    from PostComment pc
    """, PostComment.class)
.getResultList();

Hibernate将执行一个SQL语句:

SELECT 
    pc.id AS id1_1_, 
    pc.post_id AS post_id3_1_, 
    pc.review AS review2_1_ 
FROM 
    post_comment pc

但是,如果之后,您将引用延迟加载的post关联:

for(PostComment comment : comments) {
    LOGGER.info(
        "The Post '{}' got this review '{}'", 
        comment.getPost().getTitle(), 
        comment.getReview()
    );
}

您将收到N + 1查询问题:

SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 1
-- The Post 'High-Performance Java Persistence - Part 1' got this review 
-- 'Excellent book to understand Java Persistence'

SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 2
-- The Post 'High-Performance Java Persistence - Part 2' got this review 
-- 'Must-read for Java developers'

SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 3
-- The Post 'High-Performance Java Persistence - Part 3' got this review 
-- 'Five Stars'

SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 4
-- The Post 'High-Performance Java Persistence - Part 4' got this review 
-- 'A great reference book'

因为 post关联是延迟获取的,所以在访问惰性关联以构建日志消息时将执行辅助SQL语句。

同样,此修复程序包括在JOIN FETCHJPQL查询中添加子句:

List<PostComment> comments = entityManager.createQuery("""
    select pc
    from PostComment pc
    join fetch pc.post p
    """, PostComment.class)
.getResultList();

for(PostComment comment : comments) {
    LOGGER.info(
        "The Post '{}' got this review '{}'", 
        comment.getPost().getTitle(), 
        comment.getReview()
    );
}

而且,就像在FetchType.EAGER示例中一样,此JPQL查询将生成单个SQL语句。

即使您正在使用FetchType.LAZY并且未引用双向@OneToOneJPA关系的子关联,您仍然可以触发N + 1查询问题。

有关如何克服@OneToOne关联产生的N + 1查询问题的更多详细信息,请参阅本文

如何自动检测N + 1查询问题

如果您想在数据访问层中自动检测到N + 1查询问题,本文将介绍如何使用db-util开源项目来实现。

首先,您需要添加以下Maven依赖项:

<dependency>
    <groupId>com.vladmihalcea</groupId>
    <artifactId>db-util</artifactId>
    <version>${db-util.version}</version>
</dependency>

之后,您只需要使用SQLStatementCountValidator实用程序来声明所生成的基础SQL语句:

SQLStatementCountValidator.reset();

List<PostComment> comments = entityManager.createQuery("""
    select pc
    from PostComment pc
    """, PostComment.class)
.getResultList();

SQLStatementCountValidator.assertSelectCount(1);

如果您正在使用FetchType.EAGER并运行上述测试用例,则会遇到以下测试用例失败的情况:

SELECT 
    pc.id as id1_1_, 
    pc.post_id as post_id3_1_, 
    pc.review as review2_1_ 
FROM 
    post_comment pc

SELECT p.id as id1_0_0_, p.title as title2_0_0_ FROM post p WHERE p.id = 1

SELECT p.id as id1_0_0_, p.title as title2_0_0_ FROM post p WHERE p.id = 2


-- SQLStatementCountMismatchException: Expected 1 statement(s) but recorded 3 instead!

有关db-util开源项目的更多详细信息,请参阅本文


但是现在您在分页上遇到了问题。如果您有10辆车,那么每辆车有4个轮子,而您想对每页有5辆车的汽车进行分页。所以你基本上就拥有了SELECT cars, wheels FROM cars JOIN wheels LIMIT 0, 5。但是您得到的是2辆5轮的汽车(第一辆4轮的汽车和第二辆只有1轮的汽车),因为LIMIT会限制整个结果集,而不仅是根子句。
CappY


谢谢你的文章。我会读的。通过快速滚动-我看到了解决方案是Window Function,但是它们在MariaDB中是相当新的-因此问题在旧版本中仍然存在。:)
CappY

@VladMihalcea,每次您在解释N + 1问题时都提到ManyToOne案例时,我都会从您的文章或文章中指出。但是实际上,人们对与N + 1问题有关的OneToMany案例最感兴趣。您能否参考并解释OneToMany案例?
JJ Beam

18

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

在某些O / R配置中,当您拥有一个映射的Company对象并访问其Employee对象时,O / R工具将为每个员工进行一次选择,如果您只是使用直接SQL进行操作,则可以select * from employees where company_id = XX。因此,N(员工人数)加1(公司)

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


18

这是一个很好的问题描述

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


17

查看有关主题的Ayende帖子:打击NHibernate中的Select N +1问题

基本上,当使用NHibernate或EntityFramework之类的ORM时,如果您具有一对多(主从细节)关系,并且希望列出每个主记录的所有详细信息,则必须对N数据库中,“ N”表示主记录的数量:1个查询以获取所有主记录,N个查询,每个主记录一个,以获取每个主记录的所有详细信息。

更多的数据库查询调用→更多的延迟时间→降低了应用程序/数据库性能。

但是,ORM具有避免此问题的选项,主要是使用JOIN。


3
联接不是一个好的解决方案(通常),因为它们可能会产生笛卡尔积,这意味着结果行数是根表结果数乘以每个子表中结果数。在多个等级制度下尤其糟糕。选择20个“博客”,每个博客有100个“帖子”,每个帖子有10个“评论”,将产生20000条结果行。NHibernate有解决方法,例如“批处理大小”(选择在父ID上带有in子句的子代)或“ subselect”。
艾里克·哈特

13

在我看来,Hibernate Pitfall撰写的文章:为什么关系应该变得懒惰与真正的N + 1问题完全相反。

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

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


2
我阅读了休眠页面。它并没有说N + 1选择问题实际上是什么。但是它说您可以使用联接来修复它。
伊恩·博伊德

3
选择获取需要批处理大小,以便在一个选择语句中为多个父对象选择子对象。子选择可能是另一种选择。如果您具有多个层次结构级别并且创建了笛卡尔积,则联接可能会变得非常糟糕。
埃里克·哈特

13

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


10

提供的链接非常简单地说明了n + 1问题。如果将其应用于Hibernate,则基本上是在谈论同一件事。查询对象时,将加载实体,但是任何关联(除非另行配置)都将延迟加载。因此,一个查询针对根对象,而另一查询针对每个对象加载关联。返回的100个对象意味着一个初始查询,然后是100个其他查询以获取每个对象的关联,n + 1。

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


9

一位百万富翁拥有N辆汽车。您想要所有(4)个轮子。

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

费用:

假设索引适合ram。

1 + N查询解析和计划+索引搜索,以及1 + N +(N * 4)个用于装载有效载荷的板访问。

假设索引不适合ram。

在最坏的情况下,额外的成本为1 + N个板块以获取装载指数。

摘要

瓶颈是板的访问(在硬盘上每秒约有70次随机访问)急切的联接选择还将对板进行1 + N +(N * 4)次有效负载访问。因此,如果索引适合ram-没问题,它的速度足够快,因为仅涉及ram操作。


9

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

只需在测试类中添加特殊的JUnit规则,然后在测试方法中放置带有预期查询数量的注释即可:

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

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

5

正如其他人更优雅地指出的那样,问题是您要么拥有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列,您只需SELECT在id表INNER JOIN上对子表做一个WHERE batch_id=(反之亦然)。您只需要确保按id列排序即可,因为这将使合并结果列更加容易(否则,整个结果集都需要HashMap / Table,这可能还不错。)

然后,您只需定期清理id表。

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

现在,您要执行的查询数是由OneToMany列数决定的。


1

以Matt Solnit为例,假设您将Car和Wheels之间的关联定义为LAZY,并且需要一些Wheels字段。这意味着在第一次选择之后,休眠将为每辆汽车执行“从* where car_id =:id的车轮中选择*”。

这使得每N辆汽车都具有第一选择权和更多选择权,这就是为什么它被称为n + 1问题。

为避免这种情况,请尽快获取关联,以便休眠状态通过联接加载数据。

但是请注意,如果很多次您都没有访问关联的Wheels,最好将其保持为LAZY或使用Criteria更改获取类型。


1
同样,联接不是一个好的解决方案,尤其是当可能加载两个以上的层次结构级别时。改为选择“ subselect”或“ batch-size”;最后一个将通过“ in”子句中的父ID加载子项,例如“从(1,3,4,6,7,8,11,13)中car_id的车轮中选择...”。
埃里克·哈特
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.