N + 1 SELECT查询问题是什么?

时间:2020-03-06 14:23:36  来源:igfitidea点击:

SELECT N + 1在对象关系映射(ORM)讨论中通常被认为是一个问题,我理解它与必须对对象世界中看起来很简单的某些事物进行大量数据库查询有关。

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

解决方案

假设我们有COMPANY和EMPLOYEE。 COMPANY有许多员工(即EMPLOYEE有一个字段COMPANY_ID)。

在某些O / R配置中,当我们有一个映射的Company对象并访问其Employee对象时,O / R工具将为每个员工做一次选择,如果我们只是使用直接SQL进行操作,则可以选择*来自company_id = XX`的员工。因此,N个(员工)加1个(公司)

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

假设我们有一个"汽车"对象集合(数据库行),而每个"汽车"都有一个"车轮"对象集合(也有行)。换句话说,"汽车"->"车轮"是一对多关系。

现在,假设我们需要遍历所有汽车,并为每辆汽车打印出车轮清单。天真的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

这样就得到了一个结果集,其中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的优点是降低了查询复杂性,我们可以使用延迟加载,其中子结果集仅在第一次请求时才加载。

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

现在我们已经了解了问题,通常可以通过在查询中进行联接提取来避免此问题。基本上,这将强制获取延迟加载的对象,因此将在一个查询而不是n + 1个查询中检索数据。希望这可以帮助。

提供的链接非常简单地说明了n + 1问题。如果将其应用于Hibernate,则基本上是在谈论同一件事。查询对象时,将加载实体,但是任何关联(除非另行配置)都将延迟加载。因此,一个查询根对象,另一个查询以加载每个根对象的关联。返回的100个对象意味着一个初始查询,然后是100个其他查询以获取每个对象的关联,n + 1.

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

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

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

在我看来,Hibernate Pitfall撰写的文章:为什么关系应该变得懒惰与真正的N + 1问题完全相反。

如果我们需要正确的解释,请参考Hibernate第19章:改进性能获取策略。

Select fetching (the default) is
  extremely vulnerable to N+1 selects
  problems, so we might want to enable
  join fetching