“N+1选择问题”在对象关系映射(ORM)讨论中通常被称为一个问题,我理解这与必须为对象世界中看似简单的东西进行大量数据库查询有关。
有人对这个问题有更详细的解释吗?
“N+1选择问题”在对象关系映射(ORM)讨论中通常被称为一个问题,我理解这与必须为对象世界中看似简单的东西进行大量数据库查询有关。
有人对这个问题有更详细的解释吗?
当前回答
Hibernate和Spring数据JPA中的N+1问题
N+1问题是对象关系映射中的一个性能问题,它为应用层的单个选择查询在数据库中触发多个选择查询(准确地说是N+1,其中N=表中的记录数)。Hibernate&Spring Data JPA提供了多种方法来捕获和解决这个性能问题。
什么是N+1问题?
为了理解N+1问题,让我们考虑一个场景。假设我们有一个映射到数据库中DB_User表的User对象集合,每个用户都有一个使用联接表DB_User_Role映射到DB_Role表的集合或角色。在ORM级别,用户与角色具有多对多关系。
Entity Model
@Entity
@Table(name = "DB_USER")
public class User {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
private String name;
@ManyToMany(fetch = FetchType.LAZY)
private Set<Role> roles;
//Getter and Setters
}
@Entity
@Table(name = "DB_ROLE")
public class Role {
@Id
@GeneratedValue(strategy= GenerationType.AUTO)
private Long id;
private String name;
//Getter and Setters
}
一个用户可以有许多角色。角色加载缓慢。现在假设我们想从这个表中获取所有用户,并为每个用户打印角色。非常幼稚的对象关系实现可能是-具有findAllBy方法的UserRepository
public interface UserRepository extends CrudRepository<User, Long> {
List<User> findAllBy();
}
ORM执行的等效SQL查询为:
首先获取所有用户(1)
Select * from DB_USER;
然后获取每个用户执行N次的角色(其中N是用户数)
Select * from DB_USER_ROLE where userid = <userid>;
因此,我们需要为User选择一个选项,为每个用户提取角色选择N个选项,其中N是用户总数。这是ORM中的一个经典N+1问题。
如何识别?
Hibernate提供了跟踪选项,可以在控制台/日志中启用SQL日志记录。使用日志,您可以很容易地看到hibernate是否为给定的调用发出N+1个查询。
如果您在给定的select查询中看到多个SQL条目,那么很可能是由于N+1问题。
N+1分辨率
在SQL级别,ORM要避免N+1,需要实现的是启动一个连接两个表的查询,并在单个查询中获得组合结果。
获取联接SQL,用于检索单一查询中的所有内容(用户和角色)
OR普通SQL
select user0_.id, role2_.id, user0_.name, role2_.name, roles1_.user_id, roles1_.roles_id from db_user user0_ left outer join db_user_roles roles1_ on user0_.id=roles1_.user_id left outer join db_role role2_ on roles1_.roles_id=role2_.id
Hibernate和Spring Data JPA提供了解决N+1 ORM问题的机制。
1.弹簧数据JPA方法:
如果我们使用的是SpringDataJPA,那么我们有两个选项来实现这一点-使用EntityGraph或使用带有fetch连接的select查询。
public interface UserRepository extends CrudRepository<User, Long> {
List<User> findAllBy();
@Query("SELECT p FROM User p LEFT JOIN FETCH p.roles")
List<User> findWithoutNPlusOne();
@EntityGraph(attributePaths = {"roles"})
List<User> findAll();
}
N+1个查询在数据库级别使用左连接获取发出,我们使用attributePaths解决N+1问题,Spring Data JPA避免N+1问题
2.休眠方法:
如果它是纯Hibernate,那么以下解决方案将起作用。
使用HQL:
from User u *join fetch* u.roles roles roles
使用标准API:
Criteria criteria = session.createCriteria(User.class);
criteria.setFetchMode("roles", FetchMode.EAGER);
所有这些方法的工作原理都类似,它们使用左连接获取发出类似的数据库查询
其他回答
这是对问题的一个很好的描述
现在您了解了这个问题,通常可以通过在查询中执行连接获取来避免。这基本上强制获取延迟加载的对象,因此数据在一个查询中而不是n+1个查询中检索。希望这有帮助。
与产品有一对多关系的供应商。一个供应商拥有(供应)许多产品。
***** 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选择问题!
在Phabricator文档中可以找到问题的简短解释:
N+1查询问题是一个常见的性能反模式。它看起来像这样:$cats=load_cats();foreach($cat作为$cat){$cats_hats=>load_hats_for_cat($cat);// ...}假设load_cats()的实现归结为:从猫的位置选择*。。。..和load_hats_for_cat($cat)的实现如下:SELECT*FROM hat WHERE catID=。。。..当代码执行时,您将发出“N+1”个查询,其中N是猫的数量:从猫的位置选择*。。。SELECT*FROM hat WHERE catID=1SELECT*FROM hat WHERE catID=2SELECT*FROM hat WHERE catID=3从帽子中选择*,其中catID=4...
解决方案:
发出一个返回100个结果的查询比发出发出100个查询,每个查询返回1个结果。
在迭代之前加载所有数据。
提供的链接有一个非常简单的n+1问题示例。如果你将它应用于Hibernate,它基本上是在谈论相同的事情。查询对象时,实体将被加载,但任何关联(除非另有配置)都将被延迟加载。因此,一个查询用于根对象,另一个查询加载每个根对象的关联。返回的100个对象意味着一个初始查询,然后是100个附加查询,以获得每个对象的关联,n+1。
http://pramatr.com/2009/02/05/sql-n-1-selects-explained/
在不深入技术堆栈实现细节的情况下,从架构上讲,N+1问题至少有两种解决方案:
只有1个-大查询-带有联接。这使得大量信息从数据库传输到应用程序层,特别是如果有多个子记录。数据库的典型结果是一组行,而不是对象图(有不同的DB系统的解决方案)有两个(或更多需要连接的子级)查询-1个用于父级,在有了它们之后-通过ID查询子级并映射它们。这将最大限度地减少DB和APP层之间的数据传输。