mybatis分页深入了解

我们在需要分页查询的时候,往往会使用mp或者mybatis的pageHepler来做分页查询

但是其底层还是几个关键字:offset和pagesize

sql 复制代码
select * from user limit #{offset}, #{pageSize}

其中mp和mybatis为我们做了层封装,我们只需要输入页数,每页查询数和总数据量即可

其中分页底层分为几种模式:

底层逻辑

1、逻辑分页------RowBounds(了解即可)

MyBatis 内置了RowBounds支持逻辑分页:

java 复制代码
// offset:起始位置(从 0 开始),limit:每页条数
// 查第 3 页,每页 10 条 → offset = 20, limit = 10
List<User> users = sqlSession.selectList(
 "com.example.mapper.UserMapper.selectAll",
 null,
 newRowBounds(20,10) // 跳过前 20 条,取 10 条
);

底层原理 :MyBatis 执行完整的 SQL 查询,将所有结果加载到内存后,通过DefaultResultHandler跳过offset条记录,只取limit条。

致命缺陷 :100 万条数据只看 10 条,却要把 100 万条全加载到内存。生产环境绝对不能用

2、物理分页------手写 SQL

最原始的方式,直接在 SQL 中手写LIMIT

XML 复制代码
<selectid="selectByPage"resultType="User">
  SELECT * FROM t_user
  ORDER BY id DESC
  LIMIT #{offset}, #{pageSize}
</select>
java 复制代码
// 调用
intpageNum =3;  // 第 3 页
intpageSize =10; // 每页 10 条
intoffset = (pageNum -1) * pageSize; // 偏移量 = 20

List<User> users = userMapper.selectByPage(offset, pageSize);

缺点 :每次分页都要手算offset,还要单独写COUNT(*)查询获取总数,代码重复且容易出错。

3、物理分页------PageHelper 插件(重点)

PageHelper 是国内最流行的 MyBatis 分页插件,使用非常简单:

java 复制代码
// 使用 PageHelper 分页
PageHelper.startPage(3,10); // 第 3 页,每页 10 条
List<User> users = userMapper.selectAll(); // 正常查询,自动分页

PageInfo<User> pageInfo =newPageInfo<>(users);
System.out.println("总记录数:"+ pageInfo.getTotal());  // 100
System.out.println("总页数:"+ pageInfo.getPages());   // 10
System.out.println("当前页数据:"+ pageInfo.getList().size());// 10
System.out.println("是否有下一页:"+ pageInfo.isHasNextPage());// true

核心 API

注意PageHelper.startPage()只对紧跟其后的第一条 查询生效。这是通过ThreadLocal实现的------调用startPage()后将分页参数存入ThreadLocal,下一次查询消费后自动清除。

4、PageHelper 的底层原理

PageHelper 的核心原理是MyBatis 插件(Interceptor)机制 +SQL 改写

PageHelper 的完整执行流程可以拆解为以下关键步骤:

  • 步骤一(设置分页参数) :调用PageHelper.startPage(3, 10),将分页参数封装为Page对象,存入当前线程的ThreadLocal。这一步和查询方法是分开调用的,通过ThreadLocal在两者之间传递分页信息。

  • 步骤二(拦截查询)PageInterceptor实现了 MyBatis 的Interceptor接口,通过@Signature注解拦截了Executorquery方法。当查询执行时,插件介入。

  • 步骤三(SQL 改写) :这是核心。插件从ThreadLocal取出分页参数,使用 SQL 解析器(JSqlParser)解析原始 SQL 的 AST(抽象语法树),根据数据库方言自动拼接LIMIT(MySQL)、ROWNUM(Oracle)、TOP(SQL Server)等分页子句。

  • 步骤四(COUNT 查询) :如果需要总数(默认会查),插件会将原始 SQL 改写为SELECT COUNT(*) FROM ...的形式,去掉ORDER BYLIMITLEFT JOIN等不影响总数的部分,单独执行一次获取总记录数。

  • 步骤五(清理 ThreadLocal) :查询完成后,自动清除ThreadLocal中的分页参数,确保不影响后续查询。

问题优化

使用分页查询时,特别时数据量比较大的时候,会遇到深分页问题

limit 100000,10 这种深分页 ,数据库会先扫描 100010 条数据,丢掉前 10 万条,只返回 10 条,越往后翻页越慢,甚至卡死。


1、深分页为什么慢?

sql 复制代码
select * from user limit 100000,10;

执行过程:

  1. 从第 1 条查到第 100010 条
  2. 扔掉前 100000 条
  3. 返回最后 10 条

回表 + 大量无效扫描 = 巨慢


2、4 种最优优化方案(生产必用)

1. 延迟关联(最推荐、最简单、90% 场景用它)

原理:先只查主键 ID,再用 ID 去关联查数据。

慢 SQL:

sql 复制代码
SELECT * FROM user LIMIT 100000,10;

优化后:

sql 复制代码
SELECT u.* FROM user u
JOIN (SELECT id FROM user ORDER BY id LIMIT 100000,10) AS temp
ON u.id = temp.id;

提升: 快 5~10 倍以上


2. 主键游标翻页(性能最强,禁止跳页)

适合:APP 列表下滑加载、无需跳页的场景(性能天花板)

原理:记住上一页最后一条 ID,下一页从 ID 开始查。

sql 复制代码
-- 第1页
SELECT * FROM user ORDER BY id LIMIT 10;

-- 第2页(记住上一页最后id=10)
SELECT * FROM user WHERE id > 10 ORDER BY id LIMIT 10;

-- 深分页(比如最后id=100000)
SELECT * FROM user WHERE id > 100000 ORDER BY id LIMIT 10;

性能: 无论多少页,速度完全一样,极速!

缺点:不能跳页(不能点 "第 1000 页")


3. 禁止深分页(产品层优化)

对于大厂来说:

  • 最多只允许查前 100/200/500 页
  • 超过直接提示:"请缩小搜索范围"
  • 深分页本身无业务意义

这是成本最低、效果最好的方案。


4. ES 搜索引擎(海量数据必用)

如果数据量 百万~千万以上 MySQL 深分页怎么优化都吃力,直接用 Elasticsearch

  • 天然适合搜索
  • 深分页极快
  • 支持复杂条件

3、必须加的关键优化(必做)

1. 排序字段必须加索引
复制代码
ORDER BY id -> id 必须是主键/索引
ORDER BY create_time -> create_time 必须加索引

没索引 = 深分页一定卡死

2. 绝对不要用 SELECT *

只查需要的字段,减少 IO。


4、方案选择总结

场景 最优方案
后台管理、需要跳页 延迟关联(JOIN 子查询)
APP 列表、下滑加载 主键游标(WHERE id > ?)
超大数据量 改用 ES
简单快速 限制最大页数

1. 延迟关联(支持跳页)

XML 复制代码
<select id="findByPage" resultType="User">
    SELECT u.* FROM user u
    JOIN (
        SELECT id FROM user
        ORDER BY id
        LIMIT #{offset}, #{pageSize}
    ) temp ON u.id = temp.id
</select>

2. 游标分页(性能最高)

XML 复制代码
<select id="findNextPage" resultType="User">
    SELECT * FROM user
    WHERE id > #{lastId}
    ORDER BY id
    LIMIT #{pageSize}
</select>

总结

  1. 深分页不要直接 limit big,10
  2. 优先用 延迟关联 或 游标分页
  3. 排序字段必须加索引
  4. 数据量超大 → 上 ES
相关推荐
野生技术架构师1 小时前
Tomcat Service的设计和实现:StandardService
java·tomcat
2401_831419441 小时前
Python分类汇总怎么做_Crosstab交叉表与多条件联合频数频率统计
jvm·数据库·python
2301_787312431 小时前
Go语言怎么用channel做信号通知_Go语言channel信号模式教程【完整】
jvm·数据库·python
Gofarlic_OMS1 小时前
UG/NX许可证管理高频技术问题解答汇编
java·大数据·运维·服务器·汇编·人工智能
逐星ing2 小时前
IDEA 无法识别 `mvn install` 最新 SNAPSHOT 依赖的根因与完整解决方案
java·ide·intellij-idea
2301_818008442 小时前
如何删除ASM中的数据文件_ALTER DISKGROUP DROP FILE彻底清除
jvm·数据库·python
IT界的老黄牛2 小时前
MySQL 磁盘告警 1.2TB:从衣柜原理到 gh-ost 卧底,一次释放 540GB 的实战复盘
运维·数据库·mysql
deviant-ART2 小时前
MySQL 实战:如何根据 ID 将表 B 的字段更新到表 A
数据库·mysql
流觞 无依2 小时前
Spring Boot 未授权访问漏洞排查与修复指南
java·spring boot·后端