
开头慢 SQL 这事儿几乎每个做后端开发的人都会碰上。不管是刚工作一两年的新人还是带团队的老手只要系统流量一上来数据库迟早会给你点颜色看看——接口突然从 50ms 变成 5s监控告警刷屏老板在旁边盯着你看那一刻的心情懂的都懂。我这些年排查过的慢 SQL 没有一百条也有八十条各种奇葩场景都见过有的是一条 SQL 把数据库 CPU 打满有的是索引没生效导致全表扫描有的是数据量才几十万但查询慢得离谱还有的是 SQL 本身写得就有问题改写之后性能直接翻几十倍。这篇文章我挑了 10 个印象最深的案例覆盖了索引失效、SQL 改写、表结构设计、优化器误判、分页深翻页、隐式转换、函数导致索引失效、范围查询、多表关联、子查询这 10 类高频问题。每个案例我都会把当时的 SQL、表结构、问题现象、排查过程和最终方案完整写出来方便你对号入座。不管你是后端开发、DBA还是刚入门想系统学习 SQL 优化的同学这篇文章都能给你一些能直接落地的思路。我的习惯是先看执行计划再定位瓶颈最后再做针对性优化而不是一上来就瞎加索引。1. 索引失效的典型案例1.1 隐式类型转换导致索引失效第一个案例来自一个订单查询接口。业务反馈说订单列表页偶尔会卡查了一下慢查询日志发现有一条 SQL 平均执行时间在 2 秒以上SELECT * FROM t_order WHERE order_no SO20240115001 LIMIT 10;这条 SQL 看起来非常简单order_no 字段上也建了唯一索引按道理不应该慢。但我看了执行计划之后发现type 是 ALL也就是说走了全表扫描索引完全没生效。问题出在哪里后面查了表结构才发现order_no 字段的类型是 varchar(32)但当时的业务代码里传的参数是通过框架自动映射的某些接口传过来的是数值类型。MySQL 在遇到字符串字段和数值比较时会把字符串隐式转换为数值然后在字段上做转换导致索引失效。注意隐式类型转换是索引失效里最常见、也最坑的一种。字段是 varchar传入参数是数字字段是 datetime传入参数是字符串。只要发生了隐式转换MySQL 大概率不会走索引。解决办法就是保证参数类型和字段类型一致。如果确实无法避免可以在 SQL 里显式转换让转换发生在参数侧而不是字段侧SELECT * FROM t_order WHERE order_no CAST(SO20240115001 AS CHAR) LIMIT 10;当然最根本的做法还是在代码层面统一参数类型。比如在 Java 里不要用 Object 类型接收参数接口定义是什么类型就是什么类型避免框架帮你做隐式转换。1.2 函数作用于字段导致索引失效第二个案例是一个用户维度的统计查询。业务需要统计某个用户在最近 30 天内的下单金额SELECT user_id, SUM(amount) FROM t_order WHERE DATE(create_time) DATE_SUB(CURDATE(), INTERVAL 30 DAY) GROUP BY user_id;t_order 表的数据量大概在 800 万行左右create_time 字段上有普通索引。但这条 SQL 执行了将近 4 秒原因很直接DATE(create_time) 这个函数作用在了索引字段上导致优化器无法使用 create_time 上的索引只能全表扫描。解决办法很简单把函数从字段上移到参数侧。DATE_SUB(CURDATE(), INTERVAL 30 DAY) 的结果可以先算出来然后用 create_time 直接跟一个具体的日期去比较这样 create_time 上的索引就能正常走SELECT user_id, SUM(amount) FROM t_order WHERE create_time DATE_SUB(CURDATE(), INTERVAL 30 DAY) GROUP BY user_id;改完之后执行时间从 4 秒降到了 300 毫秒左右效果立竿见影。这类问题的通用规则就是永远不要在索引字段上做任何运算包括函数、算术运算和类型转换。1.3 前缀模糊匹配导致索引失效第三个案例是一个搜索接口慢的问题。业务是搜索商品名称包含关键词的商品SQL 写法如下SELECT * FROM t_product WHERE product_name LIKE %无线耳机%;product_name 字段上有索引但因为 LIKE 的模糊匹配是以 % 开头的B Tree 索引无法利用前缀匹配的特性优化器直接放弃索引选择全表扫描。这张表有 200 万行数据查询结果集可能有几千行每次都要全表扫一遍耗时在 1.5 秒左右。这种场景没有特别完美的解法。如果你的业务真的需要搜索包含某个关键词的商品那更适合用全文索引或者搜索引擎来做。如果数据量不大或者对实时性要求不高也可以考虑用覆盖索引加全表扫描的折中方案——但本质上没法把前缀模糊查询优化成索引精确匹配。我当时的做法是先跟业务确认搜索场景其实只关心前 10 个匹配结果于是用了一条辅助 SQL 先做精准前缀匹配SELECT * FROM t_product WHERE product_name LIKE 无线耳机% LIMIT 10;这样能走索引响应时间降到了几十毫秒。然后再在代码里做一次数据补充把前缀匹配不到的结果用搜索引擎的召回结果填充。这个方案不是所有场景都适用但思路可以参考——优先满足核心链路把非核心链路放在后面补偿。2. 分页查询深翻页的优化2.1 深翻页为什么慢第四个案例非常典型是后台管理系统的列表页。运营同学反馈说翻到第 100 页之后页面加载特别慢甚至超时。查询 SQL 长这样SELECT id, order_no, user_id, amount FROM t_order ORDER BY create_time DESC LIMIT 100000, 20;这条 SQL 慢的原因一句话就能说清楚MySQL 需要先扫描到第 100020 行然后丢弃前面的 100000 行只返回最后 20 行。这就像翻一本 10 万页的书每翻一页都要从第 1 页开始翻起翻到第 99980 页的时候前面那一万页的阅读成本全都白花了。在这个案例里即使 create_time 上有索引MySQL 也要遍历 10 万条索引记录再回表拿到完整行数据最终返回最后 20 条。整体耗时在 2.8 秒左右数据量再大的话会更夸张。2.2 延迟关联优化法我用的方案是延迟关联。核心思路是先用覆盖索引快速定位到需要的 20 个主键 ID再通过主键回表去获取完整记录。这样即使偏移量很大索引扫描阶段也只扫描了 100020 个主键而不是 100020 个完整行回表次数从 10 万次降到了 20 次。SELECT t.id, t.order_no, t.user_id, t.amount FROM t_order t INNER JOIN ( SELECT id FROM t_order ORDER BY create_time DESC LIMIT 100000, 20 ) tmp ON t.id tmp.id;改完之后耗时降到了 400 毫秒左右。这个方案在很多场景下都适用而且改动量不大只是把原来的单表查询改成了子查询关联。注意如果业务允许还有一种更彻底的方案叫基于游标的翻页。比如上一页最后一条记录是 create_time 2024-01-15 10:00:00那下一页就查询 WHERE create_time 2024-01-15 10:00:00 ORDER BY create_time DESC LIMIT 20。这种方案不管翻多少页扫描的数据量都是固定的性能最稳定但需要业务端配合改造。3. 多表关联查询的优化3.1 关联字段没有索引第五个案例是报表系统的多表关联查询。业务需要查订单表和用户表的关联数据SQL 如下SELECT o.order_no, u.user_name, u.mobile, o.amount FROM t_order o INNER JOIN t_user u ON o.user_id u.id WHERE o.create_time 2024-01-01 AND o.create_time 2024-02-01;t_order 表在 create_time 上有索引所以订单表这一侧只扫了 1 月份的数据大概 50 万行。但 t_user 表的 user_id 上没有索引这就导致每一行订单数据都要去 t_user 表做一次全表扫描匹配50 万次全表扫描性能直接崩了。我的 DBA 同事告诉我这条 SQL 跑了 12 秒。解决办法很直接在 t_user 表的 id 字段上建主键索引。等等id 本来就是主键啊我看了执行计划才发现问题不在 id而在于驱动表选错了。优化器以 t_order 作为驱动表对每一行订单记录去 t_user 表查找匹配的用户如果 t_user 表是主键查找单次查找是很快的所以理论上不至于慢到这个程度。实际排查后我发现慢的根本原因不是关联字段没有索引而是 t_order 表的 create_time 索引选择性太差。1 月份的 50 万行数据占了整张表的 60% 以上优化器觉得走索引还不如全表扫描划算就选择了全表扫描订单表再做嵌套循环关联。全表扫描加上 50 万次关联匹配就是这么慢的。3.2 改写驱动表与关联条件我的优化方案是两步走。第一步优化 SQL 写法明确指定驱动表和关联顺序SELECT o.order_no, u.user_name, u.mobile, o.amount FROM t_user u INNER JOIN t_order o ON o.user_id u.id WHERE o.create_time 2024-01-01 AND o.create_time 2024-02-01把 t_user 作为驱动表先查出所有用户再通过 user_id 去订单表里匹配。t_user 表大概有 100 万行但通过嵌套循环连接每次匹配 t_order 都能用上 user_id 上的索引单次查找成本极低。第二步在 t_order 表上建一个联合索引user_id, create_time这样关联条件和过滤条件都能走索引。ALTER TABLE t_order ADD INDEX idx_user_create (user_id, create_time);改完之后 SQL 耗时从 12 秒降到了 800 毫秒。这个案例给我的启发是多表关联查询的优化关键在于搞清楚驱动表是谁、被驱动表的关联列有没有索引。很多情况下不是你 SQL 写错了而是你没有给优化器提供足够的执行路径选择。4. 子查询与 IN 的优化4.1 IN 子查询过慢第六个案例是一个库存扣减相关的查询。场景是查一批商品的库存信息商品 ID 来自一个子查询SELECT * FROM t_inventory WHERE product_id IN ( SELECT product_id FROM t_product WHERE category_id 1001 );这个 SQL 慢得离谱执行了 8 秒。我看了执行计划发现MySQL 对 IN 子查询的处理方式是先执行子查询结果集有 10 万条然后把这 10 万个外部参数拼装成一个很大的 IN 列表再对 t_inventory 做全表扫描匹配。相当于遍历 10 万个值的列表去和 t_inventory 的 500 万行数据做全表比较不慢才怪。4.2 改为 JOIN 关联我的优化方案是改写成 JOINSELECT i.* FROM t_inventory i INNER JOIN t_product p ON i.product_id p.product_id WHERE p.category_id 1001;改完之后执行计划变成了先过滤 t_product 表走 category_id 上的索引只查出 10 万条然后以这 10 万条作为驱动表通过 product_id 关联到 t_inventory 表。t_inventory 表上的 product_id 有索引每次匹配都是索引查找总体耗时降到了 300 毫秒。这个案例的核心教训是IN 子查询并不一定慢但它的性能取决于子查询结果集的大小和外部表的索引情况。如果你碰到 IN 子查询特别慢第一反应应该是看执行计划——子查询是不是被物化成临时表了外部表索引有没有生效如果都正常还是慢直接改成 JOIN 试试通常会有惊喜。4.3 关联字段字符集不一致导致索引失效在这个案例中还顺手解决了一个隐藏问题。t_product 表的 product_id 是 varchar(20)t_inventory 表的 product_id 也是 varchar(20)但两张表的字符集不一样一个是 utf8mb4一个是 utf8。MySQL 在做 JOIN 时会自动把 utf8 的字段转换为 utf8mb4 来比较转换发生在了 t_inventory 表的 product_id 字段上导致索引失效。解决办法是统一两张表的字符集ALTER TABLE t_inventory MODIFY COLUMN product_id varchar(20) CHARACTER SET utf8mb4;这个坑非常隐蔽执行计划里可能显示关联类型是 ref但实际扫描行数却异常高。如果你在建表时没有统一字符集规范多表关联时很容易踩这个坑。我一般建议所有表默认都用 utf8mb4可以省去大量这类问题。5. 范围查询导致的索引失效5.1 范围条件放在联合索引中间列第七个案例来自一个订单查询接口。业务需要按时间范围和状态查询订单SQL 如下SELECT * FROM t_order WHERE status 1 AND create_time BETWEEN 2024-01-01 AND 2024-01-31 ORDER BY id DESC LIMIT 20;我在 t_order 表上建了联合索引 idx_status_timestatus, create_time。理论上这条 SQL 应该能走这个索引但实际执行计划显示它只用了 status 这一个条件create_time 的范围过滤是在回表之后做的扫描行数比预期的多很多。原因在于联合索引的匹配原则是从左到右依次匹配一旦遇到范围查询BETWEEN、、后面的列就无法继续使用索引。在这个索引里status 是等值匹配create_time 是范围匹配用的没有问题。但仔细看会发现SQL 里还有一个 ORDER BY id DESC。当 create_time 被用于范围过滤后id 的排序就无法使用索引MySQL 只能在回表后做一次文件排序filesort这会导致额外的性能开销。5.2 调整联合索引列顺序我的优化方案是换一个联合索引让排序字段也能走索引ALTER TABLE t_order ADD INDEX idx_status_id_time (status, id, create_time);这样优化器通过 idx_status_id_time 先按 status 等值过滤然后按 id 进行索引排序再在索引内部过滤 create_time 的范围条件。排序问题直接解决了不需要文件排序执行时间从 500 毫秒降到了 80 毫秒。这里的核心思想是联合索引设计时不只要考虑 WHERE 条件还要考虑 ORDER BY 和 GROUP BY。最常见的设计原则是等值条件放最前面其次是排序字段最后才放范围条件。这样能让索引同时服务过滤和排序效率最高。注意不要建太多联合索引。每建一个索引都会拖慢写入速度占用更多磁盘空间。理想情况下一个表的核心索引控制在 3-5 个每个索引都要能覆盖到频繁执行的查询模式而不是为了某一条慢 SQL 无脑加索引。6. 并行 SQL 执行的优化思路6.1 大查询拆分并行的适用场景第八个案例比较特殊来自一个离线数据统计任务。业务需要统计一年内所有用户的消费数据SQL 如下SELECT user_id, SUM(amount) AS total_amount FROM t_order WHERE pay_time 2024-01-01 AND pay_time 2025-01-01 GROUP BY user_id ORDER BY total_amount DESC LIMIT 100;这个 SQL 的问题很典型全表扫描一年的订单数据大概 5000 万行然后做 GROUP BY 聚合再排序取前 100。即使 pay_time 上有索引优化器也大概率会选择全表扫描因为要扫的数据量太大。这条 SQL 跑一次要 45 秒对于离线任务勉强能接受但我们希望能更快一些。对于这种场景单纯靠索引优化已经不够了。我的方案是把大查询拆成多个小查询再用并行方式执行。具体思路是按月份把一年的数据拆成 12 个区间每个区间单独一个查询同时并行执行。每个查询只处理一个月的数据扫描量只有原来的 1/12内存排序的压力也小得多。-- 1月 SELECT user_id, SUM(amount) AS total_amount FROM t_order WHERE pay_time 2024-01-01 AND pay_time 2024-02-01 GROUP BY user_id; -- 2月 SELECT user_id, SUM(amount) AS total_amount FROM t_order WHERE pay_time 2024-02-01 AND pay_time 2024-03-01 GROUP BY user_id; -- ... 以此类推6.2 多线程合并结果如果应用层用 Java 的多线程来执行这 12 个查询再对结果做合并和再排序整个任务的耗时可以从 45 秒降到 5 秒左右。但要注意并行不是银弹盲目并行反而可能把数据库打爆。提示并行查询只适合大数据量、无事务依赖的只读场景。如果查询之间有依赖关系或者你用的是小规格数据库实例强行并行可能会导致连接数耗尽、锁竞争加剧反而更慢。经验值上并行度控制在 4-8 个比较稳妥不要超过数据库 max_connections 的 20%。另外如果你们的数据库版本支持 MySQL 8.0 的并行查询特性8.0.14 之后或者用了暖气的分析型引擎也可以直接通过设置 parallel_degree 参数来触发优化器并行执行。但这类特性在不同版本上表现差异很大我建议还是先在测试环境验证效果再上生产。7. 表结构设计引发的慢查询7.1 SELECT 查询列过多第九个案例来自一个用户中心的服务。查询用户基本信息和扩展字段的接口慢SQL 如下SELECT * FROM t_user WHERE user_id 123456;t_user 表有 80 多个字段包括用户头像地址、个性化配置、还包含几个大文本字段TEXT 类型。这张表有 800 万行数据单行数据平均长度在 3KB 左右。虽然 user_id 上有唯一索引这个查询走的是索引等值查询按道理不该慢但实际耗时仍然有 1.2 秒。问题出在回表上。InnoDB 的聚簇索引叶子节点存的是整行数据如果一行数据特别宽3KB回表时就要从磁盘读取更多的数据页。MySQL 的最小 IO 单位是 16KB 的页如果每页只存了 5-6 行数据为了拿到一行数据可能就要多读好几个页。这个查询看起来走索引很快但真正的瓶颈在随机 IO。7.2 把宽表拆成窄表我的方案不是去改 SQL而是去改表结构。把 t_user 表中的大文本字段和不常用的扩展字段拆到独立的用户扩展表里t_user 只保留核心高频字段单行平均长度从 3KB 降到 500 字节左右。这样 16KB 的数据页可以容纳更多行回表时随机 IO 次数大幅下降查询耗时降到了 30 毫秒。这个案例给我的启发是SQL 优化不是只盯着 SQL 本身表结构设计对查询性能的影响往往更大。如果你的表里有大文本字段、超多字段但大部分都用不上这些都会拖慢主表的查询性能。宽表和窄表没有绝对的好坏关键是区分高频和低频字段把低频的大字段隔离出去。注意拆表之后读写逻辑都要改。如果业务对实时性要求高可以考虑引入缓存层来分摊读压力。拆表前一定要评估业务所有查询场景避免把高频字段误拆到扩展表结果每次查询都要 join 两张表反而更慢。8. 优化器误判与统计信息过期8.1 统计信息不准确引发全表扫描第十个案例也是我印象最深的一个。一个订单查询接口在某次大促后突然变慢SQL 非常简单SELECT * FROM t_order WHERE status 2 AND create_time 2024-06-01 ORDER BY create_time DESC LIMIT 20;status 2 表示已支付订单这个状态在订单表里占 90% 以上。但大促之后表里涌入了大量新订单status 2 的占比其实更高了。MySQL 优化器依赖统计信息来决定执行计划但统计信息没有及时更新优化器低估了 status 2 的过滤效果选择了全表扫描执行了 3 秒。一开始我怀疑是索引失效各种排查都没有发现问题。最后看了执行计划的预估行数和实际行数发现差距巨大——预估返回 100 行实际返回 80 万行。这时候我会优先确认是不是统计信息过期了。8.2 使用 ANALYZE TABLE 触发统计信息更新解决办法很简单执行 ANALYZE TABLE 让优化器重新收集统计信息ANALYZE TABLE t_order;执行完之后再跑原来的 SQL发现耗时降到了 50 毫秒。这个案例告诉我们如果你的 SQL 本身没什么毛病索引也都建对了但执行计划就是很离谱先看看统计信息是否过期。MySQL 的自动统计信息收集是有一定频率的大表在高并发写入场景下容易出现统计信息落后于实际数据分布的情况。日常运维中我建议在每次大批量数据导入、或者大促活动结束后手动执行一次 ANALYZE TABLE。这个操作的代价很小但可能避免很多诡异的慢查询问题。8.3 强制使用索引的兜底方案如果统计信息更新之后优化器还是选择了不理想的执行计划最后的兜底方案是用 FORCE INDEX 强制指定索引SELECT * FROM t_order FORCE INDEX (idx_status_time) WHERE status 2 AND create_time 2024-06-01 ORDER BY create_time DESC LIMIT 20;但我不推荐一上来就用 FORCE INDEX因为这会把优化器的自主权拿走。一旦未来数据分布变化这个强制索引可能反而不是最优的。正确的姿势是先分析为什么优化器选错能通过更新统计信息、重建索引解决的优先选这些方式。只有确认优化器确实反复误判且无法通过常规方式解决时才考虑 FORCE INDEX并要在代码里标注清楚原因方便后续维护。9. 常见慢 SQL 问题速查表我把这些年最常见的慢 SQL 问题和对应解法整理成了一张表方便大家排查时对照。这张表不是教科书而是我从实际案例里提炼出来的经验汇总按出现频率排序。问题类型典型特征排查方向解决方案隐式类型转换字段类型和参数类型不一致查看执行计划 typeALL统一参数类型避免字段侧转换函数作用于索引字段WHERE 条件里有 DATE()、YEAR() 等查看索引是否被使用改写 SQL函数移到参数侧前缀模糊匹配LIKE %keyword%查看索引是否被使用改前缀匹配或引入全文索引深分页LIMIT 100000, 20扫描行数远大于返回行数延迟关联、基于游标翻页多表关联无索引被驱动表关联列无索引查看执行计划 typeALL 或 ref补充索引或改写驱动表IN 子查询过大子查询结果集很大查看是否物化临时表改 JOIN 或分批 IN字符集不一致关联字段字符集不同查看 collation 不匹配统一字符集为 utf8mb4范围查询导致排序失效联合索引同时有范围和排序字段查看 Extra 有 filesort调整联合索引列顺序统计信息过期预估行数和实际行数差距大查看执行计划 rows 字段ANALYZE TABLE 手动更新SELECT * 且行宽过大表字段多、大文本字段多查看单行平均长度拆宽表、只查询必要字段数据倾斜某个值占比特别高查看过滤条件区分度改写 SQL或拆分为多个查询这张表适合在你接到一个慢 SQL 工单时先对照特征快速定位问题方向。但记住任何排查都从执行计划开始纸上谈兵只会浪费时间。10. 快慢 SQL 优化的完整排查流程优化如果你拿到一条慢 SQL不知道从哪儿下手可以按下面这个顺序来。这套流程是我这几年踩过无数坑之后沉淀下来的不说百分之百解决所有问题但至少能保证你找到 90% 的问题根源。第一步从慢查询日志里把 SQL 捞出来带上执行计划一起看。MySQL 的 EXPLAIN 可以告诉你访问类型、扫描行数、索引使用情况、是否有 filesort 或临时表。执行计划是你最重要的诊断工具比任何人的经验都可靠。第二步分析 SQL 的访问模式。是单表查询还是多表关联是等值查询还是范围查询有没有 ORDER BY、GROUP BY、LIMIT这些操作是否都能利用索引把 SQL 拆成最小的条件单元逐一判断是否走索引。第三步查看表结构和统计信息。字段类型是否合理索引设计是否符合查询模式统计信息是否过期有时候问题不在 SQL 本身而在表结构设计和索引规划上。第四步根据前两步的分析结果选择优化策略如果索引没生效先排查隐式转换、函数操作、字符集不一致这些坑。如果 SQL 本身写法有问题考虑改写 SQL比如 IN 改 JOIN、深翻页改延迟关联。如果索引已经到位但还慢考虑是不是表结构设计不合理比如宽表、大文本字段。如果数据量太大任何优化都没法根治就需要考虑数据归档、分库分表或者引入搜索引擎了。第五步验证优化效果。改完之后用 EXPLAIN 和实际执行时间双重验证然后观察一段时间确认没有引入新的问题。如果是在生产环境操作建议先在压测环境或低峰期进行避免影响线上业务。这条流程看似简单但要真正做到位需要你对执行计划的各种字段了如指掌对不同优化手段的适用范围有清晰的认知。多积累案例多分析执行计划慢慢地你看到一条慢 SQL 就能本能地判断出问题在哪儿。11. 写在最后的几个实操心得最后分享几个我在实际排查过程中积累的小技巧不算系统性的方法论就是一些零碎但很实用的经验。第一个加索引之前先确认这个 SQL 是否需要索引。如果查询本身扫描的数据量占全表的比例超过 30%优化器大概率不会走索引就算你强制索引性能也未必比全表扫描快。这种场景更适合从业务角度改造比如限制查询范围、拆分查询维度而不是硬怼索引。第二个能用覆盖索引的就别回表。我见过很多慢 SQL明明 SELECT 只需要两三个字段却用 SELECT * 把整行都捞出来回表开销白白浪费。如果核心查询的字段能全部覆盖在索引里MySQL 可以直接从索引返回结果不回表性能提升非常明显。第三个EXPLAIN 的 Extra 字段里出现 Using filesort 或 Using temporary一定要优先处理。文件排序和临时表通常意味着查询做了额外的工作这两个词一出现基本就是性能瓶颈所在。第四个慢 SQL 优化要按 80/20 原则来优先处理调用频率最高的那批 SQL而不是单次最慢的那几条。一条每天跑一次、耗时 10 秒的离线任务和一条每秒调用 100 次、耗时 200 毫秒的接口查询后者的优化价值大得多。第五个如果你在做并行 SQL 优化一定要做好结果合并的幂等性设计。并行查询的每个分片都要独立无依赖合并且结果要能通过简单的方式去重或归并。不要搞到一半发现有两个分片查了相同的数据结果还得重跑一遍时间全浪费在这上面。第六个定期做慢查询巡检。不要等业务投诉了才去排查我习惯每周花点时间看一次慢查询日志把执行时间超过阈值的 SQL 拉出来分析一遍。很多问题在变成故障之前其实早就已经在慢查询日志里躺了很久了。提前发现、提前优化比事后救火要轻松得多。这篇文章里说的 10 个案例基本都是我在真实项目中碰到并解决的问题。SQL 优化这东西理论知识看再多不如自己亲手排查一次印象深。希望这些案例能帮你少走一些弯路下次再碰到慢 SQL 的时候能先深呼吸打开执行计划然后淡定地找到问题所在。