代码之家  ›  专栏  ›  技术社区  ›  Lars A. Brekken

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

  •  1398
  • Lars A. Brekken  · 技术社区  · 17 年前

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

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

    16 回复  |  直到 17 年前
        1
  •  851
  •   Sergio    8 年前

    假设你收集了 Car 对象(数据库行),以及每个 小型车 收藏了 Wheel 对象(也包括行)。换言之, 小型车 -gt; 车轮 是一对多的关系。

    现在,假设您需要遍历所有的汽车,并为每个汽车打印出一个车轮列表。原始的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章。

        2
  •  100
  •   cfeduke    17 年前
    SELECT 
    table1.*
    , table2.*
    INNER JOIN table2 ON table2.SomeFkId = table1.SomeId
    

    这将为您提供一个结果集,其中表2中的子行通过返回表2中每个子行的表1结果而导致重复。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填充一个home实例,address=“22 valley st”,然后只需一个查询就可以用dave、john和mike的people实例填充居民数组。

    对于上面使用的相同地址的N+1查询将导致:

    Id Address
    1  22 Valley St
    

    有一个单独的查询

    SELECT * FROM Person WHERE HouseId = 1
    

    并产生一个独立的数据集

    Name    HouseId
    Dave    1
    John    1
    Mike    1
    

    最后的结果与上面的单查询相同。

    单次选择的好处是,您可以提前获得所有数据,这可能是您最终想要的。n+1的优点是减少了查询复杂性,并且您可以使用延迟加载,其中子结果集仅在第一次请求时加载。

        3
  •  60
  •   Manuel Berger Summy    12 年前

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

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

        4
  •  36
  •   Mark Goodge    12 年前

    我不能直接对其他答案发表评论,因为我没有足够的声誉。但值得注意的是,这个问题本质上只会出现,因为在历史上,很多DBMS在处理连接时都相当糟糕(MySQL是一个特别值得注意的例子)。因此,n+1通常比一个连接要快得多。还有一些方法可以改进n+1,但是仍然不需要连接,这就是最初的问题所涉及的。

    然而,MySQL现在比以前在连接方面要好得多。当我第一次学习mysql的时候,我经常使用join。然后我发现它们有多慢,于是在代码中改为n+1。但是,最近,我又回到了Join,因为与我刚开始使用MySQL时相比,MySQL现在在处理它们方面有了很大的进步。

    现在,在性能方面,对一组索引正确的表进行简单的联接很少是一个问题。如果它确实会影响性能,那么使用索引提示通常可以解决问题。

    这里由MySQL开发团队讨论:

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

    所以总结是:如果您过去一直在避免连接,因为MySQL对它们的糟糕性能,那么在最新版本上再试一次。你可能会感到惊喜。

        5
  •  26
  •   rorycl    15 年前

    因为这个问题,我们离开了在Django的ORM。基本上,如果你试着去做

    for p in person:
        print p.car.colour
    

    ORM会很高兴地返回所有人(通常作为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
    

    第二步:客观化

    将结果吸收到一个通用对象创建者中,并在第三个项后使用要拆分的参数。这意味着“琼斯”物体不会被制造一次以上。

    步骤3:渲染

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

    this web page 为了实现 扇形折叠 用于Python。

        6
  •  17
  •   davetron5000    17 年前

    假设你有公司和员工。公司有很多员工(即员工有一个外勤公司ID)。

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

    这就是EJB实体bean的初始版本的工作原理。我相信像Hibernate这样的事情已经解决了,但我不太确定。大多数工具通常包括有关其映射策略的信息。

        7
  •  14
  •   Joe Dean    17 年前

    这里有一个很好的问题描述- http://www.realsolve.co.uk/site/tech/hib-tip-pitfall.php?name=why-lazy

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

        8
  •  13
  •   Ian Boyd    16 年前

    在我看来,这篇文章写在 Hibernate Pitfall: Why Relationships Should Be Lazy 正相反的是真正的N+1问题是。

    如果您需要正确的解释,请参阅 Hibernate - Chapter 19: Improving Performance - Fetching Strategies

    选择提取(默认值)为 极易受到n+1选择的影响 问题,因此我们可能希望 连接提取

        9
  •  13
  •   Nathan    14 年前

    查看Ayend Post的主题: Combating the Select N + 1 Problem In NHibernate

    基本上,当使用诸如nhibernate或entityframework这样的ORM时,如果您有一对多(master-detail)关系,并且想要列出每个主记录的所有详细信息,则必须对数据库进行n+1查询调用,“n”是主记录数:1个查询获取所有主记录,n个查询每个主记录,以获取每个主记录的所有详细信息。

    更多的数据库查询调用——以及更多的延迟时间——&降低应用程序/数据库性能。

    然而,ORM可以选择避免这个问题,主要是使用“连接”。

        10
  •  10
  •   Jeff Mills    17 年前

    提供的链接有一个非常简单的n+1问题的例子。如果你把它应用于冬眠,基本上就是在说同样的事情。当查询对象时,将加载实体,但任何关联(除非另有配置)都将被延迟加载。因此,一个对根对象的查询和另一个为每个根对象加载关联的查询。返回的100个对象表示一个初始查询,然后再返回100个附加查询,以获取每个对象的关联,n+1。

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

        11
  •  10
  •   Vlad Mihalcea    8 年前

    当您忘记获取一个关联,然后需要访问它时,就会发生N+1查询问题:

    List<PostComment> comments = entityManager.createQuery(
        "select pc " +
        "from PostComment pc " +
        "where pc.review = :review", PostComment.class)
    .setParameter("review", review)
    .getResultList();
    
    LOGGER.info("Loaded {} comments", comments.size());
    
    for(PostComment comment : comments) {
        LOGGER.info("The post title is '{}'", comment.getPost().getTitle());
    }
    

    它生成以下SQL语句:

    SELECT pc.id AS id1_1_, pc.post_id AS post_id3_1_, pc.review AS review2_1_
    FROM   post_comment pc
    WHERE  pc.review = 'Excellent!'
    
    INFO - Loaded 3 comments
    
    SELECT pc.id AS id1_0_0_, pc.title AS title2_0_0_
    FROM   post pc
    WHERE  pc.id = 1
    
    INFO - The post title is 'Post nr. 1'
    
    SELECT pc.id AS id1_0_0_, pc.title AS title2_0_0_
    FROM   post pc
    WHERE  pc.id = 2
    
    INFO - The post title is 'Post nr. 2'
    
    SELECT pc.id AS id1_0_0_, pc.title AS title2_0_0_
    FROM   post pc
    WHERE  pc.id = 3
    
    INFO - The post title is 'Post nr. 3'
    

    首先,Hibernate执行jpql查询和 PostComment 提取实体。

    然后,为每个 事后评论 ,关联的 post 属性用于生成包含 Post 标题。

    因为 邮递 关联未初始化,Hibernate必须获取 具有辅助查询的实体,以及 对于N 事后评论 实体,将执行n个以上的查询(因此n+1查询问题)。

    首先,你需要 proper SQL logging and monitoring 这样你就能发现这个问题。

    第二,这种问题最好被集成测试捕获。你可以使用 automatic JUnit assert to validate the expected count of generated SQL statements . 这个 db-unit project 已经提供了这个功能,而且它是开源的。

    当您确定了N+1查询问题时, you need to use a JOIN FETCH so that child associations are fetched in one query, instead of N . 如果需要获取多个子关联,最好在初始查询中获取一个集合,在第二个集合中获取一个辅助SQL查询。

        12
  •  9
  •   red-o-alf    11 年前

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

        13
  •  7
  •   Martin    13 年前

    一个百万富翁有N辆车。你想要所有(4)个轮子。

    一(1)个查询加载所有汽车,但对于每(n)辆汽车,将为加载车轮提交单独的查询。

    成本:

    假设索引适合RAM。

    1+n查询解析和规划+索引搜索和1+n+(n*4)板访问加载有效载荷。

    假设索引不适合RAM。

    最坏情况下,加载指数的1+N板通道的额外成本。

    总结

    瓶颈是板接入(大约每秒70次HDD随机接入) 一个热切的连接选择也将访问板1+n+(n*4)倍的有效载荷。 所以如果索引适合RAM——没问题,它足够快,因为只涉及RAM操作。

        14
  •  7
  •   bedrin    11 年前

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

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

    @Rule
    public final QueryCounter queryCounter = new QueryCounter();
    
    @Expectation(atMost = 3)
    @Test
    public void testInvokingDatabase() {
        // your JDBC or JPA code
    }
    
        15
  •  5
  •   Community Mohan Dere    9 年前

    正如其他人所说,更优雅的问题是,要么您拥有一个任意列的笛卡尔积,要么您正在进行n+1选择。可能是巨大的结果集,也可能是与数据库聊天。

    我很惊讶没有提到这个问题,但我是如何解决这个问题的… 我做了一个半临时的身份证表 . I also do this when you have the IN () clause limitation .

    这并不适用于所有情况(可能不是大多数情况),但如果您有许多子对象,例如笛卡尔积将失控(即许多 OneToMany 列结果的数目将是列数的乘法),它更像是一个类似批处理的作业。

    首先将父对象ID作为批插入到ID表中。 这个批处理ID是我们在应用程序中生成并保存的。

    INSERT INTO temp_ids 
        (product_id, batch_id)
        (SELECT p.product_id, ? 
        FROM product p ORDER BY p.product_id
        LIMIT ? OFFSET ?);
    

    现在为每个 单点 列你只是做一个 SELECT 在ID表上 INNER JOIN 用一个 WHERE batch_id= (反之亦然)。您只需要确保按ID列排序,因为这将使合并结果列更容易(否则,您将需要一个完整结果集的hashmap/表,这可能没有那么糟糕)。

    然后定期清理IDS表。

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

    现在,您正在执行的查询的数量是按OneTomany列的数量计算的。

        16
  •  1
  •   martins.tuga    13 年前

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

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

    要避免这种情况,请使关联获取变得更加迫切,以便Hibernate使用联接加载数据。

    但是请注意,如果很多时候您不访问相关联的轮子,最好让它保持懒惰,或者根据条件更改fetch类型。

    推荐文章