MyBatis 与 SQL 层面关键技术详解

MyBatis 与 SQL 层面关键技术详解

一、MyBatis 动态 SQL

动态 SQL 是 MyBatis 最核心的能力:根据入参动态拼接 SQL,而不是写死。用于"筛选条件有就拼、没有就不拼"的场景。

1.1 <if> --- 条件拼接

入参有值才拼这段 SQL。这是"可选筛选条件"的基础:

xml 复制代码
<if test="paramDto.sourceSystem != null and paramDto.sourceSystem != ''">
  AND o.source_system = #{paramDto.sourceSystem}
</if>
  • test 里写 OGNL 表达式判断
  • 字符串要判 != null and != ''(空字符串也不拼)
  • 集合要判 != null and .size() > 0

1.2 <foreach> --- 集合展开为 IN

把 List 入参展开成 IN (?, ?, ?)

xml 复制代码
<if test="paramDto.warehouseCodes != null and paramDto.warehouseCodes.size() > 0">
  AND o.warehouse_code IN
  <foreach collection="paramDto.warehouseCodes" item="code"
           open="(" separator="," close=")">
    #{code}
  </foreach>
</if>
  • collection = 集合参数名,item = 每个元素的别名
  • open/close/separator = 拼成 (a, b, c)

1.3 <where> --- 智能处理 WHERE 和 AND

自动加 WHERE,并去掉紧跟其后的多余 AND/OR

xml 复制代码
<where>
  <if test="...">AND a = #{a}</if>
  <if test="...">AND b = #{b}</if>
</where>
<!-- 若第一个条件命中,<where> 会把开头的 AND 去掉,避免 WHERE AND a=... 语法错误 -->

1.4 <sql> + <include> --- SQL 片段复用(关键)

把公共的 FROM/JOIN、WHERE 条件抽成片段,多个查询复用。比如列表接口和合计接口就靠它保证筛选口径完全一致

xml 复制代码
<sql id="fromJoins">
  FROM main_table o
  LEFT JOIN member_base mb ON mb.member_id = o.member_id
</sql>

<sql id="whereConditions">
  <where>
    <if test="...">AND ...</if>
  </where>
</sql>

<!-- 列表查询 -->
<select id="listPager" resultType="...">
  SELECT o.*, mb.name <include refid="fromJoins"/> <include refid="whereConditions"/>
  ORDER BY o.create_time DESC
</select>

<!-- 合计查询:复用同一片段,改口径不会漏改 -->
<select id="listSum" resultType="...">
  SELECT SUM(o.qty), SUM(o.amount) <include refid="fromJoins"/> <include refid="whereConditions"/>
</select>

价值:改一处 where 条件,列表和合计同时生效,避免两处口径漂移。

注:

博客:

https://blog.csdn.net/badao_liumang_qizhi

二、#{} vs ${}(安全关键)

写法 机制 用途 风险
#{param} 预编译占位符 (PreparedStatement 的 ?),值作为参数传入 绝大多数场景,尤其是外部入参 无 SQL 注入,安全
${param} 字符串直接拼接 动态表名/列名/排序方向等无法用占位符的场景 有 SQL 注入风险,禁止拼外部输入
xml 复制代码
WHERE code = #{code}          <!-- ✅ 预编译,安全 -->
ORDER BY ${sortColumn}        <!-- ⚠️ 拼接,sortColumn 必须白名单校验 -->

原则:能用 #{} 就绝不用 ${} ,外部入参一律 #{}


三、PageHelper 物理分页

3.1 工作原理

PageHelper 是分页插件(MyBatis Interceptor),拦截紧跟其后执行的第一条查询,自动:

  1. 生成并执行一条 SELECT COUNT(*) ...(算总数)
  2. 在原 SQL 尾部拼 LIMIT ?, ?(取当页数据)
java 复制代码
PageHelper.startPage(pageNum, pageSize);      // 声明下一条查询要分页
List<Xxx> list = mapper.listXxx(param);       // 这条被拦截,自动分页
PageInfo<Xxx> page = new PageInfo<>(list);     // 拿到 total/pages/list

3.2 物理分页 vs 逻辑分页

  • 物理分页 :SQL 层 LIMIT 只取当页数据(PageHelper 用的这种),数据量大也不吃内存。
  • 逻辑分页 :查全部再在内存 subList(MyBatis 的 RowBounds 默认行为),大数据会 OOM,禁用。

3.3 count 语句的坑

PageHelper 生成 count 时会"智能优化"(去 ORDER BY、简化 SELECT),但当 SELECT 里有标量子查询、SQL 太复杂无法安全解析时,它会退化SELECT COUNT(*) FROM (原完整SQL) tmp,把子查询也带进 count 执行------导致 count 也慢。这是"SQL 直接跑快、但接口慢"的隐藏原因。解决:把大表访问移出主查询(见第六节)。


四、子查询的三种形态与性能

4.1 派生表(FROM 子查询)--- 慎用

sql 复制代码
FROM (SELECT code, MAX(time) FROM big_table GROUP BY code) t

GROUP BY/聚合的派生表会被物化(先全表算完存临时表),大表场景是性能灾难。

4.2 相关标量子查询(SELECT 子查询)--- 带值走索引

sql 复制代码
SELECT a.id,
       (SELECT MAX(b.time) FROM big_table b WHERE b.code = a.code) AS lastTime
FROM main a

带着外层 a.code 的具体值去查,能走索引。但分页 count 会连它一起执行,仍有隐患。

4.3 条件子查询(WHERE 子查询)

sql 复制代码
WHERE id IN (SELECT id FROM other WHERE ...)

五、JOIN 与行膨胀

多表 JOIN 时,若一个主记录关联到多条 子记录,结果集会出现重复行(行膨胀),导致:

  • 列表同一条数据显示多行
  • 分页 total 虚高、翻页错乱

场景:一个 A单可能有多条调拨快照、一个调拨单号可能有多条出入库单。解决办法是用子查询把"多条"收敛成"一条":

sql 复制代码
-- 取每个A单最新的一条快照(用 MAX(id) 兜底,比 MAX(create_time) 更稳,防同时间戳膨胀)
LEFT JOIN (
  SELECT t.pledge_order_id, t.transfer_order_code
  FROM snapshot t
  INNER JOIN (SELECT pledge_order_id, MAX(id) AS max_id FROM snapshot GROUP BY pledge_order_id) m
    ON m.pledge_order_id = t.pledge_order_id AND m.max_id = t.id
) ts ON ts.pledge_order_id = o.id

六、大表取"最后一次"的优化演进

主表小、要展示关联大表的"最后一次时间",且要分页。三种写法:

  1. 派生表全表聚合 (最差):对千万级大表全表 GROUP BY 物化 → 十几秒

  2. 相关标量子查询(改善):带 code 走索引,但分页 count 仍带子查询

  3. 应用层二次批量查询

    (最优):

    • 主查询和 count 完全不碰大表(只查主表)
    • 分页拿到当前页的 code 集合
    • 用一条 IN 批量查大表的 MAX(time),走索引
    • Java 层按 code 回填
java 复制代码
List<Row> list = mapper.listPager(param);              // 主查询不含大表
List<String> codes = list.stream().map(Row::getCode)
    .filter(Objects::nonNull).distinct().collect(toList());
Map<String, Date> timeMap = toMap(mapper.listLastTimeByCodes(codes)); // 一条 IN 查
list.forEach(r -> r.setLastTime(timeMap.get(r.getCode())));

核心思想:把对大表的访问次数与结果集行数解耦


七、索引相关

7.1 索引失效的常见原因

  • 对索引列用函数WHERE DATE(create_time) = '2026-09-01' 用不上 create_time 索引 → 改成范围查询

  • 左闭右开时间区间

    (这次用的):把"某月"换算成

    复制代码
    [月初, 下月初)

    ,避免对时间列套函数:

    sql 复制代码
    AND inbound_time >= '2026-09-01 00:00:00'
    AND inbound_time <  '2026-10-01 00:00:00'
  • 隐式类型转换、LIKE '%xx' 前导通配、OR 连接非索引列等

7.2 覆盖索引

(code, time) 复合索引,SELECT MAX(time) WHERE code=? 可直接从索引取值不回表,进一步加速。


八、EXPLAIN 执行计划分析

排查慢 SQL 的核心工具,重点看:

关注点
type ALL=全表扫描(差);ref/range/eq_ref=走索引(好)
key 实际用了哪个索引,NULL=没走索引
rows 预估扫描行数,越大越慢
Extra Using temporary=用临时表(派生表物化信号);Using filesort=额外排序;Using index=覆盖索引(好)
select_type DERIVED=派生表;SUBQUERY=子查询
table <derivedN>=派生表的物化结果

看到 DERIVED + big_tabletype=ALL + 大 rows + Using temporary,基本锁定派生表物化拖慢。


九、结果映射

  • resultType :查询列的别名(AS xxx)直接映射到 DTO 的同名驼峰字段(user_name AS userName)。
  • 展示串拼接放 Java 层 :像 (编码)名称 这种拼接,放 Service 层而非 SQL ------ 便于单测、格式调整,也避免 SQL 里 CONCAT/LEFT 出边界问题。SQL 只返回编码和名称原值。

十、一句话总结

MyBatis 层靠动态 SQL(<if>/<foreach>/<sql>+<include>)**灵活拼条件并复用片段保证列表与合计口径一致;用 #{} 预编译防注入;用 **PageHelper 物理分页**(注意 count 会带上 SELECT 子查询的坑)。SQL 层要警惕**派生表物化JOIN 行膨胀 两大性能陷阱,用子查询收敛(MAX id)**去重、用**应用层批量二次查询 把大表访问与结果行数解耦、用左闭右开区间 保索引不失效,最终靠 EXPLAIN 验证执行计划。

相关推荐
云贝贝贝1 小时前
MySQL 运维高频 6 坑:字符集、超时、SQL_MODE、隐式转换、Online DDL、主从延迟
运维·sql·mysql
一直走下去-明1 小时前
在 Windows 上安装 uv
windows·uv
2501_933670791 小时前
采购运营校招Excel能力清单:函数、透视表、ERP数据与SQL入门
数据库·sql·excel
Lost of 程序猿2 小时前
C# 12 / .NET 9 新特性机制深度:编译器替你写了什么,哪些糖吃下去会出事
windows·c#·.net
₍˄·͈༝·͈˄*₎◞ ̑̑码12 小时前
MyBatis操作数据库
数据库·mybatis
编程的一拳超人14 小时前
DeepSeek Harness Linux/macOS/Windows 本地下载、启动与模型配置指南
linux·windows·macos·deepseek
AIGC小尼14 小时前
Windows 本地 AI 漫剧全自动生产线部署完整教程(零基础、全指令、带源码、模型配置、排错方案)
人工智能·windows·ai漫剧
冰暮流星16 小时前
mysql之左外连接与右外连接
数据库·sql
运维全栈笔记17 小时前
Windows 安装 WSL2 并运行 Ubuntu 24.04 指南
linux·网络·windows·ubuntu