尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

MySQL索引优化:用EXPLAIN执行计划判断SQL是否用到索引

MySQL索引优化:用EXPLAIN执行计划判断SQL是否用到索引 MySQL 用没用索引这件事大概是开发同学问得最多的问题之一。很多人上线前建了一堆索引结果一条慢 SQL 把数据库拖到报警一查才发现索引压根没走。更常见的是在测试环境数据量小SQL 跑得飞快没人意识到索引没生效一上生产几百万行数据压过来问题立刻爆出来。这篇文章我就把“如何判断 SQL 是否用到了索引”这件事讲透核心工具就是 EXPLAIN。读完你能看懂执行计划的每一列能自己判断索引有没有生效、失效原因是什么以及遇到慢查询时该怎么一步步排查。这篇文章适合所有写 SQL 的开发、DBA 和运维同学尤其适合那些建了索引却不确定查询是否真正用它的人。我尽量用实际案例说话所有示例 SQL 你都可以直接在自己环境里跑一遍验证。1. 判断 SQL 是否使用索引的第一步EXPLAIN 基础用法1.1 为什么看索引不能只看表结构要看执行计划很多人有个误区以为给字段建了索引查询就一定会走索引。实际情况要复杂得多。索引就像书的目录目录建好了但你是不是真的按目录去翻需要查一下才知道。MySQL 到底怎么执行这条 SQL是扫全表还是走索引由优化器决定。优化器会根据表的数据量、索引的区分度、统计信息、查询条件等等因素做权衡。我见过不少案例明明字段上有索引查询条件也写了结果 EXPLAIN 一看 type 列是 ALL全表扫描。典型的场景包括索引列上做了函数运算、发生了隐式类型转换、使用了前置模糊匹配再或者优化器认为小表全表扫描比走索引回表更快。这些坑后面我都会详细展开。所以判断一条 SQL 是否用到索引不能靠猜也不能只看有没有索引必须看执行计划。执行计划是优化器给出的最终执行方案它告诉 MySQL“我是怎么干这件事的”。1.2 EXPLAIN 到底怎么用输出长什么样EXPLAIN 的使用极其简单在任何 SELECT 语句前面加 EXPLAIN 关键字就行。它不会真的执行这条 SQL只是让优化器计算出一个执行方案给你看。EXPLAIN SELECT * FROM user WHERE username zhangsan;我准备了一张简单的用户表用来做接下来的所有演示CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL, age INT DEFAULT NULL, status TINYINT DEFAULT NULL, phone VARCHAR(20) DEFAULT NULL, create_time DATETIME DEFAULT NULL, PRIMARY KEY (id), KEY idx_username_age (username, age) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;表里有一个主键索引 id一个复合索引 idx_username_age(username, age)。插入几条测试数据后执行上面的 EXPLAIN 语句输出类似这样mysql EXPLAIN SELECT * FROM user WHERE username zhangsan\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: user partitions: NULL type: ref possible_keys: idx_username_age key: idx_username_age key_len: 201 ref: const rows: 1 filtered: 100.00 Extra: NULL 1 row in set (0.00 sec)注意我加了\G让输出竖排显示这样字段很多的时候不会乱。这个输出里面每一行都是一个执行步骤每个字段都有特定含义。新手第一次看这堆字段容易懵其实核心只需要关注四列type、key、rows、Extra。抓住这四列90% 的问题都能看出来。2. 看懂 EXPLAIN 输出的关键列type、key、rows、Extra2.1 type 列访问类型的优劣排序到底什么才算有效的索引利用type 列表示 MySQL 找到所需行使用的方式也反映了这条 SQL 的效率强烈建议把这个优先级背下来system const eq_ref ref range index ALL这个排序从左到右效率从高到低。下面逐个说system表只有一行是 const 类型的特例基本见不到。const主键或唯一索引等值查询时出现最多返回一行。比如用主键 id 查MySQL 能直接定位到那一行这是最快的。eq_ref连接查询中被驱动表通过主键或唯一索引等值匹配每一行最多匹配一条。多表 join 时看到它是好现象。ref非唯一索引等值匹配比如刚才查 usernamezhangsanusername 有索引但不是唯一索引type 就是 ref。range索引范围扫描比如id 100、create_time BETWEEN ... AND ...用到了索引但扫描的是一个范围。index全索引扫描遍历整个索引树。比全表扫描好一点因为索引通常比数据页小但也不算高效。ALL全表扫描这个就是最不愿意看到的了等于把整张表从头翻到尾。实践中的判断标准很简单type 至少要达到 range 级别才算是有效利用了索引。ref 是非常不错的水平const 和 eq_ref 是理想状态。看到 ALL基本可以断定这条 SQL 需要优化看到 index需要进一步确认是不是真的没有过滤条件或者覆盖索引也救不回来。2.2 key 和 key_len确认列表索引名以及索引到底覆盖到哪一列EXPLAIN 输出有两列跟索引名相关possible_keys 和 key。possible_keys 列出的是优化器认为可能用到的索引key 则是优化器最终实际选择使用的索引。这里有个常见的坑仅看 key 列不为 NULL 还不够。比如复合索引 idx_username_age(username, age)如果 SQL 是WHERE username zhangsankey 确实用了 idx_username_age但 key_len 的长度只包含 username 这一列。这说明只用了复合索引的左侧第一列age 列实际上没参与索引定位。key_len 是索引列的最大字节长度可以用来反推索引到底用到了几个字段。不同字段类型的长度计算规则大致是字符类型字符数乘以字符集最大字节数。utf8mb4 下VARCHAR(50) 对应于 50 x 4 200 字节。变长字段 VARCHAR 额外加 1~2 字节记录长度这里 VARCHAR(50) 最大 200 字节加 1 字节。字段允许为 NULL额外再加 1 字节。整数类型INT 占 4 字节BIGINT 占 8 字节可为空同样加 1 字节。回到我们的表username 是 VARCHAR(50) NOT NULLage 是 INT NULL。那么只用到 username 时key_len 200 1 201同时用到 username 和 age 时key_len 200 1 4 1 206所以看到 key_len 是 201 还是 206就能知道复合索引是否用到了完整的两列。这是排查复合索引问题时非常实用的技巧。2.3 Extra 里那些让你又爱又恨的关键词Extra 这一列的信息量很大经常藏着决定性的线索。它不像 type、key 那么直观但往往能揭示 MySQL 具体做了什么额外操作。几个高频词我整理成了一张速查表Extra 内容含义优化方向Using index覆盖索引查询所需字段都在索引中无需回表好现象尽量保持Using where存储引擎返回后Server 层又对数据进行了过滤考虑是否缺索引Using index condition索引条件下推ICP部分过滤条件下推到存储引擎MySQL 5.6 默认优化正常现象Using filesort需要额外排序不是文件排序但很影响性能检查 ORDER BY 字段能否走索引Using temporary使用了临时表常见于 GROUP BY、DISTINCT、子查询尽量用索引覆盖分组排序Using MRR使用多范围读取优化好现象Backward index scan反向扫描索引MySQL 8.0 对 DESC 排序的优化这里我想重点提醒两个词。第一个是 Using filesort很多新手以为它表示性能很糟其实它只是说排序没法利用索引MySQL 要额外把数据复制到排序缓冲区处理。一旦看到它就要检查 ORDER BY 的字段顺序是否和索引一致。第二个是 Using index这是覆盖索引的标志是多少人求之不得的优化状态。我见过一句话总结得很到位Using index 是“查完索引就完事了”没有它往往意味着还要回表拿其他字段。3. 亲手演练通过三个典型场景看索引是否生效3.1 场景一主键查询 vs 普通字段查询先看主键查询这是最简单的场景mysql EXPLAIN SELECT * FROM user WHERE id 1\G *************************** 1. row *************************** id: 1 table: user type: const possible_keys: PRIMARY key: PRIMARY key_len: 4 rows: 1 Extra: NULLtype 是 constkey 是 PRIMARYkey_len 是 4INT 主键的长度说明这次查询通过主键精确定位到了唯一一行这是最优路径。再看一个普通字段但没有索引的情况我们把 phone 字段查一下phone 没有建索引mysql EXPLAIN SELECT * FROM user WHERE phone 13812345678\G *************************** 1. row *************************** id: 1 table: user type: ALL possible_keys: NULL key: NULL rows: 100 Extra: Using wheretype 是 ALLkey 是 NULLrows 估算扫描 100 行。这足以说明这条查询是全表扫描。即使数据量小也要明确它没走索引。通过这个对比你应该能体会到索引到底有没有用执行计划一目了然。3.2 场景二复合索引的最左前缀原则这是最容易踩坑的地方。我们建立了 idx_username_age(username, age)这个索引能同时为 username 和 age 查询服务但前提是必须遵循最左前缀原则查询条件中必须包含最左侧的 username 列索引才会被使用。如果跳过了 username 直接查 age索引就用不上。我们来实际验证一下。第一种情况只查 usernameEXPLAIN SELECT * FROM user WHERE username zhangsan;type 是 refkey 是 idx_username_agekey_len 是 201说明索引被使用了但只用到了第一列。第二种情况同时查 username 和 ageEXPLAIN SELECT * FROM user WHERE username zhangsan AND age 23;key 依然是 idx_username_age但 key_len 变成了 206说明索引完整用到了两列。这种情况下索引的过滤能力更强效率更高。第三种情况只查 ageEXPLAIN SELECT * FROM user WHERE age 23;type 很可能是 ALLkey 为 NULL。虽然 age 是复合索引的第二列但因为没有以 username 开头索引直接失效。这就是最左前缀原则的威力也是很多人建了复合索引却发现查询没用上的常见原因。我建议你记住一个判断口诀复合索引就像一本按“姓氏 名字”排列的通讯录你只报名字让管理员找人管理员只能从头翻没任何捷径。3.3 场景三覆盖索引能帮你省掉多少回表成本InnoDB 的二级索引非主键索引叶子节点存储的是索引列加上主键值。如果查询所需要的字段都能在索引里找到那 MySQL 查完索引直接返回结果根本不需要回表去数据页里取其他字段这种情况 Extra 会显示 Using index。举个例子EXPLAIN SELECT username, age FROM user WHERE username zhangsan;因为 username、age 都在 idx_username_age 这个索引里查询压根不用去主键索引里取别的列Extra 会显示 Using index。而如果是SELECT * FROM user WHERE username zhangsan需要查询的字段里包含 phone、status、create_time 等不在索引中的列MySQL 就必须拿着主键 id 回表找到完整的数据行返回。覆盖索引的价值在于它可以大幅减少回表次数尤其在大数据量场景下回表意味着随机的磁盘 I/O是性能杀手。所以一个非常实用的优化思路是针对高频查询尝试把 SELECT 的字段包含到索引中做成覆盖索引。但也要注意索引不是越多越好每增加一个索引都会拖慢写入速度。4. 索引失效的常见场景排查这是慢 SQL 的根源4.1 函数运算、隐式转换和模糊匹配索引杀手三件套如果说执行计划是看“有没有用索引”那接下来就要讨论“为什么没用索引”。根据我长期的排查经验索引失效的案例九成以上可以归结到下面几类。第一类对索引列使用函数。比如在 create_time 字段上建了索引查询写成了EXPLAIN SELECT * FROM user WHERE DATE(create_time) 2024-01-01;我们在 create_time 上套了 DATE 函数MySQL 无法直接利用索引进行比较只能全表扫描。正确的写法是改成范围查询EXPLAIN SELECT * FROM user WHERE create_time 2024-01-01 AND create_time 2024-01-02;第二类隐式类型转换。phone 字段建了索引但它是 VARCHAR 类型写 SQL 时给了数字EXPLAIN SELECT * FROM user WHERE phone 13812345678;MySQL 会把字符串列和数字比较时默认把字符串转换为数字导致索引列上发生隐式函数运算索引就失效了。解决办法是严格按照字段类型传参写成phone 13812345678。第三类LIKE 前置模糊匹配。WHERE username LIKE %zhang%这种写法因为通配符在前面索引定位无从谈起会全表扫描。如果业务确实需要这种模糊查询要么接受全表扫描要么考虑全文索引、ES 等外部方案。如果是LIKE zhang%则可以利用 range 扫描。除了上面三类OR 连接的查询也很典型。比如EXPLAIN SELECT * FROM user WHERE username zhangsan OR status 1;如果 status 没有索引MySQL 在优化时往往只能选择全表扫描。更稳妥的做法是使用 UNION ALL 拆分或者确保 OR 两侧的字段都有索引。4.2 优化器为什么“有索引不用”统计信息、数据分布和回表成本有时候 SQL 写法没有问题索引也存在但 EXPLAIN 结果依然显示 ALL。这时候别急着怀疑人生先想想优化器是“笨”还是它觉得“没必要用索引”。一个非常常见的情况是表里只有几十条数据。优化器一算全表扫描才几十行走索引反而要额外访问索引树再回表成本更高。它当然选择 ALL。这种情况在小表上天然会发生不是索引失效也不是优化器有 bug你放一两百万行数据进去再看逻辑可能就变了。另一个常见原因是数据分布。比如在 status 字段上建了索引但整张表 90% 的数据 status 都是 1。你查询WHERE status 1优化器评估后发现用索引要扫 90% 的索引节点再回表还不如直接全表扫这样反而更快。还有一个很容易被忽略的点统计信息过期。如果表的数据量发生了大幅变化但统计信息没及时更新优化器可能基于过期的统计做了错误判断。这时候执行ANALYZE TABLE user;刷新统计信息往往能解决问题。如果排除了上述所有情况你仍然认为走索引更好可以临时尝试 FORCE INDEX 进行验证EXPLAIN SELECT * FROM user FORCE INDEX (idx_username_age) WHERE age 20;这会强制优化器使用指定索引对比一下强制前后的执行计划能帮你理解优化器的选择依据也可以用于临时压测。4.3 一个可复用的排查流程和问题速查表在实际工作中我建议你遇到慢 SQL 时按照下面这个流程来排查这样最省时间先跑EXPLAIN SELECT ...看 type 是不是 ALLkey 是不是 NULL。如果没走索引检查 SQL 条件是不是对索引列做了函数、隐式转换、前置模糊、OR 连接。检查查询条件是否满足最左前缀原则复合索引第一列在不在 WHERE 里。确认表的数据量小表全表扫描可能是合理行为。执行ANALYZE TABLE刷新统计信息再跑一次 EXPLAIN。用 FORCE INDEX 对比确认优化器判断是否合理。如果 SQL 写法正常但就是慢考虑覆盖索引、拆分查询或者调整索引结构。为了方便查阅我把常见失效场景整理成了速查表失效场景示例处理方式索引列使用函数DATE(create_time) 2024-01-01改写为范围查询隐式类型转换phone 13812345678按字段类型传参LIKE 前置模糊username LIKE %zhang%换方案或接受全表扫描复合索引违反最左前缀WHERE age 23调整索引顺序或补查第一列OR 连接非索引列username a OR status 1用 UNION ALL 拆分统计信息过期大量增删后估算 rows 严重偏差ANALYZE TABLE小表数据量太少几十行全表扫描比索引走查还快正常现象无需处理5. 进阶select_type、filtered 与真实执行时间验证5.1 select_type 和 filtered识别子查询和连接问题的关键前面讲到的字段可以应付大部分单体查询的场景但如果 SQL 里有子查询、关联查询或 UNION就需要额外关注 select_type 和 filtered 这两列。select_type 表示查询的类型常见的包括SIMPLE简单的 SELECT没有子查询和 UNION。PRIMARY最外层查询。SUBQUERY子查询中的第一个 SELECT。DERIVED派生表即 FROM 后面的子查询。UNIONUNION 中第二个及之后的 SELECT。如果 EXPLAIN 结果出现多条记录每行代表执行计划中的一个步骤。你需要按 id 顺序从小到大逐条看。当出现 DERIVED 时通常意味着 MySQL 要把子查询结果物化成临时表再参与查询这种性能开销不容忽视。很多情况下用 JOIN 改写子查询或者用窗口函数能有效减少这种问题。filtered 是一个百分比表示 InnoDB 返回给 Server 层的数据中经过 WHERE 条件过滤后剩余行数的比例。它和 rows 列配合使用可以估算最终返回的行数rows x filtered / 100。如果 SQL 连接了大量数据但 filtered 只有百分之几说明索引选择性差或者连接条件有问题。举个例子如果 EXPLAIN 显示 rows 是 10000filtered 是 1%那说明最终可能只有 100 行是有效的但 MySQL 为此扫描了 10000 行这往往可以通过在过滤列上添加合适的索引来优化。5.2 EXPLAIN ANALYZE 与 profiling用真实数据验证索引效果EXPLAIN 给出的 rows 是估算值不是实际值。你可能会遇到一种尴尬EXPLAIN 明明显示走了索引但 SQL 实际执行还是很慢。这种情况说明问题不在“有没有用索引”而在于索引使用之后仍然需要处理大量数据或者某些统计因子和实际差异很大。MySQL 8.0.18 及以上版本提供了 EXPLAIN ANALYZE真正执行这条 SQL 并返回实际的执行时间和行数EXPLAIN ANALYZE SELECT * FROM user WHERE username zhangsan;输出大致长这样- Index lookup on user using idx_username_age (usernamezhangsan) (cost0.35 rows1) (actual time0.118..0.121 rows1 loops1)注意看 actual time 和 rows这是真实执行后的数据。如果实际行数远大于估算 rows那统计信息可能不准。如果 actual time 很大就要继续往下追查回表成本或者其他开销。对于 MySQL 8.0 之前的版本也可以使用传统 profiling 手段SET profiling 1; SELECT * FROM user WHERE username zhangsan; SHOW PROFILES; SHOW PROFILE FOR QUERY 1;这种方式能看到执行各阶段消耗的时间帮助定位瓶颈到底是在 Sending data、Sorting result 还是其他阶段。不过说实话这个功能在 8.0 之后已经被标记为废弃新环境我更推荐直接用 EXPLAIN ANALYZE。要说个人体会的话我做 SQL 调优这些年最大的感受是判断是否走索引只是第一步真正的难点在于理解优化器的决策逻辑。没有哪个索引进阶技能是看几个教程就会的一定要在真实数据量、真实业务模型下反复验证。同一个 SQL在数据分布不同的两张表上执行计划可能完全不同。所以生产环境出了问题别指望靠经验拍脑袋第一件事永远是跑一遍 EXPLAIN用执行计划说话。
返回列表