mybatis高效分页需避开count(*)统计和offset深度扫描两大陷阱:禁用count、改用游标分页(基于主键或时间字段)、精简sql与关联查询、建立覆盖索引,并配合数据库层优化。

MyBatis 本身不提供开箱即用的高性能分页能力,面对百万级以上数据,直接用 LIMIT offset, size 会严重拖慢查询——因为数据库必须扫描并跳过前 offset 行。要真正高效翻页,关键不是“怎么配 PageHelper”,而是“怎么避开传统分页的两大陷阱”:COUNT(*) 统计耗时 + OFFSET 深度扫描。
禁用 COUNT 或改用异步/缓存策略
多数列表页(如信息流、“加载更多”)根本不需要精确总页数。强行查总数,等于每次请求都多执行一次全表或索引扫描。
- PageHelper:调用
PageHelper.startPage(pageNum, pageSize, false),第三个参数false明确跳过 count 查询 - MyBatis-Plus:设置
page.setSearchCount(false),再传入selectPage - 若必须显示总页数,可用定时任务统计表行数快照(如每5分钟更新一次
SELECT COUNT(*) FROM table结果到 Redis),前端展示近似值即可
放弃页码,改用游标分页(推荐深分页场景)
游标分页不依赖页码,而是以“上一页最后一条记录的主键值”为锚点,彻底绕开 OFFSET。它性能稳定、可水平扩展,适合高并发和大数据量。
Java项目代码review工具。分析Git变更+完整调用链路上下文,推断业务需求,进行多维度评分和分类汇总,生成完整PRD文档。包含细粒度Java代码审查清单(Null安全、异常处理、Streams、并发、equals/hashCode、资源管理、API设计、性能、MyBatis/ORM、事务边界、SQL/DD...
- 要求:主键(如
id)或时间字段(如create_time)有唯一性且已建索引 - SQL 示例:
SELECT * FROM user WHERE id > #{lastId} ORDER BY id LIMIT 20 - MyBatis-Plus 写法:
lambdaQuery().gt(User::getId, lastId).orderByAsc(User::getId).last("LIMIT " + size).list() - 前端需保存并传递
lastId,不支持随机跳页,但换页响应极快
精简查询本身:避免关联爆炸与字段冗余
分页慢常因 SQL 写法不当放大性能问题,而非分页插件本身。
- 禁用
<collection></collection>做一对多嵌套分页——易触发 N+1 或笛卡尔积,应拆成两阶段:先查主表 ID 列表,再用IN批量查关联数据(注意控制 IN 数量,建议 ≤ 1000) - 只查必要字段,不用
SELECT *;XML 中用<resultmap></resultmap>精确映射,减少对象封装和网络传输 - 大文本、JSON、BLOB 字段延迟加载,或单独接口获取
配套数据库层优化不能少
再好的分页逻辑,没有底层支撑也白搭。
- 覆盖索引:确保
WHERE条件字段、排序字段、游标字段(如id)落在同一联合索引中,避免回表 - 流式导出大数据:用
ResultHandler或 MyBatis-Plus 的selectObjs配合fetchSize = Integer.MIN_VALUE,逐批拉取防 OOM - 防恶意请求:PageHelper 开启
reasonable=true自动修正越界页码;MyBatis-Plus 设置最大页码限制,拦截pageNum=999999类攻击
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










