引言
MP分页插件极大的提升了开发效率,但随着数据量从几万涨到百万、千万级,点“下一页”等半天、页面卡成PPT的情况,相信很多人遇见过。问题根源往往出在MP默认的分页逻辑——基于OFFSET+LIMIT的物理分页,在OFFSET过大时,数据库需要扫描前N条数据再丢弃,效率直线下降。今天就结合实战经验,聊聊如何用7招把分页性能从“龟速”提到“火箭”,让用户再也不用对着加载中的转圈干瞪眼!
一、先搞懂:MyBatis Plus分页为啥慢?
MP的分页原理其实很简单:通过MybatisPlusInterceptor
拦截SQL,动态拼接LIMIT offset, size
。但问题就出在这OFFSET
上——
比如你要查第10000页(每页10条),SQL会是SELECT * FROM user LIMIT 100000, 10
。这时候数据库得先把前10万条数据全扫一遍,再扔掉,最后取10条。这就像你要找书架第100层的书,得先把前99层的书全搬下来,效率能高吗?
结论:OFFSET
越大,性能越崩!这就是深分页问题。
二、7招优化,让分页快到飞起
第1招:深分页?改用游标分页(Cursor Pagination)
核心思路:不用OFFSET
,改用上一页的最后一条数据的唯一ID作为查询条件,直接定位下一页数据。
比如用户表用自增主键id
排序,第一页查完,记录最后一条的id=100
,下一页直接查id > 100 LIMIT 10
,数据库不用扫前面的数据!
实战代码(以MySQL自增主键为例):
// 第一页
Page<User> page1 = new Page<>(1, 10);
QueryWrapper<User> wrapper1 = new QueryWrapper<>();
wrapper1.orderByAsc("id"); // 必须按唯一递增字段排序!
mpMapper.selectPage(page1, wrapper1);
Long lastId = page1.getRecords().get(page1.getRecords().size()-1).getId(); // 记录最后一条id
// 第二页及以后
Page<User> pageN = new Page<>(n, 10);
QueryWrapper<User> wrapperN = new QueryWrapper<>();
wrapperN.gt("id", lastId).orderByAsc("id"); // 关键:用id>lastId过滤
mpMapper.selectPage(pageN, wrapperN);
注意:排序字段必须是唯一且递增的(自增主键、雪花ID都行),否则可能漏数据或重复!
第2招:少查点字段!别总用SELECT *
问题:很多人写select *
,把身份证号、备注这些大字段全捞上来,网络传输、内存占用、反序列化全得跟着遭殃。
优化:用select()
明确指定需要的字段,减少无效数据。
实战代码:
// 只查id、name、email,其他字段不要!
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.select("id", "name", "email");
mpMapper.selectPage(page, wrapper);
// Lambda写法更安全(防字段名写错)
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.select(User::getId, User::getName, User::getEmail);
第3招:给排序和过滤字段加索引!别让数据库全表扫描
原理:如果WHERE
条件或ORDER BY
字段没索引,数据库得全表扫描+临时排序(filesort
),慢得离谱!
优化:给常用查询字段加索引,最好建联合索引!
举个栗子:
业务需求:查年龄>18岁的用户,按注册时间倒序排。
正确索引:(age, create_time)
(先过滤age,再按create_time排序,一步到位)。
验证索引是否生效:
用EXPLAIN
看SQL执行计划,重点看type
(最好是ref
或range
)和Extra
(别出现Using filesort
):
EXPLAIN SELECT * FROM user WHERE age > 18 ORDER BY create_time DESC LIMIT 10;
第4招:关闭总数统计!总页数真的那么重要?
痛点:MP默认会执行SELECT COUNT(*)
算总记录数,100万条数据的表,这SQL可能要执行几百毫秒!
优化:如果业务不需要显示"共1000页",直接关掉总数统计!
实战代码:
Page<User> page = new Page<>(1, 10);
page.setSearchCount(false); // 关键!关闭总数统计
mpMapper.selectPage(page, wrapper);
// 此时page.getTotal()是0,总页数自己算(比如当前页+是否还有下一页)
第5招:数据库特性利用:不同DB有不同优化招
不同数据库的分页语法和优化策略不一样,咱得因地制宜:
数据库 | 传统分页SQL | 优化方案 |
---|---|---|
MySQL | LIMIT offset, size | 深分页改用WHERE id > lastId LIMIT size (需有序主键) |
PostgreSQL | LIMIT size OFFSET offset | 大数据量时用FETCH FIRST size ROWS ONLY (无OFFSET) |
Oracle | 子查询+ROWNUM | 改用ROW_NUMBER() 窗口函数,或游标分页 |
第6招:少JOIN!大表关联能拆就拆
问题:分页时如果JOIN
了大表(比如用户表JOIN部门表),数据库得先把两个表的数据全关联一遍,再排序分页,性能直接崩!
优化:
- 小表关联:如果从表数据量小(比如部门表只有几十行),直接JOIN没问题。
- 大表拆分:先分页主表,再用主表ID去查从表(适合从表数据按主表ID索引的情况)。
- 冗余字段:把常用的从表字段(如部门名称)直接存到主表,避免JOIN(空间换时间)。
第7招:监控+验证:别优化完不知道有没有用!
优化后一定要验证效果,不然白忙活!推荐俩方法:
方法1:看执行计划
用EXPLAIN
分析SQL,确认是否用了索引,有没有filesort
(全表排序)或ALL
(全表扫描)。
方法2:测耗时
用数据库慢查询日志(MySQL的slow_query_log
),设置阈值(比如超过1秒),监控优化前后的耗时变化。
总结:分页优化的核心就3点
- 减少无效扫描:深分页用游标分页,别让数据库扫前N条数据。
- 利用索引加速:排序、过滤字段必须有索引,最好联合索引。
- 降低计算量:少查字段、关总数统计、少JOIN大表。
亿级数据量别硬刚分页,要么用ES做搜索分页,要么分库分表,这才是终极解法~ 兄弟们有其他问题,评论区见!