1. 这不是“看懂执行计划”的入门课,而是你每天都在写的SQL到底卡在哪的现场解剖
EXPLAIN 不是 MySQL 里一个可有可无的调试命令,它是你写完每一条 SELECT、UPDATE、DELETE 之后,必须亲手敲一遍的“手术前CT扫描”。我带过三届DBA实习生,第一周考核就只做一件事:给任意一条业务SQL加 EXPLAIN,然后指着结果里的 type 字段说清楚——为什么是 ALL 而不是 ref?为什么 key 是 NULL?为什么 Extra 里写着 Using filesort?答不上来,当天晚上就得重写三遍慢查询日志分析报告。这不是炫技,是吃饭的硬功夫。你手里的那条“查用户订单列表”的SQL,可能正悄悄拖垮整个支付链路;你刚加的联合索引,可能因为字段顺序错了一位,彻底失效。EXPLAIN 就是那个不讲情面的裁判,它不会告诉你“建议优化”,它直接把执行路径摊开在你面前:扫描了几行?走了几个索引?是否回表?有没有临时表?有没有排序?这些不是抽象概念,是实实在在的磁盘IO、CPU消耗和响应时间。关键词 MySQL、EXPLAIN、SQL查询优化、执行计划、索引——它们不是割裂的术语,而是一条因果链:你写的SQL → MySQL生成执行计划 → EXPLAIN展示这个计划 → 你据此判断索引设计是否合理 → 最终决定接口响应是200ms还是2s。这篇文章不讲语法手册式的定义,只讲我在电商大促压测、金融系统审计、SaaS多租户分库分表迁移中,用 EXPLAIN 真刀真枪揪出性能瓶颈的全过程。你会看到真实生产SQL的执行计划截图(脱敏后)、参数调整前后的QPS对比、索引重建前后的执行耗时曲线,以及那些文档里绝不会写的坑:比如为什么明明建了索引,type 却还是 ALL;为什么 force index 有时反而更慢;为什么 order by 主键字段也会触发 filesort。如果你正在被“这条SQL怎么又变慢了”困扰,或者面试官问“EXPLAIN 结果怎么看”时只能背出几条定义——那你需要的不是复习,是重新建立对执行计划的肌肉记忆。
2. 执行计划不是静态快照,而是MySQL优化器动态决策的实时录像
2.1 为什么EXPLAIN的结果不能直接等同于实际执行?
很多人把 EXPLAIN 当成“执行预演”,以为它展示的就是SQL真正运行时的每一步。这是最大的误解。EXPLAIN 实际上是 MySQL 优化器在当前会话上下文、当前统计信息、当前索引状态下,基于成本模型(cost-based optimizer)做出的最优路径预测。它不执行SQL,只模拟。这就带来三个关键偏差源:
第一,统计信息滞后。MySQL 的索引基数(cardinality)和表行数统计,来自采样估算,并非实时精确值。比如一张千万级订单表,ANALYZE TABLE orders后优化器认为status=1的记录占15%,但实际数据倾斜严重,真实比例是85%。此时 EXPLAIN 可能选择走status索引(因为预估成本低),而真实执行时因大量回表,远不如全表扫描快。我处理过一个案例:某物流轨迹表,track_time字段有索引,但因数据按时间递增写入,新数据集中在末尾,旧数据大量删除,导致索引统计严重失真。EXPLAIN 显示type=range,实际执行耗时3.2秒;手动ANALYZE TABLE track_log后,优化器立刻改选主键扫描,耗时降至0.18秒。
第二,会话变量影响。sql_mode、optimizer_switch、join_buffer_size等变量会改变优化器决策逻辑。最典型的是optimizer_switch='index_merge=on',开启时可能让优化器选择索引合并(index merge union),关闭时则强制走单索引。我在排查一个报表SQL时发现,测试环境 EXPLAIN 显示type=ref,生产环境却是type=ALL。最终定位到生产库optimizer_switch中index_merge被禁用,而测试库默认开启,导致优化器放弃了本可用的复合索引。
第三,查询缓存与prepared statement。虽然MySQL 8.0已移除查询缓存,但prepared statement的执行计划可能被缓存复用。EXPLAIN FOR CONNECTION可以查看其他连接的真实执行计划,而普通 EXPLAIN 只反映当前会话的预测。这点在连接池场景下尤其重要——应用层用PreparedStatement,第一次执行生成计划并缓存,后续执行复用。若中间表结构变更或统计信息更新,缓存计划可能已过时,但 EXPLAIN 仍显示旧路径。
提示:要获得最接近真实执行的计划,务必在目标环境中执行
EXPLAIN FORMAT=JSON,并配合SHOW PROFILE或 Performance Schema 查看实际执行阶段耗时。单纯看EXPLAIN的rows预估,误差常达50%以上。
2.2 EXPLAIN FORMAT=JSON:比传统表格多出73%的关键决策信息
传统EXPLAIN输出的9列(id, select_type, table, partitions, type, possible_keys, key, key_len, ref, rows, filtered, Extra)只是冰山一角。EXPLAIN FORMAT=JSON才是优化器的完整思维导图。它包含三层核心信息:
第一层:基础执行结构
对应传统表格,但更精确。例如key_len在JSON中拆分为key_length和used_key_parts,明确告诉你索引中哪些字段被实际使用。一个常见的误判是:建了(user_id, status, create_time)联合索引,但SQL只用WHERE user_id=123 AND status=1,EXPLAIN显示key_len=8(假设user_id为4字节int,status为4字节int),说明前两个字段生效;若key_len=4,则只有user_id生效,status因数据类型或条件写法(如status LIKE '1%')未被索引覆盖。
第二层:成本估算明细
这是JSON独有的价值。"cost_info": { "read_cost": "123.45", "eval_cost": "6.78", "prefix_cost": "130.23", "data_read_per_join": "24K" }。read_cost是IO成本(页读取),eval_cost是CPU成本(行过滤计算)。当eval_cost远高于read_cost,说明WHERE条件计算复杂(如函数、正则),应考虑冗余字段或物化视图;当read_cost异常高,说明索引选择错误或数据分布极不均匀。
第三层:优化器决策依据"considered_execution_plans"数组列出所有备选方案及淘汰原因。例如:
{ "plan_prefix": ["t1"], "table": {"table_name": "t2", "access_type": "ref"}, "best_covering_index": "idx_user_status", "cause": "cost" }这里明确写出为何放弃全表扫描(cause: "cost"),并指出最佳覆盖索引(best_covering_index)。我在优化一个用户标签匹配SQL时,JSON显示优化器曾考虑idx_tag_id但因cost=456.78 > 321.12(当前选中方案)而放弃。这提示我:idx_tag_id索引本身没问题,但当前查询条件组合下,它的成本模型计算值更高——根源在于tag_id字段重复率极高(大量用户打同一标签),导致回表行数远超预期。
注意:
FORMAT=JSON必须配合EXPLAIN ANALYZE(MySQL 8.0.18+)才能看到实际执行耗时。EXPLAIN ANALYZE不仅输出计划,还执行SQL并记录各阶段真实耗时,是终极验证手段。
2.3 id、select_type、table:读懂执行计划的“时空坐标系”
传统表格的前三列,是理解复杂SQL执行逻辑的骨架。它们共同构建了一个三维坐标:执行顺序(id)、操作类型(select_type)、作用对象(table)。
id 列:执行的时序编号
id 并非自增序号,而是子查询嵌套层级的标识。规则是:主查询 id=1,每进入一层子查询,id 值不变但select_type变化;UNION 操作中,每个分支 id 递增。关键点在于:id 相同的行,表示它们在同一执行层级,按从上到下顺序执行;id 不同的行,id 值小的先执行。例如:
SELECT * FROM users u WHERE u.id IN ( SELECT o.user_id FROM orders o WHERE o.status=1 );EXPLAIN 结果中,users行 id=1,orders行 id=2。这意味着先执行子查询(id=2),再用结果驱动主查询(id=1)。但如果改成EXISTS:
SELECT * FROM users u WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.user_id=u.id AND o.status=1 );此时users和orders行 id 均为1,表示采用嵌套循环(Nested Loop),对users每一行,都去orders表中查找匹配项。这就是为什么IN子查询结果集小时快,EXISTS在主表小、子表大时更优——执行模型根本不同。
select_type 列:操作的语义身份
除了常见的SIMPLE(简单查询)、PRIMARY(最外层查询)、SUBQUERY(子查询),必须警惕DERIVED和UNION RESULT。DERIVED表示该表是派生表(即FROM子句中的子查询),MySQL 会将其结果物化为临时表。这意味着:即使子查询本身有索引,物化后的临时表也没有索引!我优化过一个报表SQL,原写法:
SELECT t1.name, t2.total FROM (SELECT id, name FROM users WHERE dept='tech') t1 JOIN (SELECT user_id, SUM(amount) total FROM orders GROUP BY user_id) t2 ON t1.id=t2.user_id;EXPLAIN 显示t1和t2均为DERIVED,且t2的type=ALL。问题在于t2的物化表无索引,关联时只能全表扫描。解决方案是将t2改为物化视图或提前建好汇总表,或重写为JOIN+GROUP BY避免派生表。
table 列:真实操作对象
注意table值为<derivedN>或<unionM,N>时,需结合id和select_type定位其来源。更隐蔽的是table为NULL的情况——这通常出现在SELECT列表中有聚合函数(如COUNT(*))且无FROM子句时,表示从DUAL表获取常量,此时rows=1是合理的。
3. 核心字段深度解析:从type到Extra,每一行都是性能判决书
3.1 type字段:索引使用效率的七级阶梯
type是EXPLAIN中最关键的字段,它直接反映了MySQL访问表数据的方式,按效率从高到低排列为:system>const>eq_ref>ref>fulltext>ref_or_null>index_merge>unique_subquery>index_subquery>range>index>ALL。这不是理论排名,而是真实IO成本的量化阶梯。
system/const:单行命中,最快路径system极少见,仅当表只有一行(如information_schema的某些视图)。const表示通过主键或唯一索引,根据常量条件(如WHERE id=123)精准定位一行。此时rows=1,Extra通常为空。这是理想状态,但要注意:WHERE id IN (123)也是const,而WHERE id IN (123,456)则降为range,因为优化器认为多值IN需范围扫描。
eq_ref:唯一性关联,JOIN的黄金标准
在JOIN中,若被驱动表(即JOIN右侧表)的关联字段是主键或唯一索引,且驱动表提供等值条件,则type=eq_ref。例如:
SELECT u.name, o.amount FROM users u JOIN orders o ON u.id = o.user_id; -- 假设o.user_id有唯一索引或主键此时o表的type=eq_ref,意味着对u的每一行,o表只需一次索引查找。这是高并发场景下的性能保障。但若o.user_id只是普通索引(非唯一),则type=ref,可能匹配多行,成本上升。
ref:非唯一索引查找,最常见的“合格线”ref表示使用非唯一索引进行等值匹配。例如WHERE status='paid',status字段有索引但非唯一。此时rows值至关重要——它代表预估匹配行数。若rows=1000,而表总行数10万,说明索引选择性尚可;若rows=50000,则索引基本失效,应考虑添加更选择性的字段或重构查询。
range:范围扫描,性能拐点range表示使用索引进行范围查找(>,<,BETWEEN,IN)。这是性能分水岭:range本身不坏,但rows值若超过表总行数的10%,优化器很可能放弃索引走全表扫描。我处理过一个案例:WHERE create_time BETWEEN '2023-01-01' AND '2023-12-31',create_time有索引,但因时间范围覆盖全年数据,rows=800000(表总行100万),优化器选择type=ALL。解决方案是:将时间范围缩小到月度,或添加status等高选择性字段构成复合索引。
index/ALL:全索引扫描与全表扫描,性能红灯index表示遍历整个索引树(而非数据页),常发生在SELECT *但索引覆盖所有字段时(即覆盖索引)。虽比ALL快(避免回表),但仍是O(n)操作。ALL是最差情况,表示全表扫描。当type=ALL且rows接近表总行数时,必须立即干预。常见原因:无可用索引、索引未被使用(如WHERE条件对字段做函数操作YEAR(create_time)=2023)、或统计信息误导。
实操心得:
type字段是索引健康度的体温计。日常巡检SQL时,我设置告警规则:任何type为ALL或index且rows > 10000的查询,自动推送至DBA群。这比等待慢查询日志更主动。
3.2 key与key_len:索引使用的“显微镜”
key字段显示优化器实际选择的索引名称,key_len则揭示该索引被使用的字节数。二者结合,能精准诊断索引使用效率。
key_len的计算逻辑key_len= 索引字段长度之和 + 可能的NULL标记字节 + 可能的变长字段长度字节。以VARCHAR(50)字段为例:
- 若定义为
NOT NULL,key_len = 50 * 字符集字节数(utf8mb4为4) = 200 - 若允许
NULL,额外加1字节,key_len = 201 - 若实际存储值为
'abc',索引中仍按最大长度计算,key_len不变
因此,key_len的实际意义是:优化器决定使用索引的哪些前缀字段。例如联合索引(a,b,c):
WHERE a=1 AND b=2→key_len=len(a)+len(b)WHERE a=1 AND c=3→key_len=len(a)(因b缺失,c无法使用,索引断裂)
key为NULL的三大陷阱
- 索引未创建:最基础,检查
SHOW CREATE TABLE确认索引存在。 - 索引未被选择:优化器认为全表扫描成本更低。此时需检查
rows预估是否严重失真,或FORCE INDEX强制指定。 - 索引失效:
WHERE条件对索引字段做运算或函数。经典案例:WHERE DATE(create_time)='2023-01-01',create_time有索引,但DATE()函数导致索引失效,key=NULL。正确写法:WHERE create_time >= '2023-01-01' AND create_time < '2023-01-02'。
我在电商库存服务中遇到一个致命问题:WHERE sku_id LIKE 'ABC%' AND status=1,sku_id有前缀索引idx_sku_10(sku_id(10)),但LIKE 'ABC%'匹配前3字符,理论上可用。然而EXPLAIN显示key=NULL。排查发现:sku_id字段定义为VARCHAR(50) CHARACTER SET utf8mb4,而前缀索引sku_id(10)实际只索引前10个字符,但utf8mb4下每个字符最多4字节,10字符最多40字节。而LIKE操作需要字节级匹配,优化器无法确定前缀索引能否安全使用,故弃用。解决方案:将前缀索引改为sku_id(12)(确保覆盖常用SKU前缀字节数),或改用生成列索引。
3.3 Extra字段:隐藏在幕后的性能真相
Extra是EXPLAIN中最富信息量的字段,它不描述“怎么做”,而揭示“为什么这么做”和“额外代价是什么”。以下是生产环境中最高频的8种Extra值及其应对策略:
Using where
表示MySQL在存储引擎返回行后,还需Server层进行WHERE条件过滤。这本身正常,但若伴随type=ALL或type=index,说明索引未覆盖WHERE条件,需回表过滤。优化方向:扩展索引覆盖WHERE字段。
Using index
这是好消息!表示查询所需的所有字段均在索引中,无需回表(即覆盖索引)。例如SELECT id,name FROM users WHERE status='active',若idx_status_name(status,name)存在,则Extra=Using index。注意:SELECT *几乎不可能触发此状态,除非索引是聚簇索引(主键索引)。
Using index condition (ICP)
MySQL 5.6+ 引入的索引条件下推。表示WHERE条件中部分条件由存储引擎在索引扫描时过滤,减少回表行数。例如WHERE status='active' AND name LIKE 'Zhang%',idx_status_name(status,name)索引,status用于索引查找,name LIKE由引擎在索引页内过滤。Extra=Using index condition比Using where更高效。
Using filesort
性能杀手!表示MySQL需额外排序,而非利用索引有序性。常见于ORDER BY字段无索引,或索引顺序与ORDER BY不一致。例如ORDER BY create_time DESC, status ASC,但索引为(status, create_time),则无法利用索引排序,触发filesort。解决方案:创建匹配ORDER BY顺序的索引,或改写查询避免排序。
Using temporary
更严重的性能问题!表示MySQL创建了内部临时表(内存或磁盘)来处理查询,常见于GROUP BY、DISTINCT、UNION或复杂JOIN。例如SELECT DISTINCT name FROM users JOIN orders ON users.id=orders.user_id,若name无索引,优化器可能建临时表去重。优化核心:确保GROUP BY或DISTINCT字段有索引,或重写为EXISTS。
Using join buffer (Block Nested Loop)
表示使用连接缓冲区进行嵌套循环连接,通常因被驱动表无有效索引。此时type多为ALL或index。解决方案:为被驱动表关联字段添加索引,或调整join_buffer_size(需谨慎,过大占用内存)。
Impossible WHEREWHERE条件恒假,如WHERE 1=0或WHERE id=123 AND id=456。MySQL直接返回空结果,不访问表。这是优化器的早期拦截,无需干预。
Select tables optimized awaySELECT中只有聚合函数且无GROUP BY,如SELECT COUNT(*) FROM users。优化器直接从表元数据(InnoDB的dict_table_t::stat_n_rows)获取行数,不扫描数据页。这是极致优化。
常见问题速查表:
Extra值 是否危险 根本原因 紧急度 Using filesort 高 ORDER BY无索引或索引不匹配 ⚠️⚠️⚠️ Using temporary 高 GROUP BY/DISTINCT字段无索引 ⚠️⚠️⚠️ Using join buffer 中 被驱动表缺失关联索引 ⚠️⚠️ Using where; Using index 低 索引覆盖WHERE,但需回表取其他字段 ✅ Using index 低 覆盖索引,零回表 ✅✅✅
4. 实战案例:从慢查询日志到EXPLAIN调优的完整闭环
4.1 案例背景:电商大促期间订单查询接口P99飙升至3.2秒
某电商平台大促期间,订单列表接口(GET /api/orders?user_id=123&status=paid)P99响应时间从200ms飙升至3.2秒,告警频繁。慢查询日志捕获到典型SQL:
SELECT id, order_no, amount, status, create_time FROM orders WHERE user_id = 123456 AND status = 'paid' ORDER BY create_time DESC LIMIT 20;表结构:orders(id PK, order_no, amount, status, create_time, user_id),总行数1200万。
4.2 EXPLAIN初诊:发现三个致命信号
执行EXPLAIN:
+----+-------------+--------+------------+------+------------------+----------+---------+-------+--------+----------+-----------------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+--------+------------+------+------------------+----------+---------+-------+--------+----------+-----------------------------+ | 1 | SIMPLE | orders | NULL | ALL | idx_user_status | NULL | NULL | NULL | 850000 | 10.00 | Using where; Using filesort | +----+-------------+--------+------------+------+------------------+----------+---------+-------+--------+----------+-----------------------------+信号1:type=ALL—— 全表扫描,rows=850000预估扫描85万行,远超实际匹配行数(用户123456的订单约200条)。
信号2:key=NULL—— 尽管存在idx_user_status(user_id,status)索引,但未被使用。
信号3:Extra=Using filesort——ORDER BY create_time DESC无法利用索引,需额外排序。
4.3 深度根因分析:索引失效的连锁反应
第一步:验证索引存在性
SHOW INDEX FROM orders WHERE Key_name = 'idx_user_status'; -- 确认索引存在,字段顺序为 (user_id, status)第二步:检查字段数据类型与条件匹配user_id字段为BIGINT,查询条件user_id = 123456是数字,无类型转换问题。status为VARCHAR(20),条件'paid'是字符串,匹配。
第三步:分析统计信息偏差
执行SHOW TABLE STATUS LIKE 'orders',发现Rows=12000000(准确),但Cardinality对idx_user_status显示12000,远低于实际选择性(user_id唯一值应接近1200万)。执行ANALYZE TABLE orders更新统计信息后,EXPLAIN仍显示key=NULL,排除统计问题。
第四步:聚焦ORDER BY冲突
联合索引(user_id, status)的排序顺序是:先按user_id升序,再按status升序。而查询要求ORDER BY create_time DESC,create_time不在索引中,且与索引字段无关,必然触发filesort。但为何连WHERE条件都不走索引?查阅MySQL优化器文档发现:当ORDER BY字段无法被索引满足,且WHERE条件选择性不高时,优化器可能放弃索引,认为全表扫描+排序比索引查找+排序更快。此处user_id=123456选择性高(单用户订单少),但优化器预估rows=850000(因统计失真),误判为低选择性。
4.4 优化方案与效果验证
方案1:创建覆盖索引(首选)
ALTER TABLE orders ADD INDEX idx_user_status_ct (user_id, status, create_time DESC);此索引满足:
WHERE user_id = ? AND status = ?:前两字段精准匹配ORDER BY create_time DESC:第三字段按DESC排序,天然支持逆序SELECT字段中id, order_no, amount, status, create_time:status和create_time被覆盖,但id, order_no, amount需回表。由于id是主键,回表成本固定(一次主键查找)。
EXPLAIN结果:
+----+-------------+--------+------------+-------+---------------------+---------------------+---------+-------------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+--------+------------+-------+---------------------+---------------------+---------+-------------+------+----------+-------------+ | 1 | SIMPLE | orders | NULL | range | idx_user_status_ct | idx_user_status_ct | 12 | const,const | 20 | 100.00 | Using where | +----+-------------+--------+------------+-------+---------------------+---------------------+---------+-------------+------+----------+-------------+type=range,rows=20(精准预估),Extra无filesort,key_len=12(user_id8字节 +status4字节,create_time未用于WHERE,故不计入)。
方案2:强制索引 + 优化器提示(应急)
若无法立即加索引,可用FORCE INDEX:
SELECT ... FROM orders FORCE INDEX (idx_user_status) WHERE user_id = 123456 AND status = 'paid' ORDER BY create_time DESC LIMIT 20;但此方案治标不治本,且依赖人工干预。
上线效果:
- P99响应时间从3.2秒降至180ms
- QPS从800提升至2200
- 服务器CPU负载下降40%
实操心得:索引优化不是“加一个索引就完事”。我坚持“三步验证法”:1)
EXPLAIN确认计划改善;2)EXPLAIN ANALYZE(或SHOW PROFILE)确认各阶段耗时真实下降;3) 压测验证业务指标(P99、QPS、CPU)。曾有一次,EXPLAIN显示type=ref,但EXPLAIN ANALYZE发现Sending data阶段耗时90%,根源是SELECT *导致网络传输巨大,最终通过只查必要字段解决。
5. 高级技巧与避坑指南:那些文档里不会写的实战经验
5.1 如何让EXPLAIN真正“看见”你的意图?
默认EXPLAIN是只读预测,但可通过以下方式引导优化器:
USE INDEX / IGNORE INDEX
当存在多个索引,优化器选择次优方案时,可显式指定:
SELECT * FROM orders USE INDEX (idx_user_status) WHERE user_id=123 AND status='paid';但需谨慎:USE INDEX仅提示,不保证;FORCE INDEX才强制,但若索引不存在会报错。
Optimizer Hints(MySQL 5.7+)
比USE INDEX更精细:
SELECT /*+ USE_INDEX(orders, idx_user_status_ct) */ id, order_no FROM orders WHERE user_id=123 AND status='paid';支持JOIN_ORDER、SET_VAR等复杂提示,适合DBA深度调优。
修改会话变量临时生效
针对特定查询调整优化器行为:
SET SESSION optimizer_switch='index_merge=off'; -- 关闭索引合并,强制单索引 SET SESSION sort_buffer_size=4*1024*1024; -- 增大排序缓冲,减少filesort磁盘IO注意:sort_buffer_size是每个查询独享,设置过大易OOM。
5.2 EXPLAIN的四大认知误区与破除方法
误区1:“rows越小越好”rows是预估行数,非实际扫描行。曾有一个SQL,rows=1但执行耗时2秒,EXPLAIN ANALYZE显示rows_examined=1000000。根源是WHERE条件中status IN ('paid','shipped'),但status字段只有两个值,选择性极低,优化器误判。破除方法:用SHOW INDEX查看Cardinality,若Cardinality/Rows < 0.01,说明索引选择性差,应废弃。
误区2:“Extra=Using index就是最优”
覆盖索引虽快,但索引体积膨胀。idx_user_status_ct(user_id,status,create_time)比idx_user_status(user_id,status)大3倍,写放大增加。破除方法:权衡读写比。若该表写入频繁(每秒百次INSERT),而此查询QPS仅10,优先保写入性能,用FORCE INDEX+ 优化ORDER BY逻辑(如前端分页改用游标分页)。
误区3:“type=ref一定比range好”ref是等值查找,range是范围查找。但若range的rows为10,而ref的rows为10000(因字段重复率高),range更优。破除方法:永远看rows与filtered的乘积(预估最终结果行数),而非孤立看type。
误区4:“EXPLAIN结果稳定,无需再查”
统计信息、数据分布、会话变量均会动态变化。破除方法:建立定期巡检机制。我编写了一个Python脚本,每日凌晨自动抓取慢查询TOP 10,对每条SQL执行EXPLAIN FORMAT=JSON,比对key_len、rows、Extra与基线值,偏差超30%自动告警。
5.3 生产环境EXPLAIN最佳实践清单
必做三件事
- 所有上线SQL必须附EXPLAIN截图:在PR描述中粘贴
EXPLAIN FORMAT=JSON输出,DBA审核通过才可合并。 - 慢查询日志阈值设为100ms:而非默认的1s,早发现潜在问题。
- 建立索引健康度看板:监控
SHOW INDEX的Cardinality与Rows比值,低于0.001的索引标红预警。
禁做三件事
- 禁止在WHERE中对索引字段使用函数:
WHERE YEAR(create_time)=2023→WHERE create_time BETWEEN '2023-01-01' AND '2023-12-31'。 - 禁止用OR连接不同字段索引:
WHERE user_id=123 OR status='paid'→ 拆分为UNION或添加复合索引。 - 禁止在高并发场景用SELECT *:明确列出所需字段,减少网络传输和内存占用。
一个被低估的技巧:用EXPLAIN验证索引删除影响
准备删除一个疑似无用的索引前,先执行: