Mybatis 分页查询统计方法重写,在查询方法后面固定追加:"_COUNT"
比如:
分页查询方法:pageQuery
重写分页查询统计:pageQuery_COUNT
单表查询统计:select count(1) from xxxdb.t_order
sql 查询优化
利用LIMIT语法确认偏移量,缩小数据范围,随后依据ID查询主表。
- 结合前端分页传参动态计算数据的偏移量
- 条件筛选字段仅为id,并根据id字段升序排列
- 以查询结果的id为结果集 inner join 全表字段
SELECT t.* FROM (SELECT id FROM xxxdb.t_order ORDER BY id LIMIT 1000000, 10) tmp
inner join xxxdb.t_order t ON t.id = tmp.id
limit value 计算逻辑
LIMIT offset, rows
offset = limit的偏移量 = (pageNumber-1) *pageSize = 1000000
rows = limit的限定行 = pageSize = 10
Deepseek 对比以下sql的性能
sql1:
SELECT t.* FROM (SELECT id FROM xxxdb.t_order ORDER BY id LIMIT 1000000, 10) tmp
inner join xxxdb.t_order t ON t.id = tmp.idsql2:
SELECT * FROM xxxdb.t_order t
ORDER BY t.id
LIMIT 1000000, 10
结论先行:在绝大多数情况下,SQL1 的性能远优于 SQL2。
下面是详细的分析和原因解释。
核心区别
两条 SQL 的目标完全相同:获取 xxxdb.t_order
表中第 1000000 行开始的 10 条记录,并按 id
主键排序。
它们的根本区别在于 执行路径 不同。
SQL2 分析:SELECT * ... LIMIT 1000000, 10
SELECT * FROM xxxdb.t_order t
ORDER BY t.id
LIMIT 1000000, 10
-
执行过程:
- 首先,服务器需要构建一个完整的结果集。它会读取所有满足条件的行(这里是整个表),并按照
ORDER BY t.id
进行排序。 - 由于
id
是主键,并且ORDER BY
与主键一致,理论上可以利用主键索引来避免一次全表排序。这是一个优化点。 - 但是,
LIMIT 1000000, 10
意味着 MySQL 必须顺序扫描直到找到第 1000000 条记录。它会从第 1 条开始,一条一条地数,数过 1000000 条之后,再返回接下来的 10 条。 - 在这个过程中,它需要读取这 1000010 条记录的完整行数据(因为你是
SELECT *
),这包括所有列,可能包含TEXT
,BLOB
等大字段。
- 首先,服务器需要构建一个完整的结果集。它会读取所有满足条件的行(这里是整个表),并按照
-
性能瓶颈:
- 巨大的 I/O 开销:即使只需要 10 条数据,数据库也需要从存储中读取超过 100 万条完整的数据行。如果表很宽(列很多),这个 I/O 量会非常惊人。
- 巨大的网络和内存开销:虽然最终只返回10行,但服务器内部需要准备超过100万行的数据,这会消耗大量内存和CPU资源进行数据的临时存储和排序(如果无法走索引排序的话)。
SQL1 分析:先查ID,再回表
SELECT t.* FROM (SELECT id FROM xxxdb.t_order ORDER BY id LIMIT 1000000, 10) tmp
inner join xxxdb.t_order t ON t.id = tmp.id
-
执行过程(分两步):
- 第1步 - 子查询:
SELECT id FROM ... LIMIT 1000000, 10
- 这个子查询只查询
id
这一列。由于id
是主键,这个查询可以完全通过遍历主键索引(聚簇索引的叶子节点)来完成。 - 主键索引的叶子节点只包含
id
值本身(以及指向实际数据的指针)。这意味着:- 需要读取的数据量远小于SQL2。索引文件通常比数据文件小得多。
- 同样的,它也需要扫描索引中的 1000010 个条目,但因为它只读取
id
这一个轻量级的字段,速度会快几个数量级。
- 这个子查询只查询
- 第2步 - 回表查询:
INNER JOIN ... ON t.id = tmp.id
- 子查询得到10个目标
id
后,再用这10个id
去主表(聚簇索引)里精确查找对应的10条完整记录。 - 这个过程称为 "回表"(Bookmark Lookup)。由于是通过主键
id
等值连接,这10次查询是非常快速的。
- 子查询得到10个目标
- 第1步 - 子查询:
-
性能优势:
- 极致的 I/O 优化:将一次需要读取 100 万条宽记录的沉重操作,拆解成了:
- 一次读取 100 万个轻量级索引键的操作。
- 十次通过主键精确查找的极速操作。
- 第一步虽然扫描量大,但数据密度高,速度快。第二步是点查,效率极高。总体耗时远低于SQL2的单次沉重扫描。
- 极致的 I/O 优化:将一次需要读取 100 万条宽记录的沉重操作,拆解成了:
对比表格
特性 | SQL2 (直接LIMIT) | SQL1 (子查询+回表) | 胜出方 |
---|---|---|---|
数据扫描量 | 扫描 1000010 条完整记录 | 扫描 1000010 个索引键 + 10 条完整记录 | SQL1 |
I/O 压力 | 非常大(涉及所有列) | 较小(主要操作在紧凑的索引上) | SQL1 |
执行速度 | 慢,随 Offset 值线性增长 | 快,虽然也受 Offset 影响,但基数小得多 | SQL1 |
网络/内存开销 | 高(在服务器端准备大量数据) | 低(服务器端只处理ID和最终10条数据) | SQL1 |
代码可读性 | 高,简洁明了 | 较低,需要理解其优化意图 | SQL2 |
总结与建议
- 对于大偏移量的分页查询,SQL1 是标准的性能优化写法。 它通过利用覆盖索引(在这个案例中就是主键索引本身)的优势,最大限度地减少了数据访问量,从而大幅提升性能。
- SQL2 在偏移量很小(比如
LIMIT 0, 10
)时,性能可能与 SQL1 相差无几,甚至因为减少了一次关联而略快。 但随着OFFSET
(本例中的 1000000)的增大,SQL2 的性能会急剧下降,而 SQL1 的性能下降曲线则平缓得多。
最佳实践:
对于这种深度分页问题,除了使用 SQL1 的优化模式,还可以考虑以下更进一步的优化:
- 游标分页(Cursor-based Pagination):不使用
LIMIT offset, number
,而是记录上一页最后一条记录的id
,然后查询WHERE id > last_id LIMIT 10
。这种方式可以做到常数时间的查询速度,但牺牲了直接跳到任意页的能力。这在移动端App和现代Web应用中非常流行。