我们团队最近排查了一个线上接口变慢的问题,慢得毫无征兆。翻了下慢查询日志,发现有一条语句执行了将近三秒,长这样:
sql复制SELECT * FROM orders
WHERE user_id = 10086
ORDER BY create_time DESC
LIMIT 100000, 20;
这是一个很常见的后台订单分页查询。数据量也不算大,orders表也就两百多万行,按user_id过滤后命中的结果集大概几十万。问题就出在那个 LIMIT 100000, 20 上。
我用 EXPLAIN 看了一眼执行计划,索引走的没问题,是 user_id 的二级索引,但扫描行数已经到了十万行。也就是说,MySQL 为了拿到第 100001 到 100020 这二十条数据,得先把前十万行全部扫出来,然后一个个丢掉。这不是咱们写错 SQL,也不是缺索引,而是 LIMIT 这种分页写法本身就带着一个性能陷阱。
这篇内容我把 LIMIT 相关的坑、原理和优化方案一次性说清楚,尤其是深分页、写操作加 LIMIT 的副作用、以及 FOR UPDATE SKIP LOCKED 这类锁查询组合,全部用实际场景拆开讲。
1. 别把 LIMIT 当成简单的"只取前 N 条":执行器远比你想的辛苦
LIMIT 的语法看起来简单到可以闭眼写:
sql复制SELECT * FROM table LIMIT 5;
很多人对它的理解停留在"返回前 5 行"这个层面。但你要是真这么理解,后面遇到 LIMIT 100000, 20 写法的性能问题就完全不知道从哪里排查。
1.1 LIMIT offset, count 的真实执行代价:拿到又丢掉
LIMIT 100000, 20 等价于 LIMIT 20 OFFSET 100000,意思是:跳过前十万行,从第 100001 行开始取二十行。
问题在于,MySQL 在执行的时候并不是"直接定位到第 100001 行",而是老老实实地从第一条满足 WHERE 条件的记录开始数,数到十万行,然后把这十万行全部丢弃,最后才取出你要的那二十行。用 MySQL 官网文档里的原话说:offset 那一堆行会被 server 层一条一条地读取并丢弃。
这就好比你在一本两百页的书里找第 100 页的内容,正常做法是翻到那一页,但 MySQL 的做法是从第 1 页开始,一页一页翻过去,翻到第 100 页的时候还要把前面 99 页的内容都过一遍脑子才能确定这就是你要的。单次这么干没问题,但你要是反复翻几百次,或者书的页数变成几百万页,体力消耗就完全不是一个量级了。
1.2 理解这个细节,才能解释线上那些奇慢的分页
我见过不少案例,SQL 优化之后索引全都到位了,type 是 ref,key 也对了,但深分页还是慢得离谱。原因就在这:索引帮 MySQL 定位到了 user_id = 10086 对应的第一条记录,但之后的十万次"找下一条"操作,每一次都是 B+ 树遍历、回表、逐行判断,这十万次循环省不掉,索引再合适也没用。
LIMIT 不是查询优化器能"智能跳过"的操作。它就是一个计数操作,跟 COUNT 一样,得一行行数。
所以在排查性能问题时,别一上来就盯着 EXPLAIN 看有没有用上索引,还要看看 LIMIT 的偏移量到底多大。偏移量过一万就已经需要警惕,过十万基本等于在扫一张小表。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 深分页为什么越往后翻越慢:B+树扫描与回表搬运的积少成多
要真正理解 LIMIT 的性能特征,得深入到 InnoDB 的索引结构里看一眼。
InnoDB 的索引是 B+ 树,叶子节点存的是完整的数据(聚簇索引)或者索引列加上主键值(二级索引)。当我们执行 SELECT * FROM orders WHERE user_id = 10086 ORDER BY create_time DESC LIMIT 100000, 20 的时候,执行过程大概是这样的:
- 根据
user_id这个二级索引定位到第一行满足条件的数据,此时拿到的是主键值。 - 用主键值去聚簇索引回表,把整行数据取出来。
- 判断是否满足
WHERE剩余条件(如果只按user_id过滤,这一步基本恒定满足)。 - 如果满足,计数加一,然后根据
ORDER BY create_time DESC维护排序。 - 重复以上过程,直到数够十万行。
- 丢弃前十万行,返回编号十万零一到十万零二十的数据。
这里每一步单看都不慢,但步骤 1 到 4 要循环十万次。每次循环都涉及两次 B+ 树查找(二级索引 + 聚簇索引回表),总共就是二十万次树查找。页面的随机 IO 就藏在这二十万次回表里,页不在内存就直接磁盘 IO。
有人会问,那我把 user_id 索引改成 (user_id, create_time) 联合索引,ORDER BY 是不是就不用排序了?对,排序确实可以用索引顺序避免 filesort,但 LIMIT 的十万次遍历和回表一个都省不掉。联合索引的叶子节点依然需要一个个数过去,数到十万条。
2.1 从 EXPLAIN 验证深分页的扫描行数
直接看执行计划最直观。假设有张表 t_user_orders,数据量 200 万行:
sql复制EXPLAIN SELECT * FROM t_user_orders
WHERE user_id = 1
LIMIT 100000, 20;
执行计划里 rows 那一列会显示一个很大的值(不同版本、不同统计
