N+1 到底发了多少条 SQL?@EntityGraph 和 join fetch 有什么区别?同时 fetch 两个集合为什么报错?batch fetch 值不值得开?这篇在 Spring Boot 4.0.8、Hibernate 7、MySQL 上用 200 个作者 × 10 本书逐个抓 SQL。
authorRepo.findAll() 之后遍历每个作者的 books.size() ⇒ 2000 本书;发出 201 条 SQL(2 种)
select … from author a1_0
200 × select … from book b1_0 where b1_0.author_id=?
bookRepo.findAll() 之后访问每本书的 author(@ManyToOne 默认 EAGER) ⇒ 200 个作者;发出 201 条 SQL(2 种)
select … from book b1_0
200 × select … from author a1_0 where a1_0.id=?一对多的 lazy 和多对一的 EAGER 都会出现 N+1。
@Query("select a from Author a join fetch a.books") ⇒ 200 个作者,2000 本书;发出 1 条 SQL(1 种)
select … from author a1_0 join book b1_0 on a1_0.id=b1_0.author_id
@EntityGraph(attributePaths = "books") + @Query("select a from Author a") ⇒ 200 个作者,2000 本书;发出 1 条 SQL(1 种)
select … from author a1_0 left join book b1_0 on a1_0.id=b1_0.author_id
@EntityGraph 加在派生查询 findByNameStartingWith("author1") ⇒ 111 个作者,1110 本书;发出 1 条 SQL(1 种)
select … from author a1_0 left join book b1_0 on a1_0.id=b1_0.author_id where a1_0.name like ? escape '\\'
bookRepo.withAuthor()(join fetch 把 ManyToOne 一起取回) ⇒ 200 个作者;发出 1 条 SQL(1 种)
select … from book b1_0 join author a1_0 on a1_0.id=b1_0.author_id
DTO 投影 select new AuthorDto(name, count(b)) … group by ⇒ 200 行 DTO;发出 1 条 SQL(1 种)
select … from author a1_0 join book b1_0 on a1_0.id=b1_0.author_id group by a1_0.name
接口投影 findAllProjectedBy()(只有 getName) ⇒ 200 行;发出 1 条 SQL(1 种)
select … from author a1_0join fetch、@EntityGraph、DTO 与接口投影都是 1 条 SQL;@EntityGraph 用 left join。
Author:join fetch a.books join fetch a.awards(两个 List) ⇒ 抛 InvalidDataAccessApiUsageException:cannot simultaneously fetch multiple bags: [demo.Author.awards, demo.Author.books];发出 0 条 SQL(0 种)
Poet:join fetch p.poems join fetch p.prizes(两个 Set) ⇒ 20 个诗人;发出 1 条 SQL(1 种)
select … from poet p1_0 join poem p2_0 on p1_0.id=p2_0.poet_id join prize p3_0 on p1_0.id=p3_0.poet_id
对应的 SQL 在数据库里实际返回的行数:Poet join poem join prize = 300 行(20 个诗人 × 5 首 × 3 个奖);Author join book = 2000 行两个 List 一起 fetch 抛异常;两个 Set 可以,但行数是笛卡尔积。
N+1(findAll 再遍历 books):25.9 ms join fetch:7.5 ms @EntityGraph:7.7 ms
N+1 约是 join fetch 的 3.5 倍(本机);远程数据库差距更大。
findAll() 后遍历每个作者的 books.size() ⇒ 2000 本书;发出 5 条 SQL(2 种)
select … from author a1_0
4 × select … from book b1_0 where b1_0.author_id in (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
bookRepo.findAll() 后访问每本书的 author ⇒ 200 个作者;发出 5 条 SQL(2 种)
select … from book b1_0
4 × select … from author a1_0 where a1_0.id in (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)default_batch_fetch_size 把 N+1 变成 1 + N/批大小。