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

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


当前回答

N+1的推广

N+1问题是一个ORM特有的问题名称,它将可以在服务器上合理执行的循环移动到客户端。通用问题不是ORM特有的,您可以通过任何远程API解决。在本文中,我展示了如果您调用一个API N次而不是仅调用1次,JDBC往返是如何代价高昂的。示例中的区别在于您是否调用Oracle PL/SQL过程:

dbms_output.get_lines(调用一次,接收N个项目)dbms_output.get_line(调用N次,每次接收1项)

它们在逻辑上是等价的,但由于服务器和客户端之间的延迟,您需要在循环中添加N个延迟等待,而不是只等待一次。

ORM案例

事实上,ORM-y N+1问题甚至不是ORM特有的,您也可以通过手动运行自己的查询来实现,例如,当您在PL/SQL中执行以下操作时:

-- This loop is executed once
for parent in (select * from parent) loop

  -- This loop is executed N times
  for child in (select * from child where parent_id = parent.id) loop
    ...
  end loop;
end loop;

使用联接(在本例中)实现这一点会更好:

for rec in (
  select *
  from parent p
  join child c on c.parent_id = p.id
)
loop
  ...
end loop;

现在,循环只执行一次,并且循环的逻辑已经从客户端(PL/SQL)移动到服务器(SQL),这甚至可以以不同的方式对其进行优化,例如,通过运行哈希连接(O(N))而不是嵌套循环连接(带索引的O(N log N))

自动检测N+1个问题

如果您使用的是JDBC,可以在后台使用jOOQ作为JDBC代理来自动检测N+1问题。jOOQ的解析器规范化您的SQL查询,并缓存有关连续执行父查询和子查询的数据。如果您的查询不完全相同,但在语义上是等价的,这甚至可以起作用。

其他回答

假设您有一组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章。

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

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

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

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

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

***** 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”(默认)用于查询产品的获取模式为Select获取模式(默认):访问供应商信息缓存第一次不起作用访问供应商

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

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

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

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

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

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);
    }
}