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
相关推荐
廿一夏2 小时前
MySql存储引擎与索引
数据库·sql·mysql
Mahir082 小时前
Spring 循环依赖深度解密:从问题本质到三级缓存源码级解析
java·后端·spring·缓存·面试·循环依赖·三级缓存
RyFit3 小时前
SpringAI 常见问题及解决方案大全
java·ai
石山代码3 小时前
C++ 内存分区 堆区
java·开发语言·c++
绝知此事4 小时前
【算法突围 01】线性结构与哈希表:后端开发的收纳术
java·数据结构·算法·面试·jdk·散列表
无风听海4 小时前
C# 隐式转换深度解析
java·开发语言·c#
lzhdim4 小时前
SQL 入门 15:SQL 事务:从 ACID 到四种常见的并发问题
数据库·sql
瀚高PG实验室4 小时前
瀚高企业版V9.1.1在pg_restore还原备份文件时提示extract函数语法问题
数据库·瀚高数据库
一只大袋鼠5 小时前
Git 进阶(二):分支管理、暂存栈、远程仓库与多人协作
java·开发语言·git
TDengine (老段)5 小时前
TDengine Tag 设计哲学与 Schema 变更机制
大数据·数据库·物联网·时序数据库·iot·tdengine·涛思数据