引言
在Java开发的江湖里,MyBatis Plus(MP)早已是“效率利器”——它用极简的API封装了CRUD操作,让开发者从重复的SQL编写中解放出来。但随着项目数据量从“万级”跃升至“十万级”“百万级”,一个尴尬的现实逐渐浮现:曾经跑得飞快的MP应用,开始频繁出现接口超时、数据库压力骤增的情况。问题根源往往不在MP本身,而在那些“隐形的SQL”——它们可能因索引缺失、查询冗余或分页逻辑欠妥,在数据洪流中沦为性能瓶颈。今天,我们就来聊聊如何用MP的特性,精准定位并解决这些SQL性能问题。
一、定位性能问题:先学会“看日志”和“读执行计划”
很多同学遇到慢查询第一反应是“是不是MP的问题?”——其实MP本身只是ORM框架,SQL性能的核心还是在数据库本身的执行效率。所以第一步,得先搞清楚“MP到底生成了什么SQL?执行了多久?”
1. 开启MP的SQL日志:让“隐形SQL”现形
MP的日志配置非常简单,但90%的新手可能没配置对。在application.yml
里加上这几行,开发环境直接能看到完整的SQL执行过程:
mybatis-plus:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 控制台打印SQL(开发环境必备)global-config:db-config:logic-delete-field: logic_delete_field # 逻辑删除字段(自动过滤已删数据,避免全表扫描)
输出示例:
控制台会打印类似这样的信息:
==> Preparing: SELECT id,name,age FROM user WHERE age > ? AND logic_delete_field = 0
==> Parameters: 18(Integer)
<== Total: 10
重点关注Preparing
里的SQL语句和Parameters
里的参数,这是后续分析的基础。
2. 数据库层面:用EXPLAIN“解剖”SQL
光看MP日志还不够,必须结合数据库的执行计划。以MySQL为例,拿到MP生成的SQL后,在Navicat或命令行执行EXPLAIN + SQL语句
,重点关注4个指标:
指标 | 含义 | 优化目标 |
---|---|---|
type | 访问类型(性能从好到差:system > const > eq_ref > ref > range > index > ALL) | 至少达到range ,避免ALL (全表扫描) |
key | 实际使用的索引 | 不为NULL (未命中索引) |
rows | MySQL估计要扫描的行数 | 数值越小越好 |
Extra | 额外信息(如Using filesort 文件排序、Using temporary 临时表) | 避免这两种情况 |
举个栗子:
假设MP生成了SELECT * FROM user WHERE username LIKE '%张三%'
,执行EXPLAIN
后发现type=ALL
且key=NULL
,这说明:
- 没有给
username
字段加索引(左模糊LIKE '%xxx'
无法用普通索引); - 全表扫描导致性能极差(数据量10万时,扫描时间可能飙升到秒级)。
3. 慢查询日志:抓出“隐形杀手”
除了实时日志,一定要开启数据库的慢查询日志,记录执行时间超过阈值的SQL。以MySQL为例,在my.cnf
里配置:
slow_query_log = 1 # 开启慢查询
slow_query_log_file = /var/log/mysql/slow.log # 日志路径
long_query_time = 1 # 超过1秒的SQL记录
log_queries_not_using_indexes = 1 # 记录未使用索引的SQL(关键!)
重启MySQL后,所有“慢SQL”都会被记录下来,结合MP日志的时间戳,就能精准定位到具体是哪个业务接口触发了问题SQL。
二、MP常见的5大SQL性能“坑”
通过日志和执行计划分析后,你会发现MP的性能问题大多源于使用不当,而不是框架本身的bug。以下是最常见的5类问题,看看你中过几个?
1. 全表扫描:索引白加了?
典型场景:
- 对高频查询字段(如
age
、create_time
)未加索引; - 用
LIKE '%关键词%'
做模糊查询(左模糊无法用普通索引); - 逻辑删除未生效(未配置
@TableLogic
,导致查询时仍然扫描已删除数据)。
优化建议:
- 对高频查询、排序、分组的字段(如
create_time
、status
)添加索引; - 左模糊查询(
LIKE '%xxx'
)尽量改用全文索引(MySQL 5.6+支持FULLTEXT
)或搜索引擎(如Elasticsearch); - 配置
@TableLogic
标记逻辑删除字段,MP会自动过滤已删数据,避免全表扫描。
2. N+1查询:循环调用数据库,能不慢吗?
典型场景:
查询主表(如用户表)后,循环调用接口查询关联表(如订单表):
// 错误示范:1次查用户,N次查订单(总查询次数=1+N)
List<User> users = userMapper.selectList(wrapper);
users.forEach(user -> {List<Order> orders = orderMapper.selectByUserId(user.getId()); // 循环调用
});
问题:
数据量1000时,总查询次数是1001次!数据库连接池被占满,响应时间飙升。
优化方案:
- 批量查询:先查所有用户ID,再用
selectBatchIds
一次性查订单:List<Long> userIds = users.stream().map(User::getId).collect(Collectors.toList()); List<Order> orders = orderMapper.selectBatchIds(userIds); // 1次查询搞定
- 懒加载:用
@TableField(select = false)
标记不需要立即查询的关联字段,需要时手动调用; - MyBatis二级缓存:对高频读、低频写的数据(如字典表)启用缓存(需注意缓存一致性)。
3. 批量操作:旧版本MP的“隐形杀手”
典型场景:
用saveBatch
插入1万条数据,发现耗时5秒+。查日志发现MP生成了1万条INSERT
语句!
原因:
MP 3.5.0之前,默认的批量插入实现是通过foreach
拼接多条INSERT
语句(非数据库原生的批量插入),网络IO和事务提交次数暴增。
优化方案:
- 升级MP到3.5.0+:默认支持
INSERT INTO user (name,age) VALUES (a),(b),(c)
的原生批量插入; - 手动分批处理:数据量极大时(如10万条),按每1000条分批插入:
List<List<User>> batches = ListUtils.partition(userList, 1000); // 每批1000条 batches.forEach(batch -> userMapper.saveBatch(batch));
4. 分页查询:LIMIT 100000,20 等于“自杀”
典型场景:
做后台管理系统时,用户翻到第100页,SQL是SELECT * FROM user LIMIT 100000,20
。数据库需要扫描前100020行,耗时随数据量线性增长。
问题:
LIMIT offset, size
的本质是“先扫描offset+size行,再丢弃前offset行”,数据量大时效率极低。
优化方案:
- 小数据量分页:直接用MP的
Page
对象,底层会自动处理; - 大数据量分页:改用
WHERE id > lastId LIMIT size
(需主键有序):// 假设上一页最后一条记录的ID是lastId QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.gt("id", lastId).last("LIMIT 20"); // 直接拼接LIMIT语句 IPage<User> page = userMapper.selectPage(new Page<>(current, size), wrapper);
5. 冗余查询:SELECT * 害人不浅
典型场景:
用userMapper.selectById(1L)
查询用户,但表里有avatar
(大图片)、remark
(长文本)等字段,结果返回了几十KB的数据,内存和网络都被占满。
问题:
SELECT *
会查询所有字段,包括大字段和不必要的字段,增加网络传输和内存消耗。
优化方案:
- 指定查询字段:用
QueryWrapper.select()
明确需要的字段:QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.select("id", "name", "age"); // 只查这三个字段 User user = userMapper.selectOne(wrapper);
- DTO投影:定义只包含需要字段的DTO类,避免实体类膨胀:
@Data public class UserDTO {private Long id;private String name;private Integer age; } // 查询结果直接映射到DTO List<UserDTO> dtos = userMapper.selectList(wrapper);
三、总结:MP性能优化的“三板斧”
通过上面的案例,我们可以总结出MP SQL性能优化的核心思路:
1. 日志是“眼睛”,EXPLAIN是“放大镜”
开启MP日志,结合EXPLAIN
分析执行计划,90%的性能问题都能定位到索引或SQL写法上。
2. 索引不是万能的,但不加索引是万万不能
高频查询字段一定要加索引,左模糊、全表扫描这类“坑”能避则避。
3. 批量和分页要“讲究”
批量操作用3.5.0+的新特性,分页大数据量时用主键范围查询,避免LIMIT offset, size
。
最后想和大家说:MP是工具,SQL性能的核心还是在开发者对业务的理解和数据库的掌握。多动手分析日志、执行计划,多测试不同场景下的性能表现,才能让项目“快人一步”!
你在开发中遇到过哪些MP的SQL性能问题?欢迎在评论区分享你的踩坑经历和解决方案~