mybatis分页的几种方式

时间:2026-10-17 05:14:19来源:力不从心网 作者:探索

MyBatis 本身不提供分页实现,种方它只负责把 SQL 执行结果映射成对象。种方分页这件事,种方得你自己在 SQL 层面或者插件层面解决。种方我见过太多项目在分页上踩坑,种方尤其是种方数据量上了百万级之后,内存分页直接把服务拖垮。种方

最原始的种方方式是内存分页,也就是种方把全量数据查出来,再用 List.subList() 截取。种方写法简单:

List<User> all = userMapper.selectAll(); List<User> page = all.subList((pageNum - 1) * pageSize,种方 Math.min(pageNum * pageSize, all.size())); 

这种代码在测试环境跑得挺欢,生产环境数据一多就炸。种方假设一张表 50 万行,种方每页 20 条,种方用户翻到第 10 页,种方你查了 50 万条数据进 JVM,再丢掉 49.98 万条。GC 压力、网络传输、数据库 IO 全是浪费。除非你确定这张表永远不超过几千行,否则别用。

SQL 层面分页:LIMIT 和 OFFSET

MySQL 里最常见的写法是 LIMIT offset, size。MyBatis 的 XML 里直接拼:

<select id="selectByPage" resultType="User"> SELECT * FROM user ORDER BY id LIMIT #{ offset}, #{ pageSize} </select> 

Java 侧传参时算好 offset。这种方式简单直接,适合中小数据量。但 OFFSET 有个硬伤:翻到后面页码时,数据库要先扫描并丢弃前面所有行。比如 LIMIT 1000000, 20,MySQL 会先读 100 万零 20 行,再扔掉前 100 万行。翻到深页,查询时间线性增长。

我实测过一张 300 万行的订单表,LIMIT 2000000, 20 耗时 1.8 秒,而 LIMIT 0, 20 只要 3 毫秒。差距就是这么夸张。

优化思路是用游标分页,也就是基于上一页最后一条记录的 ID 来查下一页:

SELECT * FROM user WHERE id > #{ lastId} ORDER BY id LIMIT #{ pageSize} 

这种方式没有 OFFSET 的扫描开销,每次查询都走索引。缺点是不能随机跳页,只能上一页下一页。适合信息流、滚动加载场景。后台管理系统那种要跳转到第 50 页的需求,游标分页就无能为力了。

PageHelper 插件:省事但要知道它干了什么

PageHelper 是国内用得最多的 MyBatis 分页插件。代码写起来很舒服:

PageHelper.startPage(pageNum, pageSize); List<User> list = userMapper.selectAll(); PageInfo<User> info = new PageInfo<>(list); 

它通过 MyBatis 拦截器在 SQL 执行前改写语句,自动加上 LIMIT,同时执行一条 COUNT 查询。你不需要手动写分页 SQL,但代价是 COUNT 查询可能很慢。PageHelper 默认会生成 SELECT COUNT(0) FROM (原SQL),如果原 SQL 带 JOIN 和子查询,COUNT 可能比主查询还耗时。

我遇到过一种情况:原查询带 GROUP BY,PageHelper 的 COUNT 结果和实际总行数对不上。后来查文档才发现,PageHelper.startPage() 有个 count 参数可以关掉自动 COUNT,自己写 count 语句。这个细节很多人不知道,出了问题才去翻源码。

另外 PageHelper 对多数据源的支持需要额外配置,helperDialect 设成 auto 时会根据 JDBC URL 自动识别数据库类型,但某些云数据库的 URL 格式不标准,会导致方言识别失败,分页 SQL 拼错。

手写 COUNT 和分页 SQL:可控性最强

对性能敏感的项目,我倾向于手写两条 SQL。一条查总数,一条查当前页数据。COUNT 语句可以针对业务做优化,比如去掉不必要的 JOIN,或者用近似值代替精确值。

<select id="countByCondition" resultType="long"> SELECT COUNT(*) FROM user WHERE status = #{ status} </select> <select id="selectByPage" resultType="User"> SELECT * FROM user WHERE status = #{ status} ORDER BY id LIMIT #{ offset}, #{ pageSize} </select> 

Service 层组装分页对象,返回给前端。这种方式代码量多一些,但每一行 SQL 都在你掌控之中。出了慢查询,直接看 SQL 就能定位问题,不用去猜插件生成了什么。

还有个细节:ORDER BY 的字段一定要有索引,否则 LIMIT 也救不了你。我见过 ORDER BY create_time 没索引的表,分页查询直接全表排序,加了索引之后从 2 秒降到 10 毫秒。

分页方案没有银弹。数据量小就内存分页图省事,中等数据量用 LIMIT 加索引,大数据量考虑游标分页或者把 COUNT 缓存起来。PageHelper 适合快速开发,但别把它当成黑盒,至少要知道它替你执行了哪几条 SQL。

相关内容
推荐内容