SQL优化实战:用EXPLAIN读懂执行计划,破解索引失效与慢查询难题
1. 为什么每次聊 SQL 优化都绕不开 Explain 和索引做了这么多年后端开发和数据库调优我几乎每天都要面对各种慢查询。很多人一听 SQL 优化就觉得是大神才能干的活其实核心就两件事把 SQL 看透把索引建对。而看透 SQL 最直接的手段就是EXPLAIN它不会真正执行你的语句而是告诉你 MySQL 会怎么执行这条 SQL是一张真正的执行方案蓝图。至于索引优化就更基础了。很多同学对索引的认知停留在“建了索引查询就快”结果经常遇到明明建了索引却不走或者走了索引依然很慢的情况。问题出在哪就出在大家没有真正理解索引在 MySQL 内部是怎么工作的也没学会用EXPLAIN来验证自己的索引设计到底有没有生效。这篇内容我想把自己在实际项目里积累的EXPLAIN解析经验和索引优化思路整理出来。不堆理论直接讲怎么看执行计划、怎么从执行计划反推索引问题、哪些坑我踩过并且希望你绕开。适合正在使用 MySQL 的开发者、DBA、以及所有被慢查询折磨过的同学。1.1 先从一次真实的慢查询说起前一阵处理过一个线上告警某张订单表的查询平均耗时从 200ms 涨到了 3 秒多。表里数据量其实不大就几十万行按说这个量级不应该是这种表现。我拿到慢查询日志后第一反应就是先看这条 SQL 的执行计划而不是急着加索引。SELECT order_id, order_status, buyer_id, total_amount FROM trade_order WHERE order_status WAIT_PAY AND create_time 2024-09-01 00:00:00 ORDER BY total_amount DESC LIMIT 10;执行EXPLAIN之后type 是ALL也就是全表扫描rows 估算扫描了 40 万行。明明 create_time 上有索引为什么不用这正是很多同学最容易踩的坑索引不是“有”就行而是要“能用”。这条 SQL 的 WHERE 条件里同时有 order_status 和 create_time单列索引最多只能走其中一个另一个就只能靠回表过滤。MySQL 优化器经过代价估算认为走 create_time 索引再回表逐行判断 order_status还不如直接全表扫描来得“划算”。这就是典型的单列索引覆盖不了多条件查询的场景。也是我接下来要反复强调的一个观点索引设计一定是针对 SQL 的不是针对表的。你手里有多少张表并不重要重要的是你的核心 SQL 里到底会派生出哪些查询模式索引就该围绕这些模式来设计。2. Explain 输出列逐项拆解每一列到底在说什么EXPLAIN的结果通常有十几列很多人只盯着type、rows看其他列基本忽略。但如果你真遇到了一个复杂的慢查询任何一列都可能暴露出索引设计的问题。我按影响程度从高到低把最关键的几列拆开讲一遍。2.1 type 访问类型衡量扫描方式的硬指标type是执行计划里最直观的一个字段它表示 MySQL 访问表中行数据的最终方式。性能从好到差大致是类型含义能力评级system表只有一行属于 const 的特例极优const通过主键或唯一索引查询最多返回一行极优eq_ref联表查询时外表每行匹配内表唯一索引的一行优秀ref通过普通索引等值匹配到多行良好range索引上的范围扫描良好index扫描整个索引树比全表扫描快一般ALL全表扫描较差我处理过很多慢查询绝大多数ALL都能通过加索引解决但index这个类型最容易忽悠人。明明显示用了索引但实际上是把整个二级索引树从头到尾扫了一遍这是因为 SELECT 的字段和查询条件无法在索引中完成过滤或者说索引的选择性太低优化器认为直接遍历索引树比逐行回表判断成本更低。看到index一定不要觉得万事大吉如果你在线上看到一条高频 SQL 的执行计划是index直接报警处理吧。2.2 key 与 key_len索引是否真正生效的证据key显示的是优化器最后决定使用的索引名key_len是这个索引中实际用到的字节数。两者配合起来能发现一个非常普遍的问题联合索引用了但只用了左前缀。举个例子假设你有联合索引(a, b, c)SQL 里 WHERE 条件只写了a和c那么 key 会显示这个联合索引但 key_len 只计算字段 a 的长度不会包含 c。这说明 b 列没有参与索引过滤索引效率其实没完全发挥。key_len的计算还和字段类型、字符集、是否允许 NULL 密切相关。比如INT类型是 4 字节BIGINT是 8 字节。VARCHAR(100)在utf8mb4字符集下一个字符最多占 4 字节所以key_len至少是 100 * 4 2 或 100 * 4 1加 2 是变长字段长度记录占用允许 NULL 还要再加 1 字节。看到没光一个key_len就藏了这么多信息如果你不做二次计算根本不知道你的联合索引到底被用到了哪一列。2.3 rows 与 filtered优化器的“成本账单”rows表示 MySQL 认为要检查的行数。这是一个估算值但非常能反映问题。如果rows是几百万而实际返回结果只有几十行说明这条 SQL 的过滤条件严重依赖回表之后的行判断索引设计一定有问题。filtered表示经过这层条件过滤后剩余行数的百分比。比如rows显示 10000filtered是 10最终交给上层的行数就是 10000 * 10% 1000。看到低filtered值意味着大部分扫描到的行都被丢掉了浪费了大量 IO。最理想的组合是rows足够小filtered接近 100。我经常说的一句话是如果你的执行计划里filtered低于 20%哪怕type是ref这条 SQL 也有巨大的优化空间。2.4 Extra 中的隐晦信号Extra列包含的信息最多但最容易被忽略。我这里只挑几个意义重大的遇到它们必须引起警觉。Using filesort表示 MySQL 需要额外的一次排序才能得到结果可能是在内存或磁盘上。排序如果发生在非索引字段上代价极高。这个几乎是慢查询的头号元凶。Using temporary使用了临时表常见于 GROUP BY 或 DISTINCT 操作一旦出现基本意味着这条 SQL 别有用心需要重写。Using index这是好信号表示查询已经覆盖索引。看到它说明你执行的查询的全部字段都在索引里不需要回表。Using where表示存储引擎返回记录后又在 MySQL 服务层进行了过滤。在range和ref类型下出现是正常的如果出现在ALL类型里说明很多行会被无谓地读出来再扔掉。Using index condition表示使用了索引下推即部分 WHERE 过滤条件被下推到索引层进行判断减少了回表次数。这是 MySQL 5.6 之后出现的优化看到它并不差。2.5 id 与 select_type复杂 SQL 的阅读顺序当 SQL 里出现子查询、UNION、多表 JOIN 时EXPLAIN会返回多行。id相同的行表示同一组查询执行顺序按从上到下。id递增的组执行顺序则是先执行 id 大的组。select_type常见的有SIMPLE、PRIMARY、SUBQUERY、DERIVED、UNION等。很多人看复杂 SQL 时直接懵掉我教大家一个土办法先把id最大的子查询拎出来看因为它最先执行它产出的结果集是外围查询的数据来源。如果最内层的子查询出现全表扫描那整个 SQL 的性能天花板就基本定死了。3. 索引优化最佳实践从单列索引到覆盖索引搞懂EXPLAIN之后真正的重头戏就是索引设计了。很多同学在设计索引时是“凭感觉”先看 WHERE 有哪些字段就给哪些字段建索引。结果建了一堆索引慢查询反而更多了。3.1 联合索引设计的第一性原则联合索引的本质是多列构成的一棵排序树它遵循最左前缀原则索引(a, b, c)能用于(a)、(a,b)、(a,b,c)的查询场景但不能直接用于(b)或(b,c)。这个原理很简单可一旦遇到实际业务很多人就乱了。我给业务设计联合索引时有一个固定的思考顺序先找出 SQL 的 WHERE 条件里所有等值判断的字段。再找出 WHERE 条件里所有范围判断的字段。然后把等值字段按区分度从高到低排列放在最前面。范围字段放在等值字段之后且一个范围字段之后的其他字段就无法参与索引过滤了。最后如果还有 ORDER BY 字段且满足排序方向一致可以追加在索引末尾。这条规则不是教条它来源于 B 树的匹配逻辑。等值匹配可以精确定位到索引树的某一段范围匹配只能锁定一个区间。如果你把范围字段放在联合索引前面索引上后面的字段天然无法用于精确定位白白浪费。还有一个特别值得说的点并不是所有等值字段都值得放进联合索引。如果某个区分度极低的字段比如性别就算放在最前面也仅仅是把巨大的结果集缩小到 50%意义不大。实践下来联合索引最左侧字段的选择要优先考虑“能快速裁掉大量记录”的字段。3.2 覆盖索引让查询彻底绕开回表回表是 InnoDB 中一种代价很高的操作。二级索引只保存索引字段和主键值如果查询的列里出现了索引之外的字段为了获得那些字段的值就必须去主键索引树再查一次。一次回表还好几十万次回表就是灾难。覆盖索引就是让SELECT的字段、WHERE的字段、ORDER BY的字段都落在同一棵二级索引树里。这样查询过程完全不需要回表直接把索引树当成一张“迷你表”来读。EXPLAIN中看到Extra为Using index就说明命中了覆盖索引。我给大家一个非常实用的优化手法如果你发现一个高频查询里老是出现某个固定字段集而这个查询又频繁查询不要犹豫直接为这个字段集构建联合索引哪怕它不是过滤条件也必须包含进去。比如日志表统计某业务的查询天天执行SELECT order_status, COUNT(*) FROM trade_order WHERE seller_id ? GROUP BY order_status这时建一个(seller_id, order_status)的联合索引既能过滤又能覆盖 GROUP BY 的分组字段彻底避免回表和临时表。3.3 索引下推无感知的隐形加速器索引下推Index Condition PushdownICP是 MySQL 5.6 引入的一项优化。在没有 ICP 之前联合索引(name, age)面对WHERE name LIKE 张% AND age 20这样的查询存储引擎把匹配到 name 前缀的所有记录都回表再在服务层过滤 age。启用 ICP 后存储引擎会在索引遍历过程中直接判断 age 条件过滤掉不满足的记录只剩极少数需要回表。很多 DBA 从不关注 ICP因为它不需要你在 SQL 上做任何改动只要索引设计正确它自动生效。但理解它有一个重大好处你会明白联合索引中范围条件之后的字段并非完全废物。举个例子(a, b)联合索引WHERE 是a 1 AND b 2这没问题。如果 WHERE 是a 100 AND b 2a 走范围扫描后b 字段本身无法作为索引树的前缀参与定位但有了 ICPb 2 会在索引遍历时提前过滤减少回表。设计索引时在范围字段后面放一个后续等值字段依然能改善效率。3.4 重复索引与冗余索引索引不是越多越好线上数据库里最常见的问题是同一字段既出现在单列索引上又出现在多个联合索引的最左侧。比如先有一个(status)索引后来又建了(status, create_time)前者就是典型的冗余索引。大部分情况下优化器会选择最合适的联合索引单列索引常年不用却还要承担写入时的维护成本。索引不是免费的。每多一个索引INSERT、UPDATE、DELETE都要额外更新一棵 B 树。对于写多读少的表过度索引会让写入性能雪上加霜。我在索引评审时有个原则一个表核心索引数量尽量控制在 5 个以内每个索引都必须在对应真实查询中有实际收益宁缺毋滥。删冗余索引之前可以用EXPLAIN对比前后计划确认走的是你预期的新索引再放心删。4. 实操案例一次完整的 Explain 索引优化过程纸上谈兵讲了这么多我们来完整走一遍一个模拟的业务查询优化。这个案例我特意把各种常见问题堆在一起保证你看完能直接迁移到自己的场景里。4.1 案例背景与问题 SQL有一张商品评价表product_review核心字段包括字段类型说明idbigint主键product_idbigint商品 IDuser_idbigint用户 IDratingtinyint评分 1~5statustinyint状态 1 正常 2 隐藏contentvarchar(500)评价内容create_timedatetime创建时间reply_timedatetime回复时间线上慢 SQL 是这样的SELECT id, product_id, user_id, rating, content, create_time FROM product_review WHERE product_id 2024091301 AND rating 4 AND status 1 ORDER BY create_time DESC LIMIT 20;这条 SQL 用于在商品详情页展示优质评价列表每天被调用上万次。第一次拿到手上的时候执行耗时 1.8 秒线上已经开始出现偶发超时。4.2 第一轮 Explain定位问题核心执行EXPLAIN后结果如下关键列idtypekeykey_lenrowsfilteredExtra1ALLNULLNULL120万0.1Using where; Using filesorttype 是 ALLkey 是 NULLrows 估算 120 万行filtered 只有 0.1%。这意味着查询把所有行都翻了一遍最终获得的行数是 120 万乘 0.1%也就是一千行左右。而且还有 filesort意味着一百多万行全部读出来后还要在内存或磁盘排序耗时全花在没意义的 IO 上了。为什么一条条件明确的 SQL 会走全表原因很简单我当时表里根本没有可用的索引优化器只能选择全表扫描。不要笑很多生产环境的表就是这样裸奔的。这是第一轮优化最简单的切入点把基础索引建出来。4.3 第二轮优化加联合索引并验证根据前面的设计原则首先找等值字段product_id和status。然后是范围字段rating。最后是排序字段create_time。排序方向是DESC如果强行把 create_time 放进联合索引因为其它字段都是等值创建索引时设为倒序也可以但由于 MySQL 8.0 之前不支持降序索引这里很容易踩坑。我当时的做法是先把等值字段放前面把范围字段放中间排序字段先不进索引从简单方案入手。ALTER TABLE product_review ADD INDEX idx_product_status_rating (product_id, status, rating);再次执行EXPLAINidtypekeykey_lenrowsfilteredExtra1refidx_product_status_rating9240010.0Using where; Using filesorttype 变成了 refrows 从 120 万降到了 2400这个提升是肉眼可见的。但注意Extra 里还有 filesort。因为索引里根本没有 create_time 字段ORDER BY create_time 无法借助索引顺序MySQL 只能对返回的 2400 行做一次排序。虽然能跑了但还不够优雅高频查询最好连 filesort 都省掉。4.4 第三轮优化干掉 filesort要把 filesort 干掉最直接的办法就是把排序字段 create_time 也放进联合索引设计成(product_id, status, rating, create_time)。这里有一个技术细节必须强调因为查询是ORDER BY create_time DESC在 MySQL 8.0 之前的版本中索引默认是升序的按索引顺序正向扫描刚好得到的是 ASC。如果你需要 DESC 结果MySQL 会反向读取索引这依然是高效的因此不必看到 DESC 就放弃索引。真正的痛点是如果联合索引里 rating 是一个范围查询create_time 即使放在最后也无法进入定位条件但排序呢在 InnoDB 里范围条件之后的字段不能用于索引内的排序。所以更稳妥的做法是把排序字段放在范围字段前面但要保证等值字段在前。即索引(product_id, status, create_time, rating)。查询条件 product_id、status 是等值直接定位到某棵子树然后这棵子树内部天然按 create_time 排好序。此时ORDER BY create_time DESC直接反向扫这段索引得到的结果已经有序随后只需要对 rating 4 做判断。因为 rating 在排序字段之后无法参与索引过滤但有空闲的索引下推帮忙过滤实际回表量已经非常少。改造后的索引ALTER TABLE product_review ADD INDEX idx_prod_status_time_rating (product_id, status, create_time, rating);同时删除之前那个不包含排序字段的索引避免冗余。最终执行计划是idtypekeykey_lenrowsfilteredExtra1refidx_prod_status_time_rating980099.0Using index conditiontype是 refExtra只有 Using index condition没有了 filesort没有了 Using wherefiltered 上升到 99。线上实测从 1.8 秒降到了 12ms效果立竿见影。4.5 案例总结一个可复用的索引分析模板这个案例基本覆盖了一个典型的索引优化闭环。我复盘一下完整执行过程拿到慢 SQL先补一个完整EXPLAIN。看 type 是否 ALLrows 是否巨大Extra 是否带 filesort 或 temporary。列出 SQL 中所有与过滤、排序、分组相关的字段。按等值字段、范围字段、排序字段的优先级设计联合索引。用EXPLAIN验证新计划对比 rows、filtered、Extra。确认效果后删除废弃索引避免冗余。这套流程我用了很多年几乎可以应对 90% 的索引慢查询问题。5. 常见索引失效场景与排查技巧实录即便你已经理解了索引原理实际开发中还是会遇到很多“索引在你面前但你用不上”的情况。这一节全是实战中踩坑换来的知识点我按高频程度列出来。5.1 隐式类型转换导致索引失效最常见的场景是表里字段是varcharSQL 查询时却传成了数字类型。例如WHERE user_phone 13800000001而 user_phone 列是varchar。MySQL 会把列类型转换成数字去比较这个转换发生在索引列上直接导致索引列无法进行正常匹配优化器只能放弃索引。反过来如果字段是int你却传了字符串MySQL 会先把字符串转成数字然后和 int 列比较因为没有发生在索引列上的转换索引照样能用。所以关键知识点是永远不要让索引列参与任何运算、函数或类型转换。要么改 SQL要么改表结构两者必须占一个。验证方法也简单直接EXPLAIN看 key 有没有值。如果你对某个 SQL 到底会不会走索引没把握最笨也是最可靠的方式就是把真实值代入执行EXPLAIN因为EXPLAIN结果受到实际参数值的影响不同值可能导致不同的计划。5.2 在索引列上使用函数或表达式WHERE DATE(create_time) 2024-09-01这种写法我相信每个人都写过。函数把 create_time 列包了起来索引树上的顺序就被函数打乱了MySQL 无法直接根据索引树定位到某个日期区间只能全索引扫描或者全表扫描。正确写法是用范围查询WHERE create_time 2024-09-01 00:00:00 AND create_time 2024-09-02 00:00:00前面案例里的 create_time 过滤条件我也特意用了范围区间而不是DATE()函数这是索引可用性的底线。还有一个常见的表达式是WHERE a 1 100这也一定走不了索引改写为WHERE a 99才能命中索引。5.3 LIKE 前导模糊与 OR 的陷阱LIKE %关键词%这种查询一定用不到索引因为 B 树是按关键字排列的你不知道匹配字符串的起始位置自然无法二分定位。只有LIKE 关键词%这样前缀匹配能利用索引。如果业务确实需要前导模糊查询可以考虑引入全文检索能力或者用专门的搜索引擎不能指望数据库索引硬扛。OR的陷阱更隐蔽WHERE status 1 OR rating 5即使两个字段各自有单列索引MySQL 在老版本下也可能只选其中一个索引或者直接全表扫描。因为 OR 意味着取并集如果每个条件的索引分别扫描再合并成本可能比全表扫描更高。新版本虽然引入了 Index Merge 优化但它的生效条件比较苛刻不稳定。最稳妥的写法是改造成UNION ALLSELECT ... WHERE status 1 UNION ALL SELECT ... WHERE rating 5不过涉及去重时用UNION要谨慎地分析各自索引的可行性而不是盲目复制模板。5.4 ORDER BY 与 GROUP BY 相关索引问题ORDER BY走索引有一个隐藏条件排序字段必须和索引中的字段顺序完全一致并且所有字段的排序方向要么全部 ASC要么全部 DESC。如果对联合索引中的多个字段排序顺序还要求完全符合最左前缀原则。第 4 节的案例里我们把 create_time 放在联合索引中就是为了让排序不需要额外的 filesort。GROUP BY也是类似本质上是先分组再聚合。如果 GROUP BY 字段不满足索引前缀MySQL 很可能会创建临时表来分组出现Using temporary。解决思路是把 GROUP BY 字段按最左前缀顺序放进索引或者想办法把分组操作改写成等值条件下的聚合查询减少分组的数据范围。5.5 统计信息不准确引发错误执行计划经常会遇到一种情况索引明明存在且可用但优化器就是不选它反而走了全表扫描。这有可能是表的统计信息过期导致的。MySQL 优化器依赖采样统计来估算成本但如果表数据出现大幅增长或删减统计信息没有及时更新优化器会基于过时的数据进行判断。最简单的做法是ANALYZE TABLE重新收集统计信息。我有一次处理类似问题时一张表从一万行突然涨到五百万行统计信息还是旧的一万行全表扫描成本被严重低估执行计划始终是 ALL。执行ANALYZE TABLE之后优化器立刻选择了正确索引。这种问题不常见但碰到了往往让人困惑值得记在排查手册里。5.6 排查工具与氛围EXPLAIN 只是第一步EXPLAIN给出的只是估算不是真实执行情况。在确认索引生效之后可以用EXPLAIN ANALYZEMySQL 8.0进一步查看真实执行时间和各步骤消耗。它能精确输出每条路径实际扫描的行数、耗时以及排序、连接所用的时间占比。比起普通的EXPLAINEXPLAIN ANALYZE会真正执行 SQL线上使用时要谨慎最好在只读实例或低峰期执行。另外开启慢查询日志并配合pt-query-digest这类工具分析 SQL 样本能快速找出哪些 SQL 最值得优化。慢查询日志定位到的第一优先级并不是耗时最长的而是总执行次数多、单次执行虽短但累计资源消耗高的高频中等耗时查询。这些查询往往最能在不改变业务的情况下带来整体性能提升。6. 索引优化之外的性能保障细节索引优化当然不是 SQL 性能的全部。我在实际项目中见过太多“索引建得不错但整体性能还是上不去”的场景很多细节值得特别提醒。6.1 避免 SELECT *始终明确列清单SELECT *会把表中所有列都查出来如果表上存在大字段比如text、blob一次查询的 IO 成本会成倍增加。更重要的是SELECT *几乎不可能产生覆盖索引因为你无法把整张表所有字段都塞进一个索引里。我在代码评审时几乎把SELECT *当成硬性红线要求必须列出字段。从优化器角度看字段列表越小覆盖索引的命中概率越高。6.2 分页深翻页的优化方案开发过程中最常见的坑之一是分页越翻越慢。LIMIT 100000, 20的意思是读取前 100020 行再丢掉前 100000 行。这 100000 行即使走索引也都要全部读出来代价非常大。优化思路有很多种最常用的是延迟 join 或者基于上一页最大 ID 的翻页。延迟 join 的思路是先利用覆盖索引查出所需主键 ID然后再和原表做 join 取出完整行SELECT r.* FROM product_review r INNER JOIN ( SELECT id FROM product_review WHERE product_id 2024091301 AND status 1 ORDER BY create_time DESC LIMIT 100000, 20 ) t ON r.id t.id子查询只用到了索引字段和主键可以完全走覆盖索引减少回表。如果业务场景允许基于上一页 create_time 和 id 组合游标的分页方式更高效但实现上要改接口语义适合配合产品设计一起改造。6.3 读写分离与数据归档的双重保险索引优化做到极致之后如果单表数据量实在太大比如超过两千万甚至过亿单纯靠索引已经很难保证毫秒级响应。这时候就要考虑水平分片或者把历史数据归档到冷表。很多公司把半年前的历史订单从主表迁移到归档表线上主表始终控制在千万级别以内配合索引就能长期保持高性能。数据归档听着不复杂实际推进时要注意业务查询是否允许跨新旧表。如果用户历史订单可能需要查询你得先设计好一个查询路由策略。这里最容易踩的坑是归档脚本本身成为新的慢查询来源建议分批操作每次限制几千条并且选择业务低峰期运行。6.4 查询频率比单次耗时更值得关注我遇到不少团队天天盯着单条耗时最高的 SQL 看却忽略了那些执行频率极高但单次只有几十毫秒的查询。100ms 的查询一天执行 100 万次和 2 秒的查询一天执行 100 次前者消耗的总资源远大于后者对数据库的并发压力也完全不同。优化的时候我会用慢查询日志做点击率统计再把高频 SQL 逐条EXPLAIN按总耗时排优先级。这条思路比“谁最慢先搞谁”科学得多也是我个人多年优化经验里最想分享的心得之一。7. 我在实际项目中总结的几条索引军规最后分享一些零散但非常重要的经验。这些内容不像前面那样成体系却是我在真实碰壁后沉淀下来的土办法每一条都值得记下来。第一条尽量使用自增主键。InnoDB 的聚簇索引就是主键索引如果主键是自然键比如手机号写入时可能会频繁触发页分裂导致数据物理存储碎片化查询性能下降。自增主键能保证插入基本是顺序写性能最稳定。第二条区分度低于 20% 的字段不要单独建索引。比如 status 只有 0、1、2 三种值单建 status 索引并不会有太好的效果。但如果 status 和另一个高区分度字段组成联合索引放在最左侧对等值定位影响不算大可以按需保留。第三条同一个 SQL 的 WHERE 和 ORDER BY 发生冲突时优先让 WHERE 走索引排序交给文件排序前提是 WHERE 过滤后的数据量足够小。一旦你知道排序的行数只有几百filesort 成本完全可控不要为了省排序硬把一个排序字段塞进索引结果反而导致过滤性能下降。第四条索引字段长度太长时必须考虑前缀索引。比如一个varchar(255)的 url 字段全字段建索引会导致索引树膨胀占用内存和磁盘空间并且写入和查询的 IO 都不小。可以只索引前 50 个字符但如果查询条件小于 50 个字符无法直接匹配前缀索引就要配合合适的截断策略需要谨慎设计。还有一点想强调线上索引变更一定要先在测试环境用真实数据量做一遍验证。不少索引在几百行数据的表上看着没问题到了千万级表里才暴露出统计信息估算偏差。我在实际项目中已经养成了习惯在做 DDL 之前一定先模拟线上量级的数据执行EXPLAIN确认计划正常再择机在低峰期操作。数据库优化从来都不是一次性的索引要跟随业务变化持续迭代每次上线新功能、新增查询场景都应该把EXPLAIN检查加进回归流程里。这套方法论在团队里推行之后慢查询数量肉眼可见地大幅下降这才是索引优化带来的真正价值。