编程

什么是 “N+1 Select 查询问题”?

1093 2024-05-20 03:06:00

1. 介绍

本文将研究 ORM 框架中的 N+1 查询问题。我们将从问题的概念性概述开始。还将讨论理论上可能的解决方案以及如何在实践中实现这些解决方案。

2. 理论

让我们从头开始,了解我们为什么会遇到这个问题。
我们知道 ORM 可以解决将堆中的实体映射到关系数据库中的 schema 对象问题。在关系数据库中,我们可以有外键,它表示对外键表的引用。它允许我们在 RDBMS 中对表之间的关系进行建模。例如,equipment  可能有对 manufacturer  和 equipment_items 表的外键引用:

现在,我们也希望这种关联以某种方式出现在我们的代码中。因此,我们在这里分别有 ManyToOneOneToMany 逻辑关联:

现在,假设我们想通过 id 从数据库中查询设备。我们可以做到:

SELECT e.* FROM equipment e WHERE e.id = ?

假设 ORM 为我们做了映射。但无论如何,我们只获取了设备(equipment)本身。我们不可能在应用程序中获得设备(equipment)的制造商(manufacturer)表中的信息,因为我们没有读取这些信息。如果我们需要这些信息,那么只需在应用程序需要稍后使用 SELECT 加载相应的制造商(manufacturer)表行:

SELECT m.* 
FROM manufacturer m 
INNER JOIN equipment e ON e.manufacturer_id = m.id 
WHERE e.id = ?

这被称为延迟加载,这是一些 ORM 框架的另一个特性。我们不会在这里讨论它,但此功能允许我们首先仅部分加载实体及其一些依赖项,以备以后进一步需要。因此,如果我们稍后也想获得 equipment_items 和 equipment,我们还必须使用另一个 SELECT

SELECT ei.* FROM equipment_item ei WHERE ei.equipment_id = ?

通过这种方式,我们最终加载了整个设备(equipment )实体。但问题是,我们本可以在一个简单的查询中完成

SELECT * 
FROM equipment e
LEFT JOIN manufacturer m ON e.manufacturer_id = m.id
LEFT JOIN equipment_item ei ON ei.equipment_id = e.id
WHERE e.id = ? 

这样,我们就拥有了所需的所有数据。我们只有一个查询,而不是 3 个查询。即使考虑到这一个查询的复杂性,使用一个带有 2 个 JOIN 的查询也比发出 3 个单独的查询快得多。这是因为获取结果不可避免地会带来抽象层的开销:

  • 应用代码
  • JDBC 驱动
  • 网络层
  • 数据库查询解析、调度和执行
  • 以及以上这些链路的回退…

我们重现了 “N+1 Select 查询问题”。这个问题基本上代表了一种情况,即我们从数据库加载实体时,使用 1 个Select 来获取实体本身,然后使用 N 个 Selects来获取其依赖项。因此,在我们的例子中,我们先获取设备实体,然后再使用 2 个 SELECT 语句来获取 2 个关联。这个问题往往会导致应用程序性能不佳。

3. 方案

需要明白的是,问题并不是因为懒加载本身造成的。问题是有时候我们只需要加载设。因此,一个简单的 SELECT 语句用于获取设备而不需要任何 JOIN 就足够了。懒加载有助于我们处理这种情况。但有时候我们也需要设备的部分或全部关联信息。而这正是懒加载阻碍我们的时候。

从理论上讲,解决方案可以简单到为每个场景使用不同的 SQL 查询。例如,如果我们只需要设备,那么我们只需要查询设备表。如果我们需要它与以及制造商信息,那么我们发布 SELECT 与制造商表的 JOIN。因此,我们仍然受益于懒加载,并且我们也不受 N+1 问题的影响。幸运的是,ORM 框架通常也支持这种解决方案。例如,在 Hibernate 中,从 JPA 2.1 起,我们可以使用 EntityGraph 来解决这个问题。在 C# 实体框架中,我们可以使用 Include 在需要的地方急切地获取一些惰性的关联。

4. 结论

在本文中,我们探讨了 “N+1 查询问题”。当通过单独的 SQL 查询加载实体的关联时,就会出现此问题。这通常是由于延迟加载,这本身就是一个很好的功能,但有时会造成这种复杂性。为了克服这一点,我们通常需要指示 ORM 框架在查询的基础上获取实体的特定部分。大多数 ORM 框架,如 Hibernate 或 Entity 框架,都允许我们做到这一点。