“N+1选择问题”在对象关系映射(ORM)讨论中通常被称为一个问题,我理解这与必须为对象世界中看似简单的东西进行大量数据库查询有关。
有人对这个问题有更详细的解释吗?
“N+1选择问题”在对象关系映射(ORM)讨论中通常被称为一个问题,我理解这与必须为对象世界中看似简单的东西进行大量数据库查询有关。
有人对这个问题有更详细的解释吗?
当前回答
以Matt Solnit为例,假设您将Car和Wheels之间的关联定义为LAZY,并且需要一些Wheels字段。这意味着在第一次选择后,休眠将为每辆车执行“select*from Wheels where car_id=:id”。
这使得每个N辆车的第一次选择和更多的1次选择,这就是为什么它被称为N+1问题的原因。
为了避免这种情况,请使关联获取变得急切,以便hibernate使用连接加载数据。
但请注意,如果您多次无法访问关联的Wheels,最好将其保持为LAZY或使用Criteria更改获取类型。
其他回答
这是对问题的一个很好的描述
现在您了解了这个问题,通常可以通过在查询中执行连接获取来避免。这基本上强制获取延迟加载的对象,因此数据在一个查询中而不是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查询问题是什么
当数据访问框架执行N个额外的SQL语句以获取执行主SQL查询时可能检索到的相同数据时,就会出现N+1查询问题。
N值越大,执行的查询越多,性能影响越大。而且,与可以帮助您查找运行缓慢的查询的慢速查询日志不同,N+1问题不会出现,因为每个单独的附加查询运行速度都足够快,不会触发慢速查询日志。
问题是执行大量额外的查询,总的来说,这些查询需要足够的时间来降低响应时间。
让我们考虑以下post和post_comments数据库表,它们形成了一对多的表关系:
我们将创建以下4个柱行:
INSERT INTO post (title, id)
VALUES ('High-Performance Java Persistence - Part 1', 1)
INSERT INTO post (title, id)
VALUES ('High-Performance Java Persistence - Part 2', 2)
INSERT INTO post (title, id)
VALUES ('High-Performance Java Persistence - Part 3', 3)
INSERT INTO post (title, id)
VALUES ('High-Performance Java Persistence - Part 4', 4)
此外,我们还将创建4个post_comment子记录:
INSERT INTO post_comment (post_id, review, id)
VALUES (1, 'Excellent book to understand Java Persistence', 1)
INSERT INTO post_comment (post_id, review, id)
VALUES (2, 'Must-read for Java developers', 2)
INSERT INTO post_comment (post_id, review, id)
VALUES (3, 'Five Stars', 3)
INSERT INTO post_comment (post_id, review, id)
VALUES (4, 'A great reference book', 4)
普通SQL的N+1查询问题
如果使用此SQL查询选择post_comments:
List<Tuple> comments = entityManager.createNativeQuery("""
SELECT
pc.id AS id,
pc.review AS review,
pc.post_id AS postId
FROM post_comment pc
""", Tuple.class)
.getResultList();
稍后,您决定获取每个post_comment的相关文章标题:
for (Tuple comment : comments) {
String review = (String) comment.get("review");
Long postId = ((Number) comment.get("postId")).longValue();
String postTitle = (String) entityManager.createNativeQuery("""
SELECT
p.title
FROM post p
WHERE p.id = :postId
""")
.setParameter("postId", postId)
.getSingleResult();
LOGGER.info(
"The Post '{}' got this review '{}'",
postTitle,
review
);
}
您将触发N+1查询问题,因为您执行了5(1+4)而不是一个SQL查询:
SELECT
pc.id AS id,
pc.review AS review,
pc.post_id AS postId
FROM post_comment pc
SELECT p.title FROM post p WHERE p.id = 1
-- The Post 'High-Performance Java Persistence - Part 1' got this review
-- 'Excellent book to understand Java Persistence'
SELECT p.title FROM post p WHERE p.id = 2
-- The Post 'High-Performance Java Persistence - Part 2' got this review
-- 'Must-read for Java developers'
SELECT p.title FROM post p WHERE p.id = 3
-- The Post 'High-Performance Java Persistence - Part 3' got this review
-- 'Five Stars'
SELECT p.title FROM post p WHERE p.id = 4
-- The Post 'High-Performance Java Persistence - Part 4' got this review
-- 'A great reference book'
修复N+1查询问题非常简单。您只需提取原始SQL查询中所需的所有数据,如下所示:
List<Tuple> comments = entityManager.createNativeQuery("""
SELECT
pc.id AS id,
pc.review AS review,
p.title AS postTitle
FROM post_comment pc
JOIN post p ON pc.post_id = p.id
""", Tuple.class)
.getResultList();
for (Tuple comment : comments) {
String review = (String) comment.get("review");
String postTitle = (String) comment.get("postTitle");
LOGGER.info(
"The Post '{}' got this review '{}'",
postTitle,
review
);
}
这次,只执行一个SQL查询来获取我们进一步感兴趣的所有数据。
JPA和Hibernate的N+1查询问题
在使用JPA和Hibernate时,有几种方法可以触发N+1查询问题,因此了解如何避免这些情况非常重要。
对于下一个示例,考虑我们将post和post_comments表映射到以下实体:
JPA映射如下所示:
@Entity(name = "Post")
@Table(name = "post")
public class Post {
@Id
private Long id;
private String title;
//Getters and setters omitted for brevity
}
@Entity(name = "PostComment")
@Table(name = "post_comment")
public class PostComment {
@Id
private Long id;
@ManyToOne
private Post post;
private String review;
//Getters and setters omitted for brevity
}
获取类型.EAGER
隐式或显式地为JPA关联使用FetchType.EAGER是一个坏主意,因为您将获取更多所需的数据。此外,FetchType.EAGER策略还容易出现N+1个查询问题。
不幸的是,@ManyToOne和@OneToOne关联默认使用FetchType.EAGER,因此如果映射如下所示:
@ManyToOne
private Post post;
您使用的是FetchType.EAGER策略,每当您在使用JPQL或Criteria API查询加载某些PostComment实体时忘记使用JOIN FETCH时:
List<PostComment> comments = entityManager
.createQuery("""
select pc
from PostComment pc
""", PostComment.class)
.getResultList();
您将触发N+1查询问题:
SELECT
pc.id AS id1_1_,
pc.post_id AS post_id3_1_,
pc.review AS review2_1_
FROM
post_comment pc
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 1
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 2
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 3
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 4
请注意执行的其他SELECT语句,因为在返回PostComment实体列表之前必须获取post关联。
与调用EntityManager的find方法时使用的默认获取计划不同,JPQL或Criteria API查询定义了Hibernate无法通过自动注入JOIN fetch来更改的显式计划。因此,您需要手动执行。
如果你根本不需要post关联,那么你在使用FetchType.EAGER时就不走运了,因为无法避免获取它。这就是为什么默认情况下最好使用FetchType.LAZY。
但是,如果您想使用后关联,那么可以使用JOIN FETCH来避免N+1查询问题:
List<PostComment> comments = entityManager.createQuery("""
select pc
from PostComment pc
join fetch pc.post p
""", PostComment.class)
.getResultList();
for(PostComment comment : comments) {
LOGGER.info(
"The Post '{}' got this review '{}'",
comment.getPost().getTitle(),
comment.getReview()
);
}
这次,Hibernate将执行一条SQL语句:
SELECT
pc.id as id1_1_0_,
pc.post_id as post_id3_1_0_,
pc.review as review2_1_0_,
p.id as id1_0_1_,
p.title as title2_0_1_
FROM
post_comment pc
INNER JOIN
post p ON pc.post_id = p.id
-- The Post 'High-Performance Java Persistence - Part 1' got this review
-- 'Excellent book to understand Java Persistence'
-- The Post 'High-Performance Java Persistence - Part 2' got this review
-- 'Must-read for Java developers'
-- The Post 'High-Performance Java Persistence - Part 3' got this review
-- 'Five Stars'
-- The Post 'High-Performance Java Persistence - Part 4' got this review
-- 'A great reference book'
获取类型.LAZY
即使您切换到对所有关联显式使用FetchType.LAZY,您仍然会遇到N+1问题。
这一次,后关联映射如下:
@ManyToOne(fetch = FetchType.LAZY)
private Post post;
现在,当您获取PostComment实体时:
List<PostComment> comments = entityManager
.createQuery("""
select pc
from PostComment pc
""", PostComment.class)
.getResultList();
Hibernate将执行一条SQL语句:
SELECT
pc.id AS id1_1_,
pc.post_id AS post_id3_1_,
pc.review AS review2_1_
FROM
post_comment pc
但是,如果之后,您将引用延迟加载的post关联:
for(PostComment comment : comments) {
LOGGER.info(
"The Post '{}' got this review '{}'",
comment.getPost().getTitle(),
comment.getReview()
);
}
您将获得N+1查询问题:
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 1
-- The Post 'High-Performance Java Persistence - Part 1' got this review
-- 'Excellent book to understand Java Persistence'
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 2
-- The Post 'High-Performance Java Persistence - Part 2' got this review
-- 'Must-read for Java developers'
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 3
-- The Post 'High-Performance Java Persistence - Part 3' got this review
-- 'Five Stars'
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 4
-- The Post 'High-Performance Java Persistence - Part 4' got this review
-- 'A great reference book'
由于后期关联是延迟获取的,因此在访问延迟关联时将执行一个辅助SQL语句,以便生成日志消息。
同样,修复方法包括在JPQL查询中添加JOIN FETCH子句:
List<PostComment> comments = entityManager.createQuery("""
select pc
from PostComment pc
join fetch pc.post p
""", PostComment.class)
.getResultList();
for(PostComment comment : comments) {
LOGGER.info(
"The Post '{}' got this review '{}'",
comment.getPost().getTitle(),
comment.getReview()
);
}
而且,就像FetchType.EAGER示例中一样,这个JPQL查询将生成一个SQL语句。
即使您正在使用FetchType.LAZY,并且没有引用双向@OneToOne JPA关系的子关联,您仍然可以触发N+1查询问题。
如何自动检测N+1查询问题
如果您想在数据访问层中自动检测N+1查询问题,可以使用db-util开源项目。
首先,您需要添加以下Maven依赖项:
<dependency>
<groupId>com.vladmihalcea</groupId>
<artifactId>db-util</artifactId>
<version>${db-util.version}</version>
</dependency>
之后,您只需使用SQLStatementCountValidator实用程序来断言生成的底层SQL语句:
SQLStatementCountValidator.reset();
List<PostComment> comments = entityManager.createQuery("""
select pc
from PostComment pc
""", PostComment.class)
.getResultList();
SQLStatementCountValidator.assertSelectCount(1);
如果您正在使用FetchType.EAGER并运行上述测试用例,则会出现以下测试用例失败:
SELECT
pc.id as id1_1_,
pc.post_id as post_id3_1_,
pc.review as review2_1_
FROM
post_comment pc
SELECT p.id as id1_0_0_, p.title as title2_0_0_ FROM post p WHERE p.id = 1
SELECT p.id as id1_0_0_, p.title as title2_0_0_ FROM post p WHERE p.id = 2
-- SQLStatementCountMismatchException: Expected 1 statement(s) but recorded 3 instead!
因为这个问题,我们离开了Django的ORM。基本上,如果你尝试
for p in person:
print p.car.colour
ORM将很高兴地返回所有人(通常作为Person对象的实例),但随后需要为每个Person查询car表。
一种简单且非常有效的方法是我称之为“扇形折叠”的方法,它避免了来自关系数据库的查询结果应该映射回组成查询的原始表的荒谬想法。
步骤1:宽选择
select * from people_car_colour; # this is a view or sql function
这将返回类似
p.id | p.name | p.telno | car.id | car.type | car.colour
-----+--------+---------+--------+----------+-----------
2 | jones | 2145 | 77 | ford | red
2 | jones | 2145 | 1012 | toyota | blue
16 | ashby | 124 | 99 | bmw | yellow
第2步:客观化
将结果吸入通用对象创建器中,并在第三项之后添加一个要拆分的参数。这意味着“jones”对象不会被制作多次。
步骤3:渲染
for p in people:
print p.car.colour # no more car queries
有关python的扇形折叠的实现,请参阅此网页。
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);
所有这些方法的工作原理都类似,它们使用左连接获取发出类似的数据库查询