news 2026/9/26 9:31:53

MySQL EXPLAIN深度解析:从执行计划看SQL性能瓶颈

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MySQL EXPLAIN深度解析:从执行计划看SQL性能瓶颈

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的三大陷阱

  1. 索引未创建:最基础,检查SHOW CREATE TABLE确认索引存在。
  2. 索引未被选择:优化器认为全表扫描成本更低。此时需检查rows预估是否严重失真,或FORCE INDEX强制指定。
  3. 索引失效: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 WHERE
WHERE条件恒假,如WHERE 1=0或WHERE id=123 AND id=456。MySQL直接返回空结果,不访问表。这是优化器的早期拦截,无需干预。

Select tables optimized away
SELECT中只有聚合函数且无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最佳实践清单

必做三件事

  1. 所有上线SQL必须附EXPLAIN截图:在PR描述中粘贴EXPLAIN FORMAT=JSON输出,DBA审核通过才可合并。
  2. 慢查询日志阈值设为100ms:而非默认的1s,早发现潜在问题。
  3. 建立索引健康度看板:监控SHOW INDEX的Cardinality与Rows比值,低于0.001的索引标红预警。

禁做三件事

  1. 禁止在WHERE中对索引字段使用函数:WHERE YEAR(create_time)=2023→WHERE create_time BETWEEN '2023-01-01' AND '2023-12-31'。
  2. 禁止用OR连接不同字段索引:WHERE user_id=123 OR status='paid'→ 拆分为UNION或添加复合索引。
  3. 禁止在高并发场景用SELECT *:明确列出所需字段,减少网络传输和内存占用。

一个被低估的技巧:用EXPLAIN验证索引删除影响
准备删除一个疑似无用的索引前,先执行:

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 9:31:37

7个AI Agent实战项目拆解:从工具调用到企业级部署

说实话&#xff0c;AI Agent学习最不缺的就是资源和教程&#xff0c;缺的是“自己动手把一个东西跑通”的体验。今晚8点免费解锁的这7个AI Agent实战项目&#xff0c;核心标准只有一个&#xff1a;每一个都能在两三个晚上做完&#xff0c;做完之后你能真正理解Agent的一个关键环…

作者头像 李华
网站建设 2026/9/26 9:28:48

TIA Portal V18完整包安装与仿真报错排查指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/26 9:27:49

Sentinel-1 SAR数据处理全指南:从InSAR形变监测到光学协同实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/26 9:27:18

STM32开发环境四件套:CubeMX、Keil、ST-Link与串口助手分工详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华