1. 为什么我要把一条“只返回 10 行”的 SQL 拖出来解牛
SELECT * FROM orders WHERE id > 1000000 ORDER BY id LIMIT 10; 这条 SQL 看起来一点都不危险。它只返回 10 行,排序条件也简单,后面还带着 LIMIT 截断,是很多开发写完甚至不会多看一眼的“常规查询”。但正是这种 SQL,我在过去几年里陆续排查过好几次线上事故:有的是跑批任务把数据库 CPU 打满,有的是管理后台翻页越翻越慢,最后发现根因都能回溯到这一句的变形上。
先说清楚这条 SQL 在业务里到底干什么。最常见的是两类场景:一类是后台任务要做数据订正或增量同步,程序写一个循环,每次取 10 条处理,处理完再取下一批;另一类是运营后台的分页查询,页面上用户想看“ID 大于某个值的订单”,开发顺手用这个条件取前 10 条展示。无论哪种场景,大家写的时候思维都很一致:反正我只要 10 条,数据库再慢能慢到哪去?
这句话恰恰是问题所在。数据库执行一条 SQL 的成本,几乎从来不取决于最终返回几行,而取决于为了确认“这几行确实满足条件”扫描了多少数据。你可以想象成点外卖:最后 10 米的路当然不难走,可骑手已经骑了 10 公里,时间全花在前面。LIMIT 10 只是外卖送到你手上那 10 米,真正决定快慢的是 WHERE id > 1000000 这条检索路径打通得顺不顺。
所以在解这条 SQL 之前,必须先搞清楚三件事:第一个,orders 表里的 id 到底是不是主键;第二个,这张表现在大概有多少行,历史数据有没有大量删除;第三个,ORDER BY id 和 WHERE 条件是否能共用同一个索引路径。这三个问题答案不同,这句话的执行计划可能天差地别,这正是“庖丁解牛”最有意思的地方:表面同一头牛,骨架结构却不总是一样的。
1.1 人畜无害的第一印象是怎么产生的
把这条 SQL 单独拿出来看,几乎所有基础教程都会告诉你:先查所有 id > 1000000 的订单,然后按 id 升序排,最后只截取前 10 条。关键词也很直白:SELECT 查谁,WHERE 过滤谁,ORDER BY 怎么排,LIMIT 取多少。这套思维模型对学习 SQL 没有错,但它是一种“结果视角”,不是“执行视角”。
结果视角最大的盲区在于:它会让你下意识认为数据库是先把所有满足条件的行找出来,再排序,再切出 10 条。可真实世界里,如果表里有几千万甚至几亿行,先把所有满足条件的行全部找出来这一步本身就可能要跑几十秒,根本不给你后面排序和截断的机会。换句话说,这条 SQL 是否慢,在 WHERE 条件写完的那一刻就已经决定了大半,LIMIT 10 只是在最后的表演环节做了一次裁剪而已。
1.2 决定“10 行”是快是慢的,从来不是返回行数
我在给团队做慢 SQL 复盘时经常说一句话:分析 SQL 不要看结果行数,要看扫描行数。同样是返回 10 行,有的语句可能只扫描了 10 行索引,有的语句却要把整张表几千万行全部过一遍。两者的资源消耗差距是几个数量级,但在业务日志里,它们看起来都一样“只返回了 10 条”。
这里可以引入一个非常粗略的估算公式:一条查询的总耗时,约等于扫描的数据页数量乘以单页访问成本,再加上排序、回表、网络传输等附加成本。返回 10 行的情况下,网络传输成本几乎可以忽略,排序如果走对了索引也可以忽略,最大的变量就落在“为了找到这
