PageHelper-Cursor:支持游标分页的 PageHelper 修改版,解决 MyBatis 深分页性能问题

197 阅读3分钟

PageHelper-Cursor:支持游标分页的 PageHelper 修改版,解决 MyBatis 深分页性能问题

在 MyBatis 项目中,PageHelper是常用的分页插件,它默认采用 LIMIT offset size`的传统物理分页方式。这种分页很容易在大数据量表中触发深分页性能问题:当 pageNum 越来越大,SQL 执行性能呈指数级下滑,offset 越大扫描代价越高。

举个例子:

SELECT * FROM user ORDER BY id LIMIT 1000000, 20;

这种 SQL 将会导致 MySQL 扫描百万级数据后再丢弃数据,拖垮查询性能。

那么有没有更优雅、更高性能的分页方式?

答案是:有!那就是“游标分页”(Cursor Pagination)


PageHelper-Cursor 是什么?

PageHelper-Cursor 是基于 PageHelper 6.1.1 改造的版本,增加了 游标分页能力,通过一个有序索引字段充当分页游标,绕开了 OFFSET 带来的扫描成本,从而高效支持深分页

github链接,文档齐全:Cursor-PageHelper: Mybatis通用分页插件

部署指南:BUILD_AND_DEPLOY_GUIDE.md

✅ 特性总结

特性支持
避免深分页性能问题
兼容 PageHelper 使用体验
支持 MySQL & PostgreSQL
支持字段游标分页
使用简单

🚨 注意:该版本目前为非常早期的测试版,性能和可靠性无法保证,强烈建议不要用于生产环境。


为什么游标分页能解决深分页?

游标分页的核心思想:

不再跳过前面所有数据,而是根据上一个查询返回的最后一个字段作为游标继续往后查。

例如:

-- 第一次查询 
SELECT * FROM user WHERE id > 0 ORDER BY id LIMIT 20; 
-- 下一次查询,从最后一个 id 开始继续 
SELECT * FROM user WHERE id > 1020 ORDER BY id LIMIT 20;

这种做法依赖索引查找,不会扫全表,性能几乎是常数级的 O(1)


引入 PageHelper-Cursor

如果你的项目是 Spring Boot + MyBatis,在本地部署打包后直接加入下面依赖即可:

<!--PageHelper SpringBoot 整合-->
<dependency> 
    <groupId>com.github.pagehelper</groupId> 
    <artifactId>pagehelper-spring-boot-starter</artifactId> 
    <version>${pagehelper-spring-boot-starter.version}</version>
</dependency> 

<!--PageHelper Cursor 测试版--> 
<dependency>
    <groupId>com.github.pagehelper</groupId> 
    <artifactId>pagehelper</artifactId>
    <version>6.1.1-cursor-SNAPSHOT</version> 
</dependency>`

⚠️ 环境要求:最好是MyBatis 3.1.0+


使用示例

✅ 例1:基于自增 ID 分页(最简单)

@RestController
@RequestMapping("/api/users") 
public class UserController { 
    
    @Autowired private UserMapper userMapper;
    
    @GetMapping public PageInfo<User> getUsers( 
            @RequestParam(required = false, defaultValue = "0") Long cursor) { 
            // 使用游标分页 
            PageHelper.startCursor("id", cursor, 20); 
            List<User> users = userMapper.selectAll(); 
            return new PageInfo<>(users); 
    } 
}

前端分页请求示例:

GET /api/users?cursor=0 GET /api/users?cursor=1001

✅ 例2:使用 create_time 游标分页(更通用)

@GetMapping("/listWithCursor") 
@Tag(name = "获取所有评论信息", description = "管理员分页获取当前所有评论信息列表") 
public Result<List<TopCommentVo>> queryWithCursor( 
        @RequestParam(required = false) String keyWord, 
        @RequestParam(defaultValue = "0") String lastDate, 
        @RequestParam(defaultValue = "10") int pageSize) { 
    if(lastDate.equals("0")){ 
        lastDate = null; 
    } 
    // 开启分页 
    PageHelper.startCursor("c.create_time",lastDate,pageSize,false); // 执行查询 
    List<TopCommentVo> list = commentService.query(keyWord); 
    // 获取分页信息 
    PageInfo<TopCommentVo> pageInfo = new PageInfo<>(list); 
    // 清理分页 
    PageHelper.clearPage(); 
    // 使用自定义分页返回方法
    return Result.page(list, pageInfo.getTotal()); 
}

注意事项(必看❗)

✅ 成功使用游标分页必须满足几个条件:

条件必要性
游标字段必须参与排序,且是第一个排序字段✅ 必须
游标字段必须有索引支持✅ 强烈推荐
暂不支持任意表达式分页✅ 不支持
SQL 如有 ORDER BY 必须与游标字段一致✅ 必须

与传统分页对比

对比项LIMIT 分页Cursor 分页
深分页性能❌ 极差✅ 稳定
查询效率O(N)O(1)
依赖 OFFSET✅ 是❌ 否
是否要求索引✅ 推荐✅ 强制
数据一致性好⚠️ 依赖排序字段

总结

PageHelper-Cursor为 MyBatis 提供了高性能分页的新选择,在处理海量分页时性能优势非常明显。它特别适合:

✅ 评论流、订单列表
✅ Feed 流、用户动态
✅ 日志查询、时间排序类数据

目前项目处于测试阶段,如果你对性能敏感、希望替换掉 LIMIT 深分页,相信这个项目会对你有帮助。