“N+1选择问题”在对象关系映射(ORM)讨论中通常被称为一个问题,我理解这与必须为对象世界中看似简单的东西进行大量数据库查询有关。
有人对这个问题有更详细的解释吗?
“N+1选择问题”在对象关系映射(ORM)讨论中通常被称为一个问题,我理解这与必须为对象世界中看似简单的东西进行大量数据库查询有关。
有人对这个问题有更详细的解释吗?
当前回答
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的优点是减少了查询复杂性,并且可以使用延迟加载,其中子结果集仅在第一次请求时加载。
其他回答
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的优点是减少了查询复杂性,并且可以使用延迟加载,其中子结果集仅在第一次请求时加载。
正如其他人更优雅地指出的那样,问题是您要么拥有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列的数量。
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);
所有这些方法的工作原理都类似,它们使用左连接获取发出类似的数据库查询
假设您有一组Car对象(数据库行),每个Car都有一组Wheel对象(也有行)。换句话说,汽车→ 轮子是一对多的关系。
现在,假设您需要遍历所有汽车,并为每辆汽车打印出车轮列表。天真的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章。
在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个结果。
在迭代之前加载所有数据。