bigywater.tech
Java · 421

JPA N+1 查询:对象导航如何变成大量 SQL

N+1 是 JPA 最常见的性能问题。这篇用 1000 个作者的真实数据,量出它从哪里来(懒加载集合与 EAGER 的 @ManyToOne)、几种修复方式的 SQL 数与耗时,以及 join fetch 带来的两个新问题:笛卡尔积与内存分页。

1001 条
1000 个作者逐个访问 a.books.size() 发出的 SQL 数(join fetch / EntityGraph / DTO 都是 1 条)
20000 行
两个 Set 同时 join fetch(5 本书 × 4 个奖项 × 1000 个作者)数据库返回的行数(只 fetch books 是 5000 行)
6000 个实体
带集合 join fetch 的 setMaxResults(10) 在一级缓存里实际加载的实体数(警告 HHH90003004)

经典 N+1

实测 · 本机执行输出
  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

EAGER 的 @ManyToOne

实测 · 本机执行输出
  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

实测 · 本机执行输出
  不带 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

分步演示

JPA N+1 查询
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。Hibernate ORM 6.6.4.Final、PostgreSQL JDBC 42.7.4、本机 PostgreSQL 16.15(网络往返几乎为 0,真实环境差距更大);数据:1000 个作者 × 5 本书 × 4 个奖项 + 1000 条评论;耗时是 7 次中位数,SQL 数来自 Hibernate Statistics
    • 没有验证:Spring Data JPA 的 @EntityGraph 与 Specification、二级缓存对 N+1 的缓解、hibernate.default_batch_fetch_size 全局设置、@OneToOne 的 N+1、Hibernate 6.6 的 hibernate.query.fail_on_pagination_over_collection_fetch 开关、生产环境的慢查询日志与 N+1 检测工具
    京ICP备2026067502号-1京公网安备11010502063487号