MyBatis-Plus的分页查询原理
MyBatis-Plus分页查询底层原理
一、分页查询的核心痛点与 MP 的解决方案
在传统 MyBatis 开发中,分页实现往往需要手动拼接LIMIT语句(MySQL)或ROWNUM(Oracle),不仅繁琐且易出错,还存在两个核心问题:
SQL 侵入性强:业务 SQL 与分页语法耦合,切换数据库时需批量修改;
总条数统计冗余:需手动编写 count 查询,且需处理复杂查询(如多表关联、分组统计)的 count 适配。
MyBatis-Plus(以下简称 MP)的分页插件通过拦截器机制+数据库方言适配,实现了 “无侵入式分页”,其核心设计思路是:在 SQL 执行前动态改写 SQL,自动添加分页语法和 count 查询,同时封装分页结果。
二、MP 分页的核心组件与依赖关系
MP 分页功能的实现依赖 3 个核心组件,它们的协作流程决定了分页的底层逻辑:
| 组件名称 | 作用说明 |
|---|---|
PaginationInnerInterceptor | 核心拦截器,负责拦截 SQL 执行、改写 SQL、统计总条数(MP 3.4.0 + 推荐使用) |
Page | 分页参数载体,封装页码、每页条数、总条数、分页结果集等信息 |
Dialect | 数据库方言接口,适配不同数据库的分页语法(如 MySQL 的 LIMIT、PostgreSQL 的 OFFSET) |
依赖关系:Page传递分页参数 → PaginationInnerInterceptor拦截 SQL → 调用Dialect生成适配 SQL → 执行查询并封装结果。
三、底层核心流程:从 SQL 拦截到结果返回
1. 拦截器触发条件
MP 的分页插件本质是 MyBatis 的Interceptor接口实现类,通过@Intercepts注解指定拦截StatementHandler的prepare方法(SQL 预处理阶段)和query方法(结果查询阶段):
1 |
|
当执行mapper.selectPage(page, queryWrapper)时,MyBatis 的插件链会触发该拦截器。
2. SQL 动态改写原理
这是分页功能的核心步骤,MP 会根据数据库方言,将原始查询 SQL 改写成 “分页查询 SQL” 和 “总条数查询 SQL”:
(1)原始 SQL 示例(MySQL)
1 | SELECT id, name, age FROM user WHERE age > 18 |
(2)分页 SQL 改写(添加 LIMIT)
MP 通过BoundSql解析原始 SQL,拼接分页语法:
1 | SELECT id, name, age FROM user WHERE age > 18 LIMIT 0, 10 -- 第1页,每页10条 |
关键逻辑:在prepare方法中,通过Dialect.buildPaginationSql生成适配 SQL,不同数据库的方言实现不同:
MySQL:
LIMIT offset, sizeOracle:
SELECT * FROM (SELECT t.*, ROWNUM rn FROM (...) t WHERE ROWNUM ?) WHERE rn > ?PostgreSQL:
OFFSET ? LIMIT ?
(3)总条数 SQL 自动生成
MP 会自动剥离原始 SQL 的SELECT字段和ORDER BY(避免 count 统计错误),生成 count 查询:
1 | SELECT COUNT(1) FROM user WHERE age > 18 |
优化点:
若原始 SQL 含
GROUP BY,count 会改为COUNT(DISTINCT 分组字段);可通过
page.setSearchCount(false)关闭总条数统计(适用于无需总页数的场景)。
3. 分页参数传递与结果封装
参数传递:
Page对象中的current(当前页)和size(每页条数)会被转换为offset = (current - 1) * size,作为分页参数注入改写后的 SQL;结果封装:执行分页 SQL 后,查询结果会被设置到
page.getRecords();执行 countSQL 后,总条数会设置到page.setTotal(total),并自动计算pages = (total + size - 1) / size(总页数)。
四、物理分页 vs 逻辑分页:MP 为何选择物理分页?
很多人会疑惑:MP 为何采用 “物理分页”(依赖数据库 SQL 语法)而非 “逻辑分页”(查询全量数据后内存截取)?
| 对比维度 | 物理分页(MP 实现) | 逻辑分页(内存截取) |
|---|---|---|
| 性能 | 只查询当前页数据,数据库压力小 | 查询全量数据,大数据量下内存溢出 |
| 适用场景 | 大数据量、跨库兼容 | 小数据量、本地测试 |
| 依赖 | 数据库方言支持 | 无依赖,纯 Java 逻辑 |
MP 选择物理分页的核心原因是性能优化—— 对于百万级数据,逻辑分页会导致全表扫描,而物理分页通过数据库的索引优化,仅返回当前页数据,大幅降低 IO 开销。
五、源码关键片段解析(MP 3.5.3 版本)
1. 分页 SQL 改写核心代码
1 | // PaginationInnerInterceptor.java |
2. 总条数统计核心代码
1 | // PaginationInnerInterceptor.java |
六、常见问题与底层原理关联分析
1. 分页失效?—— 拦截器未注册
若分页 SQL 未添加 LIMIT,大概率是分页插件未注入 Spring 容器:
1 | // 正确配置(Spring Boot) |
原理:未注册拦截器时,SQL 不会被改写,自然无法实现分页。
2. 总条数统计错误?—— SQL 解析异常
若 count 结果与实际不符,可能是原始 SQL 含复杂语法(如UNION、子查询),导致 MP 的 SQL 解析失败。此时可手动指定 countSQL:
1 | page.setCountSql("SELECT COUNT(1) FROM (原始复杂SQL) t"); |
原理:MP 的自动 countSQL 生成依赖 SQL 解析器,复杂语法可能导致解析偏差,手动指定可绕过解析逻辑。
3. 跨库分页兼容问题?—— 方言未适配
切换数据库后分页失效,需确保DbType与实际数据库一致:
1 | // 适配PostgreSQL |
原理:不同数据库的分页语法差异,方言是连接 SQL 改写与数据库的桥梁。
七、总结与思考
MyBatis-Plus 的分页功能之所以强大,核心在于 **“无侵入式设计” 和 “数据库方言适配”**:
基于 MyBatis 拦截器机制,无需修改业务 SQL,实现分页逻辑与业务逻辑解耦;
通过方言接口封装不同数据库的分页语法,解决跨库兼容问题;
自动生成 countSQL,减少重复编码,同时支持灵活配置(关闭统计、手动指定 countSQL)。
从底层原理来看,MP 的分页插件本质是 “SQL 解析与改写的自动化工具”,其设计思路值得借鉴:将通用逻辑(分页)抽离为插件,通过拦截器嵌入框架流程,既不破坏原有架构,又能提升开发效率。
在实际开发中,需注意:
复杂 SQL 场景下,建议手动验证分页 SQL 和 countSQL 的正确性;
大数据量分页(如千万级数据),可结合
PageHelper的 “合理化分页” 或 MP 的page.setReasonable(true)避免页码越界;无需总条数时,关闭
searchCount可提升查询性能。

