PageHelper 通过 MyBatis Interceptor 在 Executor.query() 时解析重写 SQL,主动拼接 LIMIT/TOP 实现物理分页,不依赖 JDBC 驱动;其 count 统计易因子查询、注释或动态 SQL 解析错误导致条数不准;分页上下文基于 ThreadLocal,需紧邻 DAO 调用且不可跨线程;LIMIT OFFSET 在大数据量下性能差,应优先考虑游标分页等优化方案。

PageHelper 是怎么在 SQL 执行前塞进 LIMIT 的
它靠 MyBatis 的 Interceptor 接口,在 Executor.query() 方法被调用时截住 SQL,解析并重写。不是改 Mapper XML,也不是靠数据库视图或存储过程——纯内存中对 BoundSql.sql 字符串做语法级修改。
关键点在于:它不依赖 JDBC 驱动的分页能力(比如 MySQL 的 setFetchSize()),而是主动拼 LIMIT ? , ? 或 TOP N 这类方言 SQL。
- 只拦截带
@Select、<select>标签、且未显式写LIMIT/TOP的查询 - 如果原始 SQL 已含
ORDER BY,PageHelper 会保留;若没有,它不会自动加,也不会报错 - 对嵌套子查询、UNION、WITH CTE 等复杂结构,解析可能失败——这时会跳过改写,查全量再内存分页(性能雪崩)
为什么有时候 count 查询和分页结果条数对不上
PageHelper 默认会自动生成一条 SELECT COUNT(*) FROM (...) 的统计 SQL,但它的“括号提取”逻辑很朴素:只找最外层 SELECT 开始到第一个分号(如果有)、或语句结尾为止。
常见翻车场景:
- Mapper 中写了
SELECT * FROM user WHERE id IN (SELECT user_id FROM log WHERE time > ?)—— 子查询里的SELECT会被误认为主查询,导致 count SQL 错乱 - SQL 里用了注释
-- SELECT name FROM dept,注释里的SELECT也可能被误识别 - 使用了 MyBatis 的
<bind>或<foreach>动态标签,生成的 SQL 含换行/空格不规范,影响正则匹配
解决方法不是关掉 count,而是用 PageHelper.startPage(pageNum, pageSize, false) 第三个参数设为 false,手动写 count 查询。
PageHelper.startPage() 必须紧跟在 DAO 方法调用前吗
必须。它靠 ThreadLocal 存当前分页参数:PageHelper 的静态方法只是往当前线程的 LocalPage 里塞一个 Page 对象,后续拦截器从这里取值。
一旦中间穿插了其他 MyBatis 查询(哪怕只是另一个 selectOne),或者跨线程(比如用了 CompletableFuture、线程池),这个上下文就丢了。
- 错误示范:
PageHelper.startPage(1,10); userMapper.list(); otherMapper.count();——otherMapper.count()不会分页,但更糟的是:它可能污染下一个请求的分页上下文 - 正确姿势:每个需要分页的查询前单独
startPage,不要复用;异步场景下必须手动传递Page并在子线程里PageHelper.setPage() - Spring AOP 切面里用
@Before拦 Controller 层?没用——拦截器在 DAO 层生效,切太上层等于白干
MySQL 8.0+ 和 PostgreSQL 的 LIMIT OFFSET 性能陷阱
PageHelper 生成的 LIMIT 20,10 在大数据量下会拖垮查询:MySQL 要扫描前 30 行,PostgreSQL 同样要 sort + offset 跳过。
它不解决物理分页的底层缺陷,只是帮你少写两行代码。真要优化,得换方案:
- 用游标分页(cursor-based):基于上一页最后一条记录的
id或时间戳,查WHERE id > ? ORDER BY id LIMIT 10 - 禁用
PageHelper.orderBy()动态排序——排序字段一变,索引失效,offset 成本指数上升 - 确认你的
ORDER BY字段有索引,否则LIMIT前的排序本身已成瓶颈
别指望 PageHelper 自动帮你选最优分页策略。它连是否该用 ROW_NUMBER() OVER() 都不知道——那得你自己写原生 SQL。










