N+1 是 JPA 最常见的性能问题。这篇用 1000 个作者的真实数据,量出它从哪里来(懒加载集合与 EAGER 的 @ManyToOne)、几种修复方式的 SQL 数与耗时,以及 join fetch 带来的两个新问题:笛卡尔积与内存分页。
1 条查询 select a from Author a(不访问 books) SQL 语句 1 条;耗时 5.1 ms(7 次中位数);结果统计 1000 查询作者 + 逐个访问 a.books.size()(N+1) SQL 语句 1001 条;耗时 201.5 ms(7 次中位数);结果统计 5000
em.find(Review, 1)(find 会 join 出作者) SQL 语句 1 条;耗时 0.2 ms(7 次中位数);结果统计 1 JPQL select r from Review r(默认 EAGER 的作者) SQL 语句 1001 条;耗时 35.9 ms(7 次中位数);结果统计 1000 JPQL select r from Review r join fetch r.author SQL 语句 1 条;耗时 2.4 ms(7 次中位数);结果统计 1000
基线:N+1(plain lazy) SQL 语句 1001 条;耗时 183.3 ms(7 次中位数);结果统计 5000 join fetch(select distinct a … join fetch a.books) SQL 语句 1 条;耗时 9.7 ms(7 次中位数);结果统计 5000 @BatchSize(size = 25) SQL 语句 41 条;耗时 31.5 ms(7 次中位数);结果统计 5000 @Fetch(FetchMode.SUBSELECT) SQL 语句 2 条;耗时 5.8 ms(7 次中位数);结果统计 5000 EntityGraph(命名图 author.books,fetchgraph 提示) SQL 语句 1 条;耗时 6.0 ms(7 次中位数);结果统计 5000 DTO 投影(select new AuthorDto(a.id, a.name, count(b)) … group by) SQL 语句 1 条;耗时 1.4 ms(7 次中位数);结果统计 5000
两个 List 同时 join fetch:IllegalArgumentException ← MultipleBagFetchException:cannot simultaneously fetch multiple bags: [Author.awards, Author.books] Set join fetch books:返回 1000 个作者,SQL 1 条,数据库返回的行数 5000,耗时 18.1 ms Set 同时 join fetch books 与 awards:返回 1000 个作者,SQL 1 条,数据库返回的行数 20000,耗时 50.7 ms
不带 fetch 的分页查询 setMaxResults(10):返回 10 个作者,SQL 1 条,耗时 3.9 ms,一级缓存里有 10 个实体 带集合 join fetch 的分页查询 setMaxResults(10):返回 10 个作者,SQL 1 条,耗时 9.0 ms,一级缓存里有 6000 个实体 [日志警告] WARN: HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory