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

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


当前回答

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

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

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

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

其他回答

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

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

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

因为这个问题,我们离开了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的扇形折叠的实现,请参阅此网页。

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问题至少有两种解决方案:

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