N+1 查询问题
ORM 懒加载陷阱;Join Fetch 与批量查询。
30 分钟
N+1 查询问题
接口响应随列表长度线性变慢,往往是 N+1 查询:一次查主表,再对每条记录各查一次关联表。ORM 懒加载让这个问题隐蔽却致命——是 building-for-scale 中应用层最常见的性能陷阱,与 orm-essentials 紧密相关。
学习目标
- 在日志/SQL 监控中识别 N+1 模式(1 + N 条 SELECT)
- 使用 Join Fetch、@EntityGraph、批量 IN 查询三种修复手段
- 在 DTO 投影与懒加载之间做合理权衡
核心概念
典型场景:查询 100 个订单,再 lazy load 每个订单的用户。
SELECT * FROM orders LIMIT 100; -- 1 次
SELECT * FROM users WHERE id = ?; -- ×100 次 → 共 101 次
| 修复方式 | JPA 示例思路 | 适用 |
|---|---|---|
| Join Fetch | @Query("SELECT o FROM Order o JOIN FETCH o.user") | 关联必取、结果集可控 |
| @EntityGraph | 声明 fetch graph 覆盖 lazy | 复用 Repository |
| Batch Size | @BatchSize(size=50) 合并 IN 查询 | 不能 JOIN 时 |
// Hibernate 批量加载:@BatchSize on User
// 101 次 → 约 1 + ceil(100/50) = 3 次
Open Session In View(OSIV) 会掩盖 N+1:页面渲染时才触发 lazy load,把 DB 压力拖到视图层。生产建议关闭 OSIV,在 Service 层显式加载所需关联。
排查工具:Hibernate statistics、P6Spy、Micrometer + JDBC 追踪。压测时对比修复前后 P99。
与扩展
N+1 不解决,加再多 App 实例也只是 并行打爆数据库。配合 scale-cache 缓存热点关联,配合 postgresql-essentials 加索引。
练习
- 打开一个列表 API,数日志里 SELECT 条数是否等于 1 + 列表长度
- 用 Join Fetch 改写,对比
EXPLAIN ANALYZE与响应时间 - 在团队规范里加一条:PR 必须附 SQL 条数截图或断言
小结
N+1 是 ORM 送的「隐形账单」:一条列表接口就能打满连接池——显式加载或批量查,别靠懒加载碰运气。
加载本节练习…