SQL优化实战:从慢SQL到长SQL的排查与改写指南 1. 从两条“病态SQL”说起长与慢是两个维度的求生问题我在一家电商团队做技术负责人那会儿最怕的不是突然的流量高峰而是监控群里半夜弹出的慢SQL告警。有一回业务方反馈后台订单报表打开要十几秒我顺手把那条SQL捞出来一看好家伙一条语句里塞了五六个子查询每个子查询都在一张近亿行的订单表上做聚合。这还不算完等我把执行计划打开type一栏齐刷刷写着ALL也就是全表扫描。那一刻我就明白这不是什么偶发问题而是“长SQL”和“慢SQL”两个问题在同一个系统里同时爆了。先帮大家把概念拎清楚**“长SQL”通常指语句本身极其复杂——子查询嵌套、多个大表JOIN、几十个字段的聚合、一大串OR条件读起来头大执行起来更头大。“慢SQL”则指执行时间超标可能是长SQL导致的也可能是一条看起来很简单的查询因为没走索引而慢得离谱。**两者有交集但优化思路并不完全一样长SQL的关键词是“拆”慢SQL的关键词是“路径”——找到数据库最省力的执行路径。这个领域有个很常见的误区很多人一谈SQL优化就只想到“加索引”。实际上索引只是方案之一而且用不好还会帮倒忙。真正靠谱的优化思路是从设计层面把问题解决掉再靠改写技巧去修补历史遗留的坑最后用工具和验证手段把收益固化下来。这篇文章我就按这个思路把我这些年处理线上慢SQL、重构长SQL的实战经验完整写出来。不管你用的是MySQL、PostgreSQL还是其他关系型数据库这套方法论基本都是相通的。适合谁看如果你是刚接触数据库优化的开发同学这篇文章能帮你建立一套完整的排查框架如果你已经写过一些SQL、遇到过“明明加了索引还是很慢”的困惑这里也会有对应的坑和答案。我尽量不绕弯子直接给你能落地的东西。2. 动手前先找病灶如何确认一条SQL是真的“慢”老话说得好对症才能下药。你连SQL慢在哪都不知道上来就改写大概率是瞎忙活。所以第一步永远是找到慢SQL并看清它到底慢在哪儿。2.1 开启慢查询日志让“嫌疑人”自己浮出来大多数关系型数据库都提供慢查询日志也就是专门记录执行时间超过阈值的SQL。以最常用的MySQL为例# 查看当前慢查询日志是否开启 SHOW VARIABLES LIKE slow_query_log; # 开启慢查询日志并设置阈值单位是秒 SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1;long_query_time我通常建议先设为1秒——线上业务里超过1秒的查询在并发稍高时已经足以引发问题。如果你的系统容忍度更低也可以设为0.5秒或更低但要注意日志量会相应增大可能反过来影响磁盘IO。设好之后跑一段业务再去翻日志就能看到一批执行时间超标的SQL语句。提示慢查询日志记录的是真实运行中的SQL这是最重要的“嫌疑犯清单”。但请记住日志只是告诉你“这条SQL耗时超过阈值”并不直接告诉你“它为什么慢”。下一步必须去看执行计划。2.2 用EXPLAIN看执行计划摸清数据库的“行驶路线”拿到可疑SQL之后下一步就是在它前面加一个EXPLAIN关键字让数据库告诉你它打算怎么执行这条SQL。这是整个优化流程里最核心的一步没有之一。EXPLAIN SELECT * FROM orders WHERE user_id 10086 AND status 1;执行计划里需要重点看几个字段它们就像汽车的仪表盘字段看到什么说明typeALL全表扫描说明这条SQL在“徒步走完全程”急需优化typeindex、range、ref至少用上索引了情况尚可rows一个很大的数字预估扫描的行数行数越多通常越慢ExtraUsing filesort使用了文件排序数据量大时是性能杀手ExtraUsing temporary使用了临时表意味着一批数据被复制到内存或磁盘再做处理key空或PRIMARY没有命中任何索引基本可以确定是“没走索引”的坑举个我实际排查过的例子某条查询WHERE order_id 12345看表结构发现order_id有唯一索引但执行计划里的type却是ALL、key是空的。一核对发现是这个字段在创建表时被定义成了VARCHAR而查询参数传进去的是整数。数据库在比较不一致的时候会把字段隐式转换成数字结果索引在转换后的列上失效了。这种坑光看代码看不出来只有执行计划能帮你抓到现行。再提醒一个思路在拿到执行计划之后不要急着只看有没有“索引”而是要看整条路线的“代价”。rows估算值、有没有排序、有没有回表这三点基本能判断一条SQL的整改空间有多大。2.3 用Profile等工具量化耗时没有再凭感觉“猜”执行计划告诉你“怎么走”但没告诉你“每一步走了多久”。这时候可以用SHOW PROFILE或performance_schema来量化各阶段耗时。MySQL 8.0以后SHOW PROFILE被标记为废弃更推荐用performance_schema里的events_statements_history_long表来查看或者直接用各类可视化工具看慢SQL的报告。这里我不展开配置细节只说一个操作要点**当你拿到各阶段的耗时数据后优先关注“锁等待”和“临时表”这一类异常项而不是单纯看总耗时。**因为总耗时长可能就是等待时间长而不是计算时间长。前者往往要查并发、查锁冲突后者才要查SQL写法。起点判断错了后面全白搭。有了“慢日志找SQL、执行计划看路线、Profile算耗时”这一套组合拳你才能精准地把问题定位到根上。接下来我讲讲怎么在源头就把“长SQL”“慢SQL”这两个病根给拔掉。3. 防患于未然从设计层面避免长SQL的出现我见过太多人平时写SQL怎么方便怎么来等出问题了再跪求优化。但实际上很多“慢”是从表和SQL的设计阶段就埋下的。这一节聊聊怎么从源头把坑填平。3.1 把大表“拆”开分区表和分库分表的选择当一张表的行数上了千万甚至过亿任何SQL操作都会开始变沉重。这时候首先要想的不是怎么优化SQL而是“这张表本身是不是设计得太大了”。常见的解法有两种分区表和分库分表。分区表适合单表数据量大但日常查询有比较明确的“分区键”场景。比如订单表可以按月份分区查询时就只扫对应月份的数据物理上等于“把一张大表做成了多个小表”。它对应用层是透明的SQL不需要改写成本相对低。分库分表则适合数据量和写入并发都已经高到单库扛不住的场景。比如把用户表按用户ID哈希到多个库多个表同一个查询只落到其中一个分片。这个方案的代价是要在代码层做路由引入中间件或自研路由逻辑运维复杂度明显上升。我个人的经验是**能分区解决的问题不要轻易上分库分表。**分库分表一旦上了后面所有跨分片查询、全局主键、分布式事务都要跟着改牵一发动全身。3.2 字段设计里的三个“减法原则”除了拆表字段层面的设计也一样能决定SQL的生死。我总结了三个“减法”**原则一尽量减去无意义的宽字段。**有些表为了“方便扩展”动不动就放个TEXT类型存一大段JSON字符串。一条查询即使走了索引只要SELECT里带了这种大字段就可能因为要回表去取大文本而明显变慢。能用 JSON 拆分、能单独放扩展表的就别塞在主表里一起查。**原则二能减联合索引的冗余就减掉。**比如已经有了(user_id, status)这个联合索引单独建user_id索引很多时候是多余的因为联合索引的最左前缀已经覆盖了user_id单列查询。索引多了不仅占空间还拖慢写入性能属于“反向优化”。**原则三减去不必要的精确类型。**存储IP用整型而非字符串、存状态用TINYINT而非VARCHAR这些都是老生常谈。类型选小了索引能装进内存的更多查询自然更快。更重要的是这能避免我前面说的“隐式类型转换导致索引失效”那种坑。3.3 别把所有读请求都压在业务库上很多时候一个SQL慢不是它本身写得不好而是业务库的负载已经过重了。这种情况你再怎么优化SQL收益都有限。一个很常见的设计思路是给主库做读写分离把那些“报表、统计、后台查询”这些重活匀到只读从库上。从库可以单独配置更适合跑大数据量查询的参数——比如把sort_buffer_size调大、把innodb_buffer_pool_size给足。主库则专心处理事务性写入和核心查询两边互不干扰。这里要啰嗦一句**读写分离一定要处理好主从延迟的问题。**如果应用正好需要读刚写入的数据结果从库还差几百毫秒没同步过来就会读到旧数据。做法要么是让这类强一致性读走主库要么在业务层面接受短暂延迟。设计层面的这几件事如果你能做到至少能挡掉六成以上的“长SQL”“慢SQL”问题。剩下的才是真正需要靠改写技巧去解决的硬骨头咱们接下来重点说。4. 改写技巧实战从“长”到“短”、从“慢”到“快”到了这一节终于要动刀了。我按照从“治标”到“治本”的顺序把最常用的改写手段拆成几个方向每个都会给出实际SQL例子和为什么这样改的理由。4.1 拆分“巨型查询”一条长SQL拆成多次短查询先看一个非常典型的“长SQL”场景——把多个维度的统计写在一条SQL里试图一次查全-- 这样写不是不行但每个子查询都要对一张千万级大表做聚合 SELECT (SELECT COUNT(*) FROM orders WHERE status 1) AS pending_count, (SELECT COUNT(*) FROM orders WHERE status 2) AS shipped_count, (SELECT COUNT(*) FROM orders WHERE created_at NOW() - INTERVAL 7 DAY) AS week_orders FROM dual;表面上只是一条SQL实际上它可能触发了多次全表扫描或索引扫描每一行还都重复执行一遍子查询。数据量一大这条SQL能把数据库打到怀疑人生。我的做法是把不同维度的统计拆开分别执行必要时在代码层做一次合并-- 第一次查询 SELECT COUNT(*) FROM orders WHERE status 1; -- 第二次查询 SELECT COUNT(*) FROM orders WHERE status 2; -- 第三次查询 SELECT COUNT(*) FROM orders WHERE created_at NOW() - INTERVAL 7 DAY;你可能觉得“这不是多了几次数据库交互吗”——没错但每次查询都能精准地走自己那条索引执行的代价远小于一条把众多子查询硬拼起来的巨型SQL。而且拆开之后每条语句都能独立做缓存、独立看执行计划排查问题也清晰得多。我的原则是单条SQL的复杂度一旦超过“一眼看不懂”的程度就该考虑拆。4.2 给SELECT做减法只取需要的列和行有一种“长SQL”纯粹是因为字段太多太宽撑起来的。比如不管业务需不需要习惯性写SELECT *-- 这种写法看似方便实际可能把一坨TEXT字段也拉出来传输 SELECT * FROM order_detail WHERE order_id 10086;如果这张表有30个字段里面还带几个TEXT大字段哪怕命中索引也需要回表去取整行数据。改成只取要用的列之后数据库的回表成本、网络传输字节数都会显著下降SELECT order_id, product_name, quantity, amount FROM order_detail WHERE order_id 10086;同理LIMIT该加就加。很多人写分页查询时会用LIMIT 100000, 20这种“深分页”写法在MySQL里是出了名的慢因为它要先把前10万行全查出来然后再扔掉。优化方式可以改成用“上次查询最后一条记录的ID”来翻页-- 用ID大于上页末位来翻页 SELECT order_id, amount FROM orders WHERE order_id 100860 ORDER BY order_id LIMIT 20;这种方式不需要数据库去扫描并丢弃前面那10万行速度能快上好几个数量级。注意这种写法要求ID是连续且有序的如果业务里有物理删除导致ID断层就得再结合其他条件做设计。4.3 JOIN和子查询的取舍用对姿势才能飞JOIN和子查询之间的性能之争网上吵了很多年。我的判断很简单**MySQL 5.5/5.6时代很多子查询优化器做得不好IN子查询经常被转换成低效的执行计划所以大家偏爱JOIN。到了MySQL 5.7以后优化器已经把很多子查询改写成半连接直接写JOIN和直接写子查询在多数场景下差别不大了。**真正需要关注的反而是JOIN本身怎么写得高效。最核心的一条就是**JOIN字段一定要有索引且类型一致。**如果拿一个VARCHAR去JOIN一个INT照样会出现隐式转换索引失效然后就是恐怖的嵌套循环全表匹配。-- join的字段如果没索引关联时等于在循环里查全表 SELECT a.order_no, b.user_name FROM orders a LEFT JOIN users b ON a.user_id b.user_id WHERE a.created_at 2024-01-01;这里users.user_id必须有索引否则对于orders里每一行数据库都去users表里做一次全表扫描量一起来直接卡死。执行计划里如果看到Using join buffer (Block Nested Loop)基本就是在做这种低效匹配先把关联字段的索引补上再说。4.4 优化 OR 和 IN改变条件的写法OR是慢SQL的重灾区。因为OR意味着多个条件的并集优化器往往无法同时高效地利用多个索引最后变成全表扫描-- 用OR时即使两列都有索引优化器也可能弃之不用 SELECT * FROM orders WHERE user_id 10086 OR status 1;遇到这种写法优先考虑改成UNION ALLSELECT * FROM orders WHERE user_id 10086 UNION ALL SELECT * FROM orders WHERE status 1;UNION ALL能让两部分各自走自己的索引再做一个合并。但要提醒**如果两个条件本身就能命中同一个索引或者数据量不大那UNION ALL反而可能更慢因为它多了一次排序或合并操作。**所以这不是银弹得看执行计划说话。IN列表呢如果列表里的值很少用IN没什么大问题。如果列表特别长——比如上千个ID——那就要重新考虑了尤其是当这些ID不连续、跨区分布的时候优化器可能会放弃索引跳跃扫描。一个稳妥的办法是**把大IN拆成多个小批次的IN分批执行或者和业务方确认能不能改成JOIN一张临时表。**临时表的方式虽然多了一步导入但是在ID集合非常大的时候效果是立竿见影的。4.5 ORDER BY和GROUP BY排序与分组的性能黑手慢SQL里ORDER BY和GROUP BY出场率极高。尤其是一张千万级大表做ORDER BY created_at DESC如果created_at没索引数据库就得把所有满足条件的数据放到排序缓冲区里做文件排序也就是执行计划里的Using filesort。数据量大而缓冲区分不下时还会写临时文件到磁盘慢上加慢。最直接的解法是**让排序字段出现在索引里。**比如建一个(user_id, created_at)联合索引那么查询WHERE user_id ? ORDER BY created_at DESC时数据库直接按索引顺序读取连排序都省了。而GROUP BY的优化思路也类似——分组字段有索引数据库就能一次顺序扫描完成分组计数否则就是哈希聚合或临时表聚合。另外一个容易被忽略的点是带函数或表达式的排序。比如ORDER BY DATE_FORMAT(created_at, %Y-%m-%d)这种写法就是给排序字段套了函数索引在表达式上直接失效。要优化就得把查询条件改成created_at 2024-01-01 AND created_at 2024-02-01这种范围写法或者使用生成列配合索引而不是在查询里对字段做函数运算。注意这里说的“让排序字段进索引”并不是建议你为每一种排序都单独建索引。索引太多写入放慢内存占用变大。更好的做法是结合真实业务挑选“最常出现、最关键”的排序/分组组合一两个联合索引覆盖80%的场景就够了。5. 索引利用的进阶思路不只会建更要会用前面已经反复提到索引因为大部分慢SQL的根因就是“没用对索引”。这一节把它单独拎出来讲透讲讲索引的创建原则、组合索引的设计以及一些容易被忽略的失效场景。5.1 联合索引的“最左前缀”到底怎么理解联合索引(a, b, c)能匹配的查询方式遵循最左前缀原则它可以用于(a)、(a, b)、(a, b, c)这三种组合但用不上(b, c)或只查(c)。很多人把这个当成死记硬背的规则其实理解它很简单**联合索引在B树里是先按a排好序然后相同a再按b排相同b再按c排的。**你想按b直接查找等于在一本先按姓氏、再按名字排的电话本里只按名字去查——根本无从查起。所以在设计联合索引时我建议优先级这么定等值条件的字段放前面因为等值条件定位最精确。区分度高的字段放前面比如用户ID、订单号能让索引迅速缩小范围。范围查询的字段放后面比如创建时间、金额区间放在前面容易让后面的字段失去作用。有了这个思维你去设计(status, created_at)这类索引时就不会再凭感觉乱排了。5.2 常见索引失效场景清单我把自己踩过的和排查过的“索引失效”场景整理成了一份清单建议你收藏起来场景原因修法WHERE name LIKE %关键词前置通配符导致无法走索引改成后缀匹配关键词%或考虑全文索引WHERE YEAR(created_at) 2024对索引字段做函数运算改写为created_at 2024-01-01 AND created_at 2025-01-01WHERE user_id 1 10086对索引字段做表达式运算改写为user_id 10086 - 1WHERE status ! 1不等于条件通常不走索引尝试改写为两个范围的并集或者评估全表扫描是否可接受WHERE a 1 OR b 2OR导致索引合并困难用UNION ALL拆分col NULL或col ! NULLNULL判断索引失效设计表时尽量避免或使用IS NULL配合特殊索引场景这张表里每一项我都在实际项目里见过真身。尤其LIKE %关键词这种前端搜索框一传%开头整张表就遭殃。要么在代码层做限制要么就别让用户随便输入前置通配符。5.3 覆盖索引一个被严重低估的优化利器很多开发者知道索引但不知道“覆盖索引”这个概念。简单说就是**索引里已经包含了查询需要的所有字段那么数据库连表都不用回光扫索引就有结果了。**这在InnoDB里效果非常明显因为回表需要做随机IO而扫描索引是顺序IO速度差距可能在数量级。举例说明-- 这条查询如果只建了 (user_id, status)那查询计划里通常还要回表取amount SELECT user_id, status, amount FROM orders WHERE user_id 10086 AND status 1; -- 如果建的是 (user_id, status, amount)三个字段都在索引里直接覆盖那是不是意味着把所有需要的字段都塞进索引呢当然不是——索引越长占空间越大写入维护成本越高。折中的做法是**把查询频次极高、字段又不多的场景做覆盖索引长字段、大字段坚决不进索引。**比如商品列表页最常见的是查product_id, name, price, image这几个字段都不算大可以考虑做一个覆盖索引来扛住列表性能。在选择上我总是先看业务的热点查询是哪几条再决定做哪几个覆盖索引而不是见一个建一个。6. 统计大表和复杂查询的专项优化前面讲的是通用优化技巧但有一类SQL特别麻烦那就是对超大数据量做统计、报表、汇总的查询。这类查询要扫的数据多、计算量大而且经常是“长SQL”的代名词。这一节单独说说我见过几种高效的专项优化手段。6.1 预处理聚合用汇总表换实时计算先说一个非常直观的场景后台要展示“今日新增订单数”“本周销售额”这种统计。最粗暴的做法就是每次刷新都对订单表扫一遍按天聚合。可订单表一旦有千万行每次聚合都重数据库CPU直接被拉满。我更推荐的做法是维护一张按天粒度的汇总表比如daily_order_summary里面存date, status, order_count, total_amount。订单产生时由业务层同步更新或者用一个定时任务每小时从订单表聚合后写入。这样前台展示统计时只需要查这张很小的汇总表SELECT SUM(order_count), SUM(total_amount) FROM daily_order_summary WHERE date BETWEEN 2024-01-01 AND 2024-01-31;这个方案的核心是把高成本的实时聚合换算成了低频的离线或准实时聚合。它牺牲了一点实时性但换来的性能收益是数量级的。如果对实时性要求不苛刻强烈推荐。6.2 用游标分批处理替代一次性大更新另一个我常遇到的是“大批量更新”类SQL比如要给几千万元素打标签、把老数据统一洗成新格式。有些同学会直接一条UPDATE把所有记录一次更新掉结果就是锁表、超时、主从延迟甚至直接数据库崩溃。这类问题不属于经典的“SELECT慢”但确实是最让人头疼的“长SQL”之一。正确姿势是用游标或代码循环分批处理每次只更新几千条然后COMMIT一次-- 伪代码示意 DECLARE done INT DEFAULT FALSE; DECLARE cur CURSOR FOR SELECT id FROM orders WHERE status 0 LIMIT 1000; -- 循环里逐批更新并提交时间窗口拉长了但每批的锁范围小、事务提交及时系统整体稳定性反而好很多。如果业务允许还可以在低峰期跑进一步减少影响面。这个思路对“删除海量数据”同样适用——一次性DELETE FROM orders WHERE created_at 2020-01-01很容易把数据库打崩分批小步删除则安全得多。6.3 在大数据量场景下合理利用缓存最后说一个看起来不太像SQL优化的优化**把高频且不常变的数据放到缓存里。**像热门分类列表、首页聚合数据这类内容天天被请求几千次后端却每回都往数据库跑。把这类查询结果放一份到缓存里给个5分钟甚至1小时的过期时间数据库的压力瞬间就下来了。统计类需求同样适用。如果某张报表数据一天只变一次那就凌晨算好放进缓存白天所有查询都读缓存。这比高并发下来回优化一条SQL要有用得多。毕竟数据库的最佳状态就是少被无效请求轰击。提示缓存不是用来给“技术难以优化的慢SQL”兜底的。如果一条SQL本身就该优化就算加了缓存一旦缓存失效瞬间回源慢查询照样会压垮数据库。先优化SQL本身再用缓存挡流量顺序别搞反。7. 我用过的排查工具与传统手段对比前几节偏重方法论和写法。这一节聊聊真正干活时用的工具以及不同工具之间的取舍。很多新手只看“推荐工具”不看“适用场景”结果装了高大上的监控却连慢日志都没打开过这就本末倒置了。7.1 从慢查询日志到可观测平台工具的“进化链”我一般这么区分工具工具类型代表适用场景优点不足数据库自带慢日志MySQL slow query log单机或小集群零依赖、定位准数据量大时日志解析麻烦性能分析工具EXPLAIN、PROFILE单条SQL深度分析能精确看到执行计划与耗时需要人工逐条分析运维工具各类数据库管理平台慢SQL模块中大型系统日常巡检自动收集、分类、告警部署和维护成本较高全链路监控应用性能监控系统排查前端到数据库全链路问题能看到真实调用链往往价格不低、配置复杂如果你刚开始我建议先不要急着上重型监控。把数据库自带的慢日志和EXPLAIN用熟等于有了最基本的“火眼金睛”。等系统到了一定的规模再引入可视化、自动告警类的平台让慢SQL自动浮出来省去人工翻日志的痛苦。7.2 一套可复用的“SQL健康检查五步法”说到传统手段我自己不管优化什么SQL都会走一套固定的检查单。你可以把它当成一个速查流程抓通过慢日志或监控平台抓出最耗时的几条SQL。看对这些SQL跑EXPLAIN记录type、key、rows、Extra。查检查表的索引设计看是否存在冗余、缺失、失效。改按“拆、减、换、合”的思路改写SQL拆分巨型查询、减少不必要的列/行、换成高效写法、合理合并条件。验改完后用执行计划和真实耗时对比确认有收益再观察线上情况。这套流程最大的价值是让优化变成有据可依的“工程动作”而不是兴致上来的“神秘操作”。我见过不少人上来就改改完就说“好像快了”没有对比、没有数据这种优化无法沉淀为团队经验也很难持续复制。7.3 一些要命的坑为什么你的优化越改越慢工具用得不对比不用还可怕。我列几个常见的“反向优化”**为了优化加索引结果加了一堆冗余索引。**每个索引除了占磁盘还会拖慢INSERT、UPDATE、DELETE。索引不是越多越好而是越精准越好。**给所有查询都套上覆盖索引。**覆盖索引往往字段多、列宽一旦索引体量大于缓存的可用内存命中率反而下降。**把OR无脑改成UNION ALL。**如果两侧条件重叠UNION ALL会返回重复行业务结果可能出错。而且两个小结果集合并的消耗可能比直接全表扫描还大。要走执行计划看数据分布再定。**只改SQL不验证数据一致性。**比如分页优化用了id 上页末位但页之间有新增或删除数据时结果会不准。优化性能的前提是保证业务逻辑正确这条红线永远不能碰。这些坑背后都指向同一个问题**不要孤立地理解某个优化技巧要把它放到具体的数据分布、存储引擎、并发模型里去判断。**同样一条SQL在不同数据量、不同索引状态下表现可能完全相反这是SQL优化最考验经验的地方。8. 效果验证与持续观察怎么确认优化真的有效好SQL改完了工具也用过了。那到底算不算优化成功不能凭感觉必须有验证手段和持续观察的习惯。8.1 对比优化前后的关键指标我建议至少对比以下几个指标指标优化前优化后判断标准执行耗时例如 820ms例如 35ms下降幅度明显扫描行数如 120万行如 20行越少越好排序/临时表有Using filesort消失尽量避免命中索引否是执行计划key字段出现对并发的影响拖慢整体恢复平稳数据库负载下降实际操作时我会先选一条测试SQL在相同数据环境下分别跑优化前后的版本多跑几次取平均值避免冷缓存和前一次执行带来的误差。然后到线上观察一个业务周期看慢查询日志的数量有没有明显下降数据库CPU、磁盘IO占用是否恢复平稳。8.2 监控慢SQL数量和数据库负载的“心肺指标”优化不是一次性动作而是个持续过程。我见过太多的案例SQL优化完特别快但两周后又变慢了原因可能是数据量涨了、新需求加了过滤条件、或者索引被别的事务影响了。所以我会在线上保留慢查询日志的监控和告警一旦出现超阈值SQL就立即收到通知再去定位是“新SQL”还是“旧SQL退化”。数据库的几项核心指标我也会经常看CPU使用率、内存命中率、磁盘IO、活跃连接数。记住**SQL慢不一定只是SQL本身的错也可能是数据库资源本身已经接近瓶颈。**比如IO被打满时再好的SQL也会跟着慢这时候要解决的可能是硬件规格、存储方案而不是继续改SQL。8.3 复盘把每次优化沉淀成团队经验最后聊聊复盘。我自己的习惯是每次优化完结后都用一段简短文字记录问题的SQL是什么、慢在哪里、为什么慢、怎么改的、优化效果如何、有没有坑。不需要写得多高大上但一定要真实。比如记录示例某用户列表页查询超时。原SQL用OR连接用户ID与状态字段执行计划typeALL扫描约200万行。改写为UNION ALL并把(user_id, status)组合索引改为(status, user_id)后耗时从1.2s降到60ms。注意点不要无脑用UNION ALL本次是因为OR两侧结果集不重叠。这类沉淀积攒下来就是很有价值的团队知识库。下次再遇到类似SQL直接翻记录、套经验比从零开始排查快得太多。这也是我坚持写这类文章的原因——**慢SQL优化没有什么绝世秘籍无非是方法、工具、经验三样东西的长期积累。**只要你能把这三样学到手再难的SQL也总有解。我最后再顺便说个小事如果你刚开始接触SQL优化别急着去背那些天花乱坠的“百条优化准则”。先把一条SQL的执行计划看懂把最常用的几类索引失效场景记住再跟着这个流程改上三五条真实的慢SQL进步会比看十篇文章都大。纸上得来终觉浅SQL优化这事真的是要在真实数据里磨出来的。