【MySQL篇】limit深度分页性能:从原理理解小偏移量limit 1,200 vs 百万级偏移量limit 1000000,200的差异
💫《博主主页》:奈斯DB-CSDN博客
🔥《擅长领域》:擅长阿里云AnalyticDB for MySQL(分布式数据仓库)、Oracle、MySQL、Linux、prometheus监控;并对SQLserver、NoSQL(MongoDB)有了解
💖如果觉得文章对你有所帮助,欢迎点赞收藏加关注💖
在日常数据库开发中,LIMIT分页是写SQL时最常用的语句。在实际使用分页语句分别执行 LIMIT 1,200 和 LIMIT 1000000,200 时,虽然两者都只返回200条数据,但后者却比前者慢了几个数量级。
为了弄清这个问题背后的原因,博主也是查阅了一些网上的资料和MySQL8.0官方文档手册,研究了MySQL处理LIMIT分页的内部机制。理解这些底层原理,对于编写高效的SQL查询至关重要。只有掌握了LIMIT分页的工作原理,我们才能避免性能陷阱,让分页查询不再成为系统的瓶颈,才能更好地在SQL语句中使用LIMIT,优化查询性能。
LIMIT 的基本原理:
语法:
SELECT * FROM table_name LIMIT [offset,] row_count; 参数说明: row_count:指定要返回的最大行数 offset:指定要跳过的行数(从0开始计数)
原理:
MySQL 中的 LIMIT 子句用于限制查询返回的行数,其核心原理是在查询处理过程中提前终止结果集的生成。以下是 LIMIT 的主要工作机制:
查询执行过程中计数:MySQL 在执行查询时会维护一个计数器,每产生一行结果就递增
提前终止机制:当计数器达到 LIMIT 指定的行数时,立即停止查询执行
偏移处理:对于 LIMIT offset, count,MySQL 会先跳过 offset 行,然后开始计数
全表扫描 + LIMIT:
使用索引 + LIMIT(最优情况):
ORDER BY + LIMIT 的两种处理方式:
清楚了上面的原理,那么对比一下LIMIT 1,200和LIMIT 1000000,200
LIMIT 分页实现机制决定了其性能特点:当执行 LIMIT m,n 时,数据库引擎需要先扫描并获取前 m+n 条完整记录,然后丢弃前 m 条,最终只返回剩下的 n 条结果。
这种工作机制导致了一个关键的性能特征:分页查询的效率与偏移量 m 的大小直接相关。具体表现为:
LIMIT 1,200 只需读取 201 条数据,丢弃第一行数据然后返回
LIMIT 1000000,200 则需要先读取 1000200 条记录,再丢弃前 1000000 条
这种实现方式解释了为什么深度分页(大偏移量)的性能会显著下降。随着偏移量 m 的增加,数据库需要处理的数据量呈线性增长,即使最终返回的记录数 n 保持不变。这就是 LIMIT 1000000,200 比 LIMIT 1,200 慢得多的根本原因。
LIMIT 查询优化:
以下部分知识点来源于 MySQL 公开可查的官方文档手册(MySQL 8.0):https://dev.mysql.com/doc/refman/8.0/en/limit-optimization.html
如果只需要从结果集中获取指定数量的行,请在查询中使用
LIMIT
子句,而不是获取整个结果集并丢弃多余的数据。MySQL 在某些情况下优化带有
LIMIT row_count
子句且没有HAVING
子句的查询:
如果只选择少量行并使用
LIMIT
,MySQL 在某些情况下会使用索引,而通常情况下会选择进行全表扫描。如果将
LIMIT row_count
与ORDER BY
结合使用,MySQL 会在找到前row_count
行的排序结果后立即停止排序,而不是对整个结果集进行排序。如果排序是通过索引完成的,这种操作非常快速。如果必须进行文件排序(filesort),MySQL 会先选择所有符合查询条件的行,并对它们进行排序,直到找到前row_count
行为止。在找到初始的行之后,MySQL 不会再对剩余的结果集进行排序。这种行为的一个表现是,带
ORDER BY
和不带LIMIT
的查询可能会返回不同顺序的行,正如下面所述。如果将
LIMIT row_count
与DISTINCT
结合使用,MySQL 会在找到row_count
个唯一的行后立即停止。在某些情况下,
GROUP BY
可以通过按索引顺序读取数据(或者对索引进行排序)来解决,然后计算汇总,直到索引值发生变化。在这种情况下,LIMIT row_count
不会计算不必要的GROUP BY
值。一旦 MySQL 将所需数量的行发送到客户端,它就会中止查询,除非你使用了
SQL_CALC_FOUND_ROWS
。在这种情况下,可以通过SELECT FOUND_ROWS()
获取总行数。参见:https://dev.mysql.com/doc/refman/8.0/en/information-functions.html
LIMIT 0
会快速返回空结果集。这在检查查询有效性时非常有用,也可以用于在使用 MySQL API 的应用程序中获取结果列的类型。使用mysql
客户端时,可以使用--column-type-info
选项来显示结果列类型。如果服务器使用临时表来解决查询,它会使用
LIMIT row_count
子句来计算所需的空间。如果
ORDER BY
没有使用索引,但同时存在LIMIT
子句,优化器可能能够避免使用合并文件(merge file),而是通过内存中的文件排序(in-memory filesort)操作对行进行排序。如果多行在
ORDER BY
列中具有相同的值,服务器可以自由地以任何顺序返回这些行,并且可能会根据整体执行计划以不同的顺序返回这些行。换句话说,这些行的排序顺序在非排序列上是非确定性的。一个影响执行计划的因素是
LIMIT
,因此带有LIMIT
和不带LIMIT
的ORDER BY
查询可能会返回不同顺序的行。
LIMIT 与 ORDER BY 配合使用的不确定性示例:
当 ORDER BY 子句排序的字段存在重复值时,MySQL 对这些相同值的行记录可以以任意顺序返回。这种不确定性源于:
执行计划影响:MySQL 优化器可能选择不同的执行计划
存储引擎特性:不同存储引擎处理数据的方式不同
并行处理机制:多线程处理可能导致结果顺序变化
因此,当 ORDER BY 语句中有 LIMIT 时,每次查询结果都有可能不同。
假设有一个员工表
employees
,其中department_id
字段有重复值:-- 创建示例表 CREATE TABLE employees ( id INT PRIMARY KEY, name VARCHAR(50), department_id INT, salary DECIMAL(10,2) ); -- 插入示例数据 INSERT INTO employees VALUES (1, '张三', 101, 5000), (2, '李四', 101, 6000), (3, '王五', 102, 5500), (4, '赵六', 102, 6500), (5, '钱七', 101, 5200);
不确定排序结果示例
-- 第一次查询可能返回 SELECT * FROM employees ORDER BY department_id LIMIT 3; -- 结果可能为: | id | name | department_id | salary | |----|------|---------------|--------| | 1 | 张三 | 101 | 5000 | | 2 | 李四 | 101 | 6000 | | 5 | 钱七 | 101 | 5200 | -- 第二次相同查询可能返回 SELECT * FROM employees ORDER BY department_id LIMIT 3; -- 结果可能变为: | id | name | department_id | salary | |----|------|---------------|--------| | 5 | 钱七 | 101 | 5200 | | 1 | 张三 | 101 | 5000 | | 2 | 李四 | 101 | 6000 |
要确保完全确定性的排序结果,应在 ORDER BY 子句中添加足够唯一的列,比如像 id 这样保证不会重复的字段:
-- 添加主键作为次要排序条件确保结果稳定 SELECT * FROM employees ORDER BY department_id, id -- id是主键,确保唯一性 LIMIT 3; -- 这样每次查询都会返回相同顺序的结果 | id | name | department_id | salary | |----|------|---------------|--------| | 1 | 张三 | 101 | 5000 | | 2 | 李四 | 101 | 6000 | | 5 | 钱七 | 101 | 5200 |
LIMIT 与 ORDER BY 配合使用的不确定性实际应用建议:
在分页查询中,总是确保 ORDER BY 包含足够唯一的列
对于有重复值的排序字段,添加主键或其他唯一列作为次要排序条件
在需要精确排序的业务场景中,避免仅依赖可能有重复值的字段排序
🌹 完结,撒花 🌹