“N+1选择问题”在对象关系映射(ORM)讨论中通常被称为一个问题,我理解这与必须为对象世界中看似简单的东西进行大量数据库查询有关。
有人对这个问题有更详细的解释吗?
“N+1选择问题”在对象关系映射(ORM)讨论中通常被称为一个问题,我理解这与必须为对象世界中看似简单的东西进行大量数据库查询有关。
有人对这个问题有更详细的解释吗?
当前回答
这是对问题的一个很好的描述
现在您了解了这个问题,通常可以通过在查询中执行连接获取来避免。这基本上强制获取延迟加载的对象,因此数据在一个查询中而不是n+1个查询中检索。希望这有帮助。
其他回答
假设你有公司和雇员。公司有许多雇员(即雇员有一个字段COMPANY_ID)。
在某些O/R配置中,当您有一个映射的Company对象并访问其Employee对象时,O/R工具将为每个员工执行一次选择,如果您只是在直接SQL中执行操作,则可以从Company_id=XX的员工中选择*。因此,N(员工人数)加1(公司)
这就是EJB实体bean的初始版本是如何工作的。我相信像Hibernate这样的东西已经解决了这个问题,但我不太确定。大多数工具通常包含有关其映射策略的信息。
假设您有一组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章。
这是对问题的一个很好的描述
现在您了解了这个问题,通常可以通过在查询中执行连接获取来避免。这基本上强制获取延迟加载的对象,因此数据在一个查询中而不是n+1个查询中检索。希望这有帮助。
在不深入技术堆栈实现细节的情况下,从架构上讲,N+1问题至少有两种解决方案:
只有1个-大查询-带有联接。这使得大量信息从数据库传输到应用程序层,特别是如果有多个子记录。数据库的典型结果是一组行,而不是对象图(有不同的DB系统的解决方案)有两个(或更多需要连接的子级)查询-1个用于父级,在有了它们之后-通过ID查询子级并映射它们。这将最大限度地减少DB和APP层之间的数据传输。
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!