“N+1选择问题”在对象关系映射(ORM)讨论中通常被称为一个问题,我理解这与必须为对象世界中看似简单的东西进行大量数据库查询有关。
有人对这个问题有更详细的解释吗?
“N+1选择问题”在对象关系映射(ORM)讨论中通常被称为一个问题,我理解这与必须为对象世界中看似简单的东西进行大量数据库查询有关。
有人对这个问题有更详细的解释吗?
当前回答
查看Ayende关于以下主题的帖子:打击NHibernate中的选择N+1问题。
基本上,当使用像NHibernate或EntityFramework这样的ORM时,如果您有一对多(主详细信息)关系,并且希望列出每个主记录的所有详细信息,则必须对数据库进行N+1次查询调用,“N”是主记录的数量:1次查询获取所有主记录,N次查询,每个主记录一次,获取每个主记录所有详细信息。
更多数据库查询调用→ 延迟时间更长→ 降低了应用程序/数据库性能。
然而,ORM可以选择避免这个问题,主要是使用JOIN。
其他回答
查看Ayende关于以下主题的帖子:打击NHibernate中的选择N+1问题。
基本上,当使用像NHibernate或EntityFramework这样的ORM时,如果您有一对多(主详细信息)关系,并且希望列出每个主记录的所有详细信息,则必须对数据库进行N+1次查询调用,“N”是主记录的数量:1次查询获取所有主记录,N次查询,每个主记录一次,获取每个主记录所有详细信息。
更多数据库查询调用→ 延迟时间更长→ 降低了应用程序/数据库性能。
然而,ORM可以选择避免这个问题,主要是使用JOIN。
在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的推广
N+1问题是一个ORM特有的问题名称,它将可以在服务器上合理执行的循环移动到客户端。通用问题不是ORM特有的,您可以通过任何远程API解决。在本文中,我展示了如果您调用一个API N次而不是仅调用1次,JDBC往返是如何代价高昂的。示例中的区别在于您是否调用Oracle PL/SQL过程:
dbms_output.get_lines(调用一次,接收N个项目)dbms_output.get_line(调用N次,每次接收1项)
它们在逻辑上是等价的,但由于服务器和客户端之间的延迟,您需要在循环中添加N个延迟等待,而不是只等待一次。
ORM案例
事实上,ORM-y N+1问题甚至不是ORM特有的,您也可以通过手动运行自己的查询来实现,例如,当您在PL/SQL中执行以下操作时:
-- This loop is executed once
for parent in (select * from parent) loop
-- This loop is executed N times
for child in (select * from child where parent_id = parent.id) loop
...
end loop;
end loop;
使用联接(在本例中)实现这一点会更好:
for rec in (
select *
from parent p
join child c on c.parent_id = p.id
)
loop
...
end loop;
现在,循环只执行一次,并且循环的逻辑已经从客户端(PL/SQL)移动到服务器(SQL),这甚至可以以不同的方式对其进行优化,例如,通过运行哈希连接(O(N))而不是嵌套循环连接(带索引的O(N log N))
自动检测N+1个问题
如果您使用的是JDBC,可以在后台使用jOOQ作为JDBC代理来自动检测N+1问题。jOOQ的解析器规范化您的SQL查询,并缓存有关连续执行父查询和子查询的数据。如果您的查询不完全相同,但在语义上是等价的,这甚至可以起作用。
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);
所有这些方法的工作原理都类似,它们使用左连接获取发出类似的数据库查询
我不能直接评论其他答案,因为我没有足够的声誉。但值得注意的是,这个问题本质上只会出现,因为从历史上看,很多dbm在处理连接时都非常糟糕(MySQL是一个特别值得注意的例子)。因此,n+1通常比join快得多。然后有一些方法可以改进n+1,但仍然不需要连接,这就是最初的问题所在。
然而,在连接方面,MySQL现在比过去好了很多。当我第一次学习MySQL时,我经常使用联接。然后我发现它们有多慢,并在代码中改用n+1。但是,最近,我又回到了连接,因为MySQL现在在处理它们方面比我刚开始使用它时要好得多。
现在,从性能角度来看,在一组索引正确的表上进行简单联接很少有问题。如果它确实影响了性能,那么使用索引提示通常可以解决这些问题。
MySQL的一个开发团队在这里讨论了这一点:
http://jorgenloland.blogspot.co.uk/2013/02/dbt-3-q3-6-x-performance-in-mysql-5610.html
所以总结是:如果您过去一直在避免连接,因为MySQL的性能糟糕,那么请在最新版本上重试。你可能会感到惊喜。