【问题标题】:What is the "N+1 selects problem" in ORM (Object-Relational Mapping)?ORM(对象关系映射)中的“N+1 选择问题”是什么?
【发布时间】:2010-09-10 23:08:33
【问题描述】:

“N+1 选择问题”通常在对象关系映射 (ORM) 讨论中被表述为一个问题,我知道这与必须为看似简单的事情进行大量数据库查询有关在对象世界中。

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

【问题讨论】:

标签: orm select-n-plus-1


【解决方案1】:

Hibernate 和 Spring Data JPA 中的 N+1 问题

N+1 问题是对象关系映射中的一个性能问题,它会在数据库中为应用层的单个选择查询触发多个选择查询(准确地说是 N+1,其中 N = 表中的记录数)。 Hibernate 和 Spring Data JPA 提供了多种方法来捕捉和解决这个性能问题。

什么是 N+1 问题?

要理解 N+1 问题,让我们考虑一个场景。假设我们有一个 User 对象的集合映射到数据库中的 DB_USER 表,并且每个用户都有一个集合或 Role 映射到 DB_ROLE 表使用连接表 DB_USER_ROLE。在 ORM 级别,用户角色具有多对多关系。

Entity Model
@Entity
@Table(name = "DB_USER")
public class User {

    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Long id;
    private String name;

    @ManyToMany(fetch = FetchType.LAZY)                   
    private Set<Role> roles;
    //Getter and Setters 
 }

@Entity
@Table(name = "DB_ROLE")
public class Role {

    @Id
    @GeneratedValue(strategy= GenerationType.AUTO)
    private Long id;

    private String name;
    //Getter and Setters
 }

一个用户可以有多个角色。角色被延迟加载。现在假设我们想要从这个表中获取所有用户并为每个用户打印角色。非常天真的对象关系实现可能是 - UserRepositoryfindAllBy 方法

public interface UserRepository extends CrudRepository<User, Long> {

    List<User> findAllBy();
}

ORM 执行的等效 SQL 查询将是:

首先获取所有用户 (1)

Select * from DB_USER;

然后获取每个用户的角色执行 N 次(其中 N 是用户数)

Select * from DB_USER_ROLE where userid = <userid>;

所以我们需要 一个用户选择N 个额外选择来为每个用户获取角色,其中 N 是用户总数这是 ORM 中的经典 N+1 问题

如何识别?

Hibernate 提供了在控制台/日志中启用 SQL 日志记录的跟踪选项。 使用日志,您可以轻松查看 hibernate 是否针对给定调用发出 N+1 个查询

如果您看到给定选择查询的多个 SQL 条目,则很有可能是由于 N+1 问题造成的。

N+1 分辨率

在 SQL 级别,为了避免 N+1,ORM 需要实现的是触发连接两个表的查询并在单个查询中获得组合结果

在单一查询中检索所有内容(用户和角色)的 Fetch Join SQL

OR 普通 SQL

select user0_.id, role2_.id, user0_.name, role2_.name, roles1_.user_id, roles1_.roles_id from db_user user0_ left outer join db_user_roles roles1_ on user0_.id=roles1_.user_id left outer join db_role role2_ on roles1_.roles_id=role2_.id

Hibernate 和 Spring Data JPA 提供了解决 N+1 ORM 问题的机制。

1. Spring Data JPA 方法:

如果我们使用 Spring Data JPA,那么我们有两种选择来实现这一点 - 使用 EntityGraph 或使用 select 查询和 fetch join。

public interface UserRepository extends CrudRepository<User, Long> {

    List<User> findAllBy();             

    @Query("SELECT p FROM User p LEFT JOIN FETCH p.roles")  
    List<User> findWithoutNPlusOne();

    @EntityGraph(attributePaths = {"roles"})                
    List<User> findAll();
}

使用left join fetch在数据库级别发出N+1个查询,我们使用attributePaths解决了N+1个问题,Spring Data JPA避免了N+1个问题

2。休眠方式:

如果它是纯 Hibernate,那么以下解决方案将起作用。

使用 HQL

from User u *join fetch* u.roles roles roles

使用标准 API:

Criteria criteria = session.createCriteria(User.class);
criteria.setFetchMode("roles", FetchMode.EAGER);

所有这些方法的工作方式都相似,它们使用左连接提取发出类似的数据库查询

【讨论】:

    【解决方案2】:

    什么是N+1查询问题

    N+1 查询问题发生在数据访问框架执行 N 个额外的 SQL 语句以获取执行主 SQL 查询时可能检索到的相同数据时。

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

    问题在于执行大量额外查询,总体而言,这些查询需要足够的时间来减慢响应时间。

    假设我们有以下 post 和 post_cmets 数据库表,它们形成了一对多的表关系:

    我们将创建以下 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 查询问题

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

    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_comment 获取关联的post title

    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 查询问题,因为您执行的不是一个 SQL 查询,而是 5 (1 + 4):

    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 查询问题,因此了解如何避免这些情况非常重要。

    对于下一个示例,假设我们将 postpost_comments 表映射到以下实体:

    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

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

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

    @ManyToOne
    private Post post;
    

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

    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 语句,因为在返回 PostComment 实体中的 List 之前必须获取 post 关联。

    与调用EnrityManagerfind 方法时使用的默认获取计划不同,JPQL 或标准 API 查询定义了一个显式计划,Hibernate 无法通过自动注入 JOIN FETCH 来更改该计划。所以,你需要手动完成。

    如果您根本不需要post 关联,那么您在使用FetchType.EAGER 时就不走运了,因为无法避免获取它。这就是为什么最好默认使用FetchType.LAZY

    但是,如果你想使用post关联,那么你可以使用JOIN FETCH来避免N+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.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语句以构建日志消息。

    同样,修复包括在 JPQL 查询中添加 JOIN FETCH 子句:

    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 并且不引用双向@OneToOne JPA 关系的子关联,您仍然可以触发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!
    

    【讨论】:

    • 但是现在你遇到了分页问题。如果您有 10 辆汽车,每辆汽车有 4 个轮子,并且您想以每页 5 辆汽车对汽车进行分页。所以你基本上有SELECT cars, wheels FROM cars JOIN wheels LIMIT 0, 5。但是你得到的是 2 辆有 5 个轮子的汽车(第一辆有四个轮子的汽车,第二辆只有一个轮子的汽车),因为 LIMIT 会限制整个结果集,而不仅仅是根子句。
    • 感谢您的文章。我会读的。通过快速滚动 - 我看到解决方案是窗口函数,但它们在 MariaDB 中相当新 - 所以问题在旧版本中仍然存在。 :)
    • @VladMihalcea,每次您在解释 N+1 问题时提到 ManyToOne 案例时,我都会从您的文章或帖子中指出。但实际上人们最感兴趣的是与 N+1 问题有关的 OneToMany 案例。您能否参考并解释一下 OneToMany 案例?
    • @VladMicalcea 可以用实体图代替join fetch吗?
    【解决方案3】:

    不谈技术栈实现细节,从架构上讲,N+1 问题至少有两种解决方案:

    • 只有 1 个 - 大查询 - 有连接。这使得大量信息从数据库传输到应用层,尤其是在有多个子记录的情况下。数据库的典型结果是一组行,而不是对象图(针对不同的数据库系统有解决方案)
    • 有两个(或更多需要加入的子级)查询 - 1 个用于父级,在拥有它们后 - 按 ID 查询子级并映射它们。这将最大限度地减少 DB 和 APP 层之间的数据传输。

    【讨论】:

      【解决方案4】:

      N+1 SELECT 问题真的很难发现,尤其是在具有大域的项目中,直到它开始降低性能的那一刻。即使问题得到解决,即通过添加预加载,进一步的开发可能会破坏解决方案和/或在其他地方再次引入 N+1 SELECT 问题。

      我创建了开源库 jplusone 来解决基于 JPA 的 Spring Boot Java 应用程序中的这些问题。该库提供了两个主要功能:

      1. 生成将 SQL 语句与触发它们的 JPA 操作的执行相关联的报告,并将其放置在涉及它的应用程序的源代码中
      2020-10-22 18:41:43.236 调试 14913 --- [主要] c.a.j.core.report.ReportGenerator : 根 com.adgadev.jplusone.test.domain.bookshop.BookshopControllerTest.shouldGetBookDetailsLazily(BookshopControllerTest.java:65) com.adgadev.jplusone.test.domain.bookshop.BookshopController.getSampleBookUsingLazyLoading(BookshopController.java:31) com.adgadev.jplusone.test.domain.bookshop.BookshopService.getSampleBookDetailsUsingLazyLoading [代理] 会话边界 操作 [隐含] com.adgadev.jplusone.test.domain.bookshop.BookshopService.getSampleBookDetailsUsingLazyLoading(BookshopService.java:35) com.adgadev.jplusone.test.domain.bookshop.Author.getName [代理] com.adgadev.jplusone.test.domain.bookshop.Author [获取实体] 声明 [阅读] 从中选择 [...] 作者作者0_ 在 author0_.genre_id=genre1_.id 上左外连接流派 Genre1_ 在哪里 author0_.id=1 操作 [隐含] com.adgadev.jplusone.test.domain.bookshop.BookshopService.getSampleBookDetailsUsingLazyLoading(BookshopService.java:36) com.adgadev.jplusone.test.domain.bookshop.Author.countWrittenBooks(Author.java:53) com.adgadev.jplusone.test.domain.bookshop.Author.books [获取收藏] 声明 [阅读] 从中选择 [...] 书籍书籍0_ 在哪里 book0_.author_id=1
      1. 提供 API,允许编写测试检查您的应用程序使用 JPA 的效率(即断言延迟加载操作的数量)
      @SpringBootTest
      class LazyLoadingTest {
      
          @Autowired
          private JPlusOneAssertionContext assertionContext;
      
          @Autowired
          private SampleService sampleService;
      
          @Test
          public void shouldBusinessCheckOperationAgainstJPlusOneAssertionRule() {
              JPlusOneAssertionRule rule = JPlusOneAssertionRule
                      .within().lastSession()
                      .shouldBe().noImplicitOperations().exceptAnyOf(exclusions -> exclusions
                              .loadingEntity(Author.class).times(atMost(2))
                              .loadingCollection(Author.class, "books")
                      );
      
              // trigger business operation which you wish to be asserted against the rule,
              // i.e. calling a service or sending request to your API controller
              sampleService.executeBusinessOperation();
      
              rule.check(assertionContext);
          }
      }
      

      【讨论】:

        【解决方案5】:

        查看有关该主题的 Ayende 帖子:Combating the Select N + 1 Problem In NHibernate

        基本上,当使用像 NHibernate 或 EntityFramework 这样的 ORM 时,如果您有一对多(主从)关系,并且想要列出每条主记录的所有详细信息,则必须进行 N + 1 查询调用数据库,“N”是主记录的数量:1 次查询获取所有主记录,N 次查询,每个主记录一个,获取每个主记录的所有详细信息。

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

        但是,ORM 有一些选项可以避免这个问题,主要是使用 JOIN。

        【讨论】:

        • 连接不是一个好的解决方案(通常),因为它们可能会导致笛卡尔积,这意味着结果行数是根表结果数乘以每个子表中的结果数.在多个等级制度层面上尤其糟糕。选择 20 个“博客”,每个博客有 100 个“帖子”,每个帖子有 10 个“cmets”,将产生 20000 个结果行。 NHibernate 有一些变通方法,例如“batch-size”(在父 id 上使用 in 子句选择子项)或“subselect”。
        【解决方案6】:

        Here's a good description of the problem

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

        【讨论】:

          【解决方案7】:

          假设您有一个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 Persistence with Hibernate,第 13 章。

          【讨论】:

          • 澄清“这很糟糕” - 您可以通过 1 个选择 (SELECT * from Wheel;) 而不是 N+1 获得所有轮子。 N 较大时,性能影响可能非常显着。
          • @tucuxi 我很惊讶你因为错误而获得了如此多的支持。数据库非常擅长索引,对特定 CarID 进行查询会很快返回。但是如果你得到了所有的 Wheels 都是一次,你将不得不在你的应用程序中搜索 CarID,它没有被索引,这会比较慢。除非您在到达数据库时遇到重大延迟问题,否则 n + 1 实际上会更快 - 是的,我使用大量真实世界的代码对其进行了基准测试。
          • @ariel '正确'的方法是获取 所有 轮子,按 CarId 排序(1 个选择),如果需要比 CarId 更多的详细信息,请再做一个查询 所有 辆汽车(总共 2 个查询)。打印出来的东西现在是最佳的,不需要索引或辅助存储(您可以迭代结果,无需全部下载)。您对错误的事物进行了基准测试。如果您仍然对自己的基准测试充满信心,您介意发表更长的评论(或完整的答案)来解释您的实验和结果吗?
          • “Hibernate(我不熟悉其他 ORM 框架)为您提供了几种处理方式。”这些方式是什么?
          • @Ariel 尝试在不同的机器上使用数据库和应用程序服务器运行您的基准测试。根据我的经验,往返数据库的开销比查询本身要高。所以,是的,查询确实很快,但造成严重破坏的是往返。我已将 "WHERE Id = const" 转换为 "WHERE Id IN (const, const, ...)" 并获得了以下命令幅度增​​加了。
          【解决方案8】:

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

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

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

          【讨论】:

            【解决方案9】:

            其他人更优雅地陈述的问题是,您要么拥有 OneToMany 列的笛卡尔积,要么正在执行 N+1 选择。分别可能是巨大的结果集或与数据库聊天。

            我很惊讶没有提到这一点,但这就是我解决这个问题的方法...... 我制作了一个半临时 ids 表I also do this when you have the IN () clause limitation

            这并不适用于所有情况(可能甚至不是大多数情况),但如果您有很多子对象,这样笛卡尔积就会失控(即很多 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 列,您只需在ids 表INNER JOIN 上使用WHERE batch_id= 对子表执行SELECT(反之亦然)。您只想确保按 id 列排序,因为它会使合并结果列更容易(否则您将需要一个 HashMap/Table 用于整个结果集,这可能还不错)。

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

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

            现在您正在执行的查询数量是 OneToMany 列的数量。

            【讨论】:

              【解决方案10】:

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

              【讨论】:

                【解决方案11】:

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

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

                因素:

                • Supplier 的惰性模式设置为“true”(默认)

                • 用于查询 Product 的 Fetch 模式是 Select

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

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

                • 供应商被访问

                Fetch 模式是 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 次?
                • @bencampbell_ 是的,最初我也有同感。但以他为例,它是许多供应商的一种产品。
                【解决方案12】:

                我不能直接评论其他答案,因为我没有足够的声誉。但值得注意的是,问题的出现本质上只是因为从历史上看,很多 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 中使用的三种常见JOIN 算法之一称为嵌套循环。它基本上是引擎盖下的 N+1 选择。唯一的区别是数据库做出了明智的选择,根据统计数据和索引使用它,而不是客户端代码明确地强制它走这条路。
                • @Brandon 是的!就像 JOIN 提示和 INDEX 提示一样,在所有情况下强制执行某个执行路径很少会击败数据库。数据库几乎总是非常非常擅长选择获取数据的最佳方法。也许在 dbs 的早期,您需要以一种特殊的方式来“表达”您的问题以哄骗 db,但是经过数十年的世界级工程,您现在可以通过向您的数据库询问一个关系问题并让它获得最佳性能弄清楚如何为您获取和组装这些数据。
                • 数据库不仅使用索引和统计信息,所有操作也是本地 I/O,其中大部分操作通常针对高效缓存而不是磁盘。数据库程序员将大量精力投入到优化这些事情上。
                【解决方案13】:

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

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

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

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

                【讨论】:

                • 同样,连接不是一个好的解决方案,尤其是当可能加载超过 2 个层次结构级别时。改为选中“subselect”或“batch-size”;最后一个将通过“in”子句中的父 ID 加载子代,例如“select ... from wheel where car_id in (1,3,4,6,7,8,11,13)”。
                【解决方案14】:

                因为这个问题,我们离开了 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
                

                请参阅this web page 了解 fanfolding 的 python 实现。

                【讨论】:

                • 我很高兴我偶然发现了你的帖子,因为我以为我快疯了。当我发现 N+1 问题时,我的直接想法是——好吧,你为什么不创建一个包含你需要的所有信息的视图,然后从该视图中提取?你证实了我的立场。谢谢你,先生。
                • 因为这个问题,我们离开了 Django 中的 ORM。 嗯? Django 有 select_related,它旨在解决这个问题 - 事实上,它的文档以类似于您的 p.car.colour 示例的示例开头。
                • 这是一个老答案,我们现在在 Django 中有 select_related()prefetch_related()
                • 酷。但是select_related() 和朋友似乎没有做任何明显有用的连接推断,例如LEFT OUTER JOIN。问题不是接口问题,而是与对象和关系数据是可映射的奇怪想法有关的问题......在我看来。
                【解决方案15】:

                在我看来Hibernate Pitfall: Why Relationships Should Be Lazy写的文章与真正的N+1问题完全相反。

                如需正确解释请参考Hibernate - Chapter 19: Improving Performance - Fetching Strategies

                选择抓取(默认)是 极易受到 N+1 次选择的影响 问题,所以我们可能想要启用 加入抓取

                【讨论】:

                • 我阅读了休眠页面。它并没有说明N+1 选择问题 实际上 是什么。但它说你可以使用连接来修复它。
                • 选择提取需要批量大小,以便在一个选择语句中为多个父对象选择子对象。子选择可能是另一种选择。如果您有多个层次结构级别并且创建了笛卡尔积,则连接可能会变得非常糟糕。
                【解决方案16】:

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

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

                【讨论】:

                  【解决方案17】:
                  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 的唯一原因是,如果您与数据库通信时有很多延迟。
                  • 加入和排序可以非常快(因为您将加入索引和可能排序的字段)。你的“n+1”有多大?你真的相信 n+1 问题只适用于高延迟的数据库连接吗?
                  • @ariel - 您认为 N+1 是“最快”的建议是错误的,即使您的基准测试可能是正确的。这怎么可能?请参阅en.wikipedia.org/wiki/Anecdotal_evidence,以及我在此问题的其他答案中的评论。
                  • @Ariel - 我想我理解得很好:)。我只是想指出您的结果仅适用于一组条件。我可以很容易地构建一个相反的例子。这有意义吗?
                  • 重申一下,SELECT N + 1 问题的核心是:我有 600 条记录要检索。在一个查询中获得全部 600 个,还是在 600 个查询中一次获得 1 个更快。除非您使用的是 MyISAM 和/或您的规范化/索引模式不佳(在这种情况下 ORM 不是问题),否则经过适当调整的数据库将在 2 毫秒内返回 600 行,同时返回单个行每个大约 1 毫秒。所以我们经常看到 N + 1 需要数百毫秒,而连接只需要几毫秒
                  【解决方案18】:

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

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

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

                  【讨论】:

                    猜你喜欢
                    • 2011-10-27
                    • 1970-01-01
                    • 2013-09-02
                    • 1970-01-01
                    • 1970-01-01
                    • 2010-11-12
                    • 1970-01-01
                    • 2011-04-15
                    • 2010-12-03
                    相关资源
                    最近更新 更多