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

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


当前回答

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

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

生成将SQL语句与触发它们的JPA操作的执行相关联的报告,并将其放置在应用程序的源代码中

2020-10-22 18:41:43.236 DEBUG 14913 --- [           main] c.a.j.core.report.ReportGenerator        :
    ROOT
        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 [PROXY]
            SESSION BOUNDARY
                OPERATION [IMPLICIT]
                    com.adgadev.jplusone.test.domain.bookshop.BookshopService.getSampleBookDetailsUsingLazyLoading(BookshopService.java:35)
                    com.adgadev.jplusone.test.domain.bookshop.Author.getName [PROXY]
                    com.adgadev.jplusone.test.domain.bookshop.Author [FETCHING ENTITY]
                        STATEMENT [READ]
                            select [...] from
                                author author0_
                                left outer join genre genre1_ on author0_.genre_id=genre1_.id
                            where
                                author0_.id=1
                OPERATION [IMPLICIT]
                    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 [FETCHING COLLECTION]
                        STATEMENT [READ]
                            select [...] from
                                book books0_
                            where
                                books0_.author_id=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);
    }
}

其他回答

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

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

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

假设您有一组Car对象(数据库行),每个Car都有一组Wheel对象(也有行)。换句话说,汽车→ 轮子是一对多的关系。

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

SELECT * FROM Cars;

然后,对于每辆车:

SELECT * FROM Wheel WHERE CarId = ?

换言之,您可以为汽车选择一个选项,然后再选择N个选项,其中N是汽车的总数。

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

SELECT * FROM Wheel;

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

参考资料:Java Persistence with Hibernate,第13章。

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

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

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 SELECT问题真的很难发现,尤其是在具有大型域的项目中,当它开始降低性能时。即使问题得到解决,即通过添加紧急加载,进一步的开发可能会破坏解决方案和/或在其他地方再次引入N+1 SELECT问题。

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

生成将SQL语句与触发它们的JPA操作的执行相关联的报告,并将其放置在应用程序的源代码中

2020-10-22 18:41:43.236 DEBUG 14913 --- [           main] c.a.j.core.report.ReportGenerator        :
    ROOT
        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 [PROXY]
            SESSION BOUNDARY
                OPERATION [IMPLICIT]
                    com.adgadev.jplusone.test.domain.bookshop.BookshopService.getSampleBookDetailsUsingLazyLoading(BookshopService.java:35)
                    com.adgadev.jplusone.test.domain.bookshop.Author.getName [PROXY]
                    com.adgadev.jplusone.test.domain.bookshop.Author [FETCHING ENTITY]
                        STATEMENT [READ]
                            select [...] from
                                author author0_
                                left outer join genre genre1_ on author0_.genre_id=genre1_.id
                            where
                                author0_.id=1
                OPERATION [IMPLICIT]
                    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 [FETCHING COLLECTION]
                        STATEMENT [READ]
                            select [...] from
                                book books0_
                            where
                                books0_.author_id=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);
    }
}

在不深入技术堆栈实现细节的情况下,从架构上讲,N+1问题至少有两种解决方案:

只有1个-大查询-带有联接。这使得大量信息从数据库传输到应用程序层,特别是如果有多个子记录。数据库的典型结果是一组行,而不是对象图(有不同的DB系统的解决方案)有两个(或更多需要连接的子级)查询-1个用于父级,在有了它们之后-通过ID查询子级并映射它们。这将最大限度地减少DB和APP层之间的数据传输。