开发调试技巧
日志不仅是查看 SQL 的工具,更是排查问题的利器。本文介绍如何通过日志快速定位慢查询、参数错误、N+1 问题等常见开发陷阱。
定位慢查询
通过日志时间差判断
DEBUG 日志自带时间戳,通过对比 SQL 执行前后的时间差可判断查询耗时:
SQL
2026-05-20 10:30:15.120 [main] DEBUG c.e.mapper.UserMapper.selectById - ==> Preparing: SELECT ...
2026-05-20 10:30:15.122 [main] DEBUG c.e.mapper.UserMapper.selectById - ==> Parameters: 1(Long)
2026-05-20 10:30:15.830 [main] DEBUG c.e.mapper.UserMapper.selectById - <== Total: 1
从 10:30:15.122 到 10:30:15.830,耗时约 708ms,明显偏慢。
排查步骤
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 复制日志中的 SQL 到数据库客户端 | 去掉 Preparing: 前缀,替换 ? 为实际参数 |
| 2 | 使用 EXPLAIN 分析执行计划 | 检查是否走索引、是否全表扫描 |
| 3 | 检查是否缺少索引 | 对 WHERE、ORDER BY 字段加索引 |
| 4 | 检查数据量 | 表数据过大时考虑分页或归档 |
Java
-- 复制 SQL 并手动执行
EXPLAIN SELECT id, username, email FROM users WHERE id = 1;
排查参数传递错误
参数类型不匹配
日志中参数值后面会标注类型,如 (String)、(Long)、(Integer):
XML
==> Parameters: admin(String)
如果 Mapper 方法签名与 XML 中参数类型不一致,会报异常:
Java
// Mapper 接口
User selectById(@Param("id") Long id);
XML
<!-- XML 映射 -->
<select id="selectById" resultType="User">
SELECT * FROM users WHERE id = #{id}
</select>
如果传入的是字符串 "1" 而非 Long,日志显示:
YAML
==> Parameters: 1(String)
此时可能触发类型转换异常,通过日志中的类型标注快速发现。
参数为空值
参数为 null 时,日志会明确显示:
YAML
==> Parameters: null
常见于对象属性未赋值就传入查询条件,导致查询结果不符合预期。
集合参数展开
使用 <foreach> 的 IN 查询,日志中会逐个显示参数:
XML
List<User> selectByIds(@Param("ids") List<Long> ids);
text
<select id="selectByIds" resultType="User">
SELECT * FROM users WHERE id IN
<foreach item="id" collection="ids" open="(" separator="," close=")">
#{id}
</foreach>
</select>
日志输出:
text
==> Preparing: SELECT * FROM users WHERE id IN (?,?,?)
==> Parameters: 1(Long), 2(Long), 3(Long)
如果集合为空,日志显示:
text
==> Preparing: SELECT * FROM users WHERE id IN ()
此时 SQL 语法报错,可在日志中快速发现空集合问题。
定位 N+1 查询问题
使用 <collection> 或 <association> 延迟加载时,日志中会出现大量重复 SQL:
text
10:30:15 DEBUG c.e.mapper.UserMapper.selectAll - ==> Preparing: SELECT * FROM users
10:30:15 DEBUG c.e.mapper.UserMapper.selectAll - ==> Parameters:
10:30:15 DEBUG c.e.mapper.OrderMapper.findByUserId - ==> Preparing: SELECT * FROM orders WHERE user_id = ?
10:30:15 DEBUG c.e.mapper.OrderMapper.findByUserId - ==> Parameters: 1(Long)
10:30:15 DEBUG c.e.mapper.OrderMapper.findByUserId - ==> Preparing: SELECT * FROM orders WHERE user_id = ?
10:30:15 DEBUG c.e.mapper.OrderMapper.findByUserId - ==> Parameters: 2(Long)
10:30:15 DEBUG c.e.mapper.OrderMapper.findByUserId - ==> Preparing: SELECT * FROM orders WHERE user_id = ?
10:30:15 DEBUG c.e.mapper.OrderMapper.findByUserId - ==> Parameters: 3(Long)
1 次主查询 + N 次关联查询 = N+1 问题。
解决方式
| 方案 | 说明 |
|---|---|
| 使用 JOIN 查询 | 在 XML 中编写 JOIN SQL,一次性查出关联数据 |
| 开启嵌套查询的懒加载 | 设置 lazyLoadingEnabled=true,按需加载 |
使用 <collection> 的 fetchType="eager" | 明确指定 eager/lazy 策略 |
事务回滚排查
开启事务日志可看到提交和回滚过程:
text
logging:
level:
org.apache.ibatis.transaction: DEBUG
日志输出:
text
10:40:20 DEBUG o.a.i.t.jdbc.JdbcTransaction - Opening JDBC Connection
10:40:20 DEBUG o.a.i.t.jdbc.JdbcTransaction - Setting autocommit to false
10:40:21 DEBUG o.a.i.t.jdbc.JdbcTransaction - Committing JDBC Connection
如果发生回滚,日志中会显示:
text
10:40:21 DEBUG o.a.i.t.jdbc.JdbcTransaction - Rolling back JDBC Connection
结合异常堆栈,可快速定位是哪一步操作触发了回滚。
调试实战技巧
技巧一:临时开启 SQL 日志
text
# 开发环境临时开启全部 MyBatis 日志
logging:
level:
org.apache.ibatis: DEBUG
注意:仅用于开发调试,生产环境务必关闭,否则日志量巨大。
技巧二:SQL 格式化输出
MyBatis 内置的 STDOUT_LOGGING 可直接在控制台输出格式化 SQL:
text
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
输出效果:
text
==> Preparing: SELECT id, username, email FROM users WHERE id = ?
==> Parameters: 1(Long)
<== Columns: id, username, email
<== Row: 1, admin, admin@example.com
<== Total: 1
无需引入额外依赖,适合快速调试。
技巧三:对比预期与实际 SQL
开发时先写好预期 SQL,再通过日志对比实际执行的 SQL 是否一致:
| 对比项 | 说明 |
|---|---|
| SQL 结构 | WHERE 条件、JOIN 是否正确 |
| 参数顺序 | #{} 占位符顺序是否对应 |
| 动态 SQL | <if> 条件是否按预期拼接 |
要点总结
- 通过日志时间戳差值判断 SQL 执行耗时,结合 EXPLAIN 分析慢查询
- 日志中参数类型标注帮助发现类型不匹配问题,null 值一目了然
- N+1 问题表现为大量重复关联查询,可通过 JOIN 或懒加载解决
- 事务日志显示回滚时,结合异常堆栈定位触发点
STDOUT_LOGGING无需额外依赖,适合快速调试场景
文章存放路径:articles/MYBATIS/入门/日志配置与调试/开发调试技巧.md
📝 发现内容有误?点击此处直接编辑