一直在做后台管理系统的同学应该都有同感,列表页的需求翻来覆去就那几件事:主子表数据一起展示、关联用户名称、统计子表数量或金额,然后还必须分页。刚开始用 MyBatis 的时候,很多人会把多表查询和分页查询分开看,等真正写起来才发现,这两个需求凑到一起才是噩梦源头。
我最近在项目里重构了一批订单列表接口,表结构并不复杂,无非是订单主表、订单明细表、用户表,但接手的代码里用了一个巨大的 JOIN 查询,主表关联明细表之后行数膨胀,分页总数怎么都对不上,接口响应从几十毫秒劣化到两秒以上。这篇文章我想把 MyBatis 里多表查询、分页查询相关的设计思路、配置细节和优化手段完整捋一遍,结合我踩过的坑来讲,适合刚接触 MyBatis 的开发者,也适合已经在写业务但没系统梳理过这类问题的朋友。
1. 多表查询的返回结构定不下来,SQL 写得再顺也是白搭
1.1 先回答一个关键问题:你要的是“平铺行”还是“嵌套对象”
很多 MyBatis 新人拿到多表查询需求时,第一反应就是写一个 JOIN,然后 resultType 一个 DTO。比如订单列表要带用户昵称,最常见的写法是这样的:
xml复制<select id="selectOrderWithUser" resultType="com.example.dto.OrderWithUserDTO">
SELECT
o.id,
o.order_no,
o.amount,
u.nickname AS userName
FROM t_order o
LEFT JOIN t_user u ON o.user_id = u.id
</select>
这种写法在数据量小、表关系简单的时候没什么问题。但如果页面要展示的不只是“用户昵称”这一个字段,而是还要展示“该订单下的商品明细列表”,问题就来了。JOIN 的结果会把订单主记录复制成多条,每条对应一个商品明细,前端拿到的是平铺数据,得自己按订单 ID 分组。
于是很多人开始在 Java 代码里做二次组装:
java复制Map<Long, OrderVO> map = new LinkedHashMap<>();
for (OrderWithItemDTO row : rows) {
OrderVO vo = map.computeIfAbsent(row.getId(), OrderVO::new);
vo.getItemList().add(row.getItemName());
}
这种手工分组逻辑一多,代码就变得很脆。而且它和 SQL 的字段顺序、返回结构强耦合,加一个关联表就要改一大片。
反过来,MyBatis 本来提供了 resultMap 来解决“数据库行到嵌套 Java 对象”的映射问题。真正合理的思路应该是:先想清楚你的接口到底需要一棵什么样的对象树,再决定 SQL 是 JOIN 还是分步查询,最后用 resultMap 把这棵树拼出来。
我个人的经验是,可以把多表查询拆成三类:
| 需求特征 | 推荐做法 | 原因 |
|---|---|---|
| 列表页只需要主表字段加另一张表的几个冗余字段 | JOIN 后 resultType 一个扁平 DTO | 结构简单,映射成本低 |
| 需要完整的父子级对象,比如订单带明细列表 | JOIN + resultMap 嵌套映射 | 一次查出完整对象树,避免循环查库 |
| 子集合量大,或获取某些关联数据不是列表主查询的刚需 | 先分页查主表,再用 IN 批量补查关联数据 | 避免 JOIN 导致行数膨胀,也避免 N+1 |
1.2 一对多 JOIN 是分页查询最大的“隐形杀手”
我之前排查过一个总数对不上的接口。场景是订单表关联明细表,业务上想统计每个订单的商品行数、金额合计,于是 SQL 写成了:
sql复制SELECT o.*, i.id AS item_id, i.quantity, i.price
FROM t_order o
LEFT JOIN t_order_item i ON i.order_id = o.id
表面看起来没毛病,但这条 SQL 查出来的记录数是“订单数 + 明细数”,而不是“订单数”。如果我用 PageHelper 自动分页,PageHelper 会把 count 查询自动生成为:
sql复制SELECT COUNT(0)
FROM t_order o
LEFT JOIN t_order_item i ON i.order_id = o.id
这个 count 结果是明细行总数,不是订单总数。前端看到的结果自然就是第二页开始全乱了。
这种问题不是 PageHelper 独有的,只要你对 JOIN 结果做 LIMIT,同样存在:LIMIT 截断的是“关联后的行”,不是“主表的行”。
所以一旦表关系变成了一对多,我强烈建议第一时间想清楚:分页的单位到底是哪张表的记录。如果分页对象是订单,那明细表的行就不能直接平铺在主查询结果里。解决办法不外乎两种,要么在查询里用 GROUP BY 把明细聚合到每行一个订单,要么主表分页后再单独补查明细。后面我会专门讲这部分。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
