做后端开发这几年最常被拉到群里去救火的场景十有八九是数据库慢查询。系统跑得好好的突然某天接口变慢、CPU飙高一看日志全是SELECT走了全表扫描几百万行的表一次查询几秒钟才回来。这种时候十次里有九次是索引设计出了问题。这篇内容我想把 MySQL 索引这条线彻底讲透从底层原理到实战调优再到我踩过的那些坑一次性梳理清楚。不管你刚接触索引还是已经写了好几年 SQL 偶尔还是会困惑这篇应该都能让你少走不少弯路。MySQL 索引本质上就是数据库维护的一份目录它存在的唯一目的就是让查询少读数据。听起来简单但真要设计好索引你得理解它底层为什么快、什么场景会失效、不同类型索引各自适合什么查询模式。这篇文章我会围绕这样几个核心问题展开索引是怎么存储和工作的联合索引那个最左前缀到底怎么回事EXPLAIN 分析执行计划该怎么看以及实际业务里建索引时的取舍逻辑。1. 先把索引的原理吃透为什么数据库会越查越慢1.1 没有索引的全表扫描到底有多慢我们经常在业务代码里说能用索引就不用全表扫描但很多人其实没直观感受过全表扫描四个字的真实成本。你可以把一张没有索引的表想象成一本没有目录、也没有章节标题的厚书。你想找一个关键词唯一的办法就是从第一页翻到最后一页逐行比对。这个动作放在数据库里就是存储引擎从表的第一行数据开始一块块地把数据页读进内存然后逐行检查是否满足WHERE条件。具体量化下来一张 1000 万行的表假设每行占用约 200 字节那么光是数据文件可能就有 2GB 到 3GB。按 InnoDB 每页 16KB 来计算这大约是十几万个数据页。全表扫描意味着要遍历这十几万页。即使数据已经部分缓存在内存里真正从磁盘读取那些冷数据页的随机 IO 开销是非常可观的。磁盘随机读取一次大约 5ms 到 10ms我们按乐观的 5ms 算扫完这十几万页理论上就是几百秒的 IO 时间——当然实际中会并发预读、有缓存命中不至于那么夸张但一次查询花几百毫秒甚至好几秒是完全正常的。这就是索引存在的核心理由索引把无序的数据变成了有序的结构让查找从逐行遍历变成按目录定位。一次原本要扫几万个数据页的查询走索引可能只需要读几层索引节点加上少数几个数据页IO 次数差了三个数量级。说到底数据库的绝大部分性能问题最后都归结为少读点数据。1.2 B树索引的物理形态与查询路径MySQL 里 InnoDB 引擎用的索引结构是 B 树这几乎是面试必问、实战必备的知识点。B 树长什么样呢你可以把它理解成一个多层级的目录结构根节点是顶层目录中间层的每个节点存储着若干个键值指针告诉你某个范围的数据在下一层的哪个分支里最底层的叶子节点才真正保存着有序的数据键值。和普通的二叉树、B树相比B 树有两个关键设计决定了它在数据库场景下的表现。第一所有数据都集中在叶子节点内部节点只存放索引键和子节点指针不存放真实数据。这意味着同样一个 16KB 的数据页能容纳的索引键数量非常多树的层级会被压得很矮。一个三层高的 B 树哪怕索引键只占 8 字节也能支撑上亿条记录的查找。查询任意一条数据最多只需要三次磁盘 IO 就能定位到叶子节点这个 IO 次数几乎就是常数级别跟数据总量关系不大。第二叶子节点之间通过双向链表连接形成一个有序的链表结构。这个设计对范围查询特别友好。比如查WHERE create_time BETWEEN 2024-01-01 AND 2024-01-31只要先在 B 树上定位到起始位置然后顺着叶子节点的链表顺序向后遍历直到超出范围条件为止不需要回头再走树的路径。这就是为什么索引能同时加速等值查询和范围查询。顺便说一句很多人会纠结为什么不直接用哈希索引。哈希索引做等值查询确实比 B 树还快O(1) 级别但没法支持范围查询也没法支持排序。而现实中大部分查询都是范围、排序、分组这些操作所以 InnoDB 选择了 B 树作为主力索引结构。MySQL 的 Memory 引擎有哈希索引但用得非常少核心原因就是适用范围太窄。1.3 聚簇索引 vs 二级索引两个核心概念InnoDB 里所有数据本质上都存在一棵 B 树上这张表的聚簇索引就是数据本身。聚簇索引的叶子节点保存的是完整行数据而这张表的聚簇索引默认建在主键上。所以 InnoDB 表有且只有一个聚簇索引你建了主键就用主键建没显式建主键就用第一个非空的唯一索引都没有的话引擎会隐藏一个自动生成的 6 字节行号。二级索引也叫辅助索引、非聚簇索引是另一回事。它的叶子节点不存完整行数据只存索引键和对应主键值。这就引出了一个非常高频的概念回表。你用一个二级索引去查数据时先走这棵二级索引 B 树找到匹配记录的主键值然后再拿这个主键值去聚簇索引的 B 树里再查一次才能取到完整行。这个查两次树的过程就叫回表。回表次数是判断查询性能的重要指标。一个二级索引命中了 1000 行就要回表 1000 次每次都是一次主键 B 树查找。这就是为什么覆盖索引会成为优化重点——如果查询需要的所有列都已经包含在二级索引里那就不需要回表了直接读索引叶子节点就能返回结果查询效率会高一大截。后面我会专门讲这个。理解了聚簇索引和二级索引你在设计时就能想明白为什么要给一张表建主键、为什么主键最好是有序的递增整数。如果主键是随机生成的 UUID新插入的行在 B 树上的位置会随机分布导致大量页分裂和碎片化写入性能会明显变差。提示给 InnoDB 表建主键时优先选自增整数或者单调递增的业务 ID。用 UUID 做主键虽然看起来无懈可击但引发的页分裂和碎片问题是真实存在的数据量大之后写入性能差距非常直观。2. 索引类型与适用场景别再一把梭建索引2.1 唯一索引、普通索引与主键索引怎么选很多同学刚开始建索引时比较随性想到哪个字段查得频繁就建哪个类型也不在意。其实不同类型的索引在语义、约束和性能上有明显差别选错类型的代价不小。主键索引就是我们前面说的聚簇索引每个表只能有一个特点是叶子节点直接保存整行数据。它同时承担两个职责约束数据唯一性和作为二级索引回表的目的地。主键设计得好不好直接影响整个表的写入性能和所有二级索引查询的最终回表成本。唯一索引允许列值为 NULL也就是说多行 NULL 不违反唯一约束但非 NULL 值不能重复。它在约束数据唯一性上跟主键类似但本身是一个二级索引查询时走的是独立 B 树需要回表。业务场景举例用户表的手机号字段逻辑上需要唯一但可能早期系统允许空手机号那就不能用主键可以建一个唯一索引来兜底。这里有个小经验只有当业务真的需要数据库层面强制唯一时才用唯一索引如果只是查询的时候希望加速某个唯一性很强的字段建普通索引就够了。因为唯一索引在写入时要多做一次唯一性检查虽然单次开销不大但高频写入下累积起来也是成本。普通索引就纯粹是加速查询用的没有唯一性约束。设计原则是用在区分度足够高、查询模式足够固定的列上。还有一种容易被忽略的情况在WHERE条件里只做等值匹配的字段建普通索引就足够了没必要因为这个字段好像很特别就强行加唯一约束。2.2 联合索引最左前缀原则详解联合索引复合索引是指在一张表的多个列上共同建一个索引比如(user_id, status, create_time)。它的核心价值是让多个过滤条件共用一棵 B 树比给每个列分别建单个索引占用空间小、查询效率更高。联合索引的排序规则是先按第一个列排序第一个列相同的再按第二个列排序以此类推。这就决定了它最核心的检索规则——最左前缀原则查询条件里必须包含联合索引最左边的列这个索引才可能被用到。比如索引(a, b, c)WHERE a 1 AND b 2能用到WHERE b 2 AND c 3就用不到因为跳过了最左边的 a。但很多人在理解最左前缀时会犯一个直觉错误以为只要 SQL 里写了最左列就行。实际上最左前缀指的是查询条件中可用的索引列范围是从最左开始连续的。WHERE a 1 AND c 3虽然用了 a但 b 条件断开之后c 就没法走索引了优化器只能在 a 命中的结果里对 c 做过滤。这被称为索引中断。联合索引设计时有一个常见的口诀把等值查询的列放前面把范围查询的列放后面把区分度高的列放前面区分度低的放后面。原因在于联合索引中键值是有序的等值条件可以精确定位范围而范围条件会终止索引继续向下匹配。举个例子索引(status, create_time)在业务上常用来查某个状态下的订单且按时间排序这时候WHERE status 1 AND create_time 2024-01-01就可以很好地利用这个索引先定位 status1 的分支再顺着有序的 create_time 扫过去非常自然。注意联合索引的列顺序一旦确定并不是说随便排都一样。同样的两个列(a, b)和(b, a)是语义完全不同的两个索引能覆盖的查询模式也完全不同。设计前先花几分钟梳理业务里的高频查询组合比上线后反复加索引省事得多。2.3 覆盖索引减少回表的实战价值前面提到二级索引叶子节点存的是索引列 主键如果查询需要的数据恰好全部在索引里连回表都省了。这被称作覆盖索引。很多人理解覆盖索引只停留在哦知道这个概念的层面实际优化时却不知道怎么操作。来看一个具体场景。假设订单表上有一个联合索引(user_id, status)业务上高频查询是统计某个用户某个状态下的订单数SELECT COUNT(*) FROM orders WHERE user_id 123 AND status 2;这个查询要的只有user_id和status两个列都已经在联合索引里InnoDB 直接在索引 B 树上遍历匹配的叶子节点就能返回计数不需要回表。反之如果查询是SELECT order_amount FROM orders WHERE user_id 123 AND status 2;order_amount不在索引里那 MySQL 就得拿着匹配到的主键回表把每一行都查一遍才能取到金额。这个差距在数据量大时非常明显——我优化过一个统计接口加了覆盖字段之后耗时从 1.8 秒降到了 80 毫秒原因就是省掉了十几万次回表。所以在设计联合索引时不是只考虑WHERE条件里的列还要把高频查询里SELECT的列考虑进去。这就是所谓的索引列包含查询所需的所有列。但也不要走向另一个极端为了覆盖索引把所有常用列都塞进一个索引。索引列过多会让 B 树的节点变大、层级变深反而降低检索效率。一般原则是覆盖那些真正高频的、核心的查询场景而不是面面俱到。2.4 前缀索引与函数索引字符串类型的列尤其像title、content、url这类比较长的字段如果整列建索引每个索引条目都要存很长的字符索引体积会很大而且 B 树里字符串比较的开销也高。这时的常用手段是前缀索引只对字符串的前 N 个字符建索引。CREATE INDEX idx_url_prefix ON pages (url(20));前缀索引的取舍在于 N 的选择。N 太小区分度不够过滤效果差N 太大省空间效果不明显。通常可以通过统计不同前缀长度的基数来判断。比如SELECT COUNT(DISTINCT LEFT(url, 10)) AS c10, COUNT(DISTINCT LEFT(url, 20)) AS c20, COUNT(DISTINCT LEFT(url, 30)) AS c30 FROM pages;和整列的唯一值总数做对比找到一个失真率可以接受的长度。我一般要求前缀区分度达到整列区分度的 90% 以上。不过要提醒一点前缀索引无法用于覆盖索引因为索引里存的不是完整值也无法用于排序和分组。如果查询只是等值匹配前缀索引通常够用如果还需要ORDER BY这个字符串字段那前缀索引帮不上忙。函数索引是 MySQL 8.0 才正式支持的。它的语法很直接CREATE INDEX idx_year ON orders ((YEAR(create_time)));如果你经常按YEAR(create_time)或者DATE_FORMAT(create_time, %Y-%m)这类函数处理后的结果来过滤建函数索引能让这些查询不再失效。更早的版本面对where 条件里对索引列用了函数这种场景基本无解这也是索引失效最主要的原因之一。3. 索引设计与优化从执行计划开始的完整思路3.1 EXPLAIN 是第一步核心字段怎么读设计索引不能靠猜。无论是排查慢查询还是验证新索引的效果第一步永远是看执行计划。MySQL 提供EXPLAIN命令加在 SELECT 查询前面就能看到优化器打算怎么执行这条 SQL。EXPLAIN SELECT user_id, status FROM orders WHERE user_id 123 AND status 2;输出里最值得关注的字段是这些type访问类型。从好到差大致是systemconsteq_refrefrangeindexALL。ALL就是全表扫描是我们要消灭的对象index虽然用了索引但属于遍历整个索引树也不算高效常见的有效类型是ref等值匹配二级索引和range范围匹配。key实际用到的索引名。如果为 NULL说明这条 SQL 没用上任何索引。rows优化器预估需要扫描的行数。这个值不是精确的但可以作为参考通常希望它远小于表的总行数。Extra这里面藏着很多重要信息。出现Using index说明是覆盖索引扫描不需要回表出现Using where说明在存储引擎返回结果后又做了一层条件过滤出现Using filesort说明排序没有用上索引效率堪忧出现Using temporary说明用了临时表常见于某些 GROUP BY / DISTINCT 场景。有经验之后你扫一眼type和key就能判断这条 SQL 有没有救。我最常用的排查路径是先看type是不是ALL再看key是不是预期的索引最后看Extra里有没有filesort和temporary。对应的解决思路分别是加索引、调整索引列顺序、调整查询写法。3.2 索引失效的七个高频场景很多人建了索引但 SQL 写得不合适索引照样用不上。我把工作中遇到最多的索引失效场景列出来每一条都是实际踩过坑总结出来的第一对索引列使用函数或表达式。比如WHERE YEAR(create_time) 2024如果 create_time 上有索引这个写法会让索引失效。解决办法是改成范围条件WHERE create_time 2024-01-01 AND create_time 2025-01-01。如果你必须要用函数就像前面说的建函数索引。第二隐式类型转换。典型场景是索引列是字符串类型但查询条件传了数字。比如phone列是VARCHAR类型WHERE phone 13800138000MySQL 会把字符串列隐式转成数字再比较导致索引失效。解决方法是保证查询参数的类型和列类型一致这种问题在 ORM 框架里尤其容易埋雷。第三前模糊匹配。WHERE name LIKE %张这种以通配符开头的写法B 树没法利用有序性来定位起点只能全扫。但WHERE name LIKE 张%是可以走索引的因为字符串排序后张开头的记录是连续的一段。区分这两者很重要。第四OR 条件连接。WHERE a 1 OR b 2这种写法如果 a 和 b 没有都建索引优化器很容易放弃索引走全表扫描。替代方案是用UNION ALL拆成两个查询或者确保 OR 两边都有可用索引。第五联合索引跳过最左列。这个前面已经提过(a, b)联合索引上查WHERE b 2是用不到的。第六NOT IN、!、这些负向查询。它们一般会走全表扫描除非匹配的行数占比极低MySQL 认为还不如扫表快。相比这些负向写法尽量把逻辑转成正向枚举比如NOT IN (cancel)经常可以转成IN (pending, paid, done)。第七数据量太少的表。优化器会算一笔账如果走索引需要多次随机 IO而全表扫描是顺序 IO表又特别小顺序扫可能更快。这是优化器主动选择不用索引不是索引失效。3.3 索引列怎么选区分度与查询模式选索引列最核心的判断依据是区分度。一个列的取值范围越广、重复值越少索引的过滤效果越好。极端反例是在性别列上建索引——区分度只有男女两种查出来一半的数据这时候走索引反而可能比全表扫描还慢。我习惯用一条 SQL 快速评估候选列的区分度SELECT COUNT(DISTINCT candidate_col) / COUNT(*) AS selectivity FROM big_table;这个比例叫选择率。一般建议选择率高于 20% 的列才值得建索引。低于这个值索引的收益会非常有限。除了区分度另一个关键指标是查询模式。分析你的业务 SQL看哪些查询是等值过滤、哪些是范围过滤、哪些是排序分组。等值过滤的列适合放在联合索引前面范围过滤的列适合放后面排序分组的列也可以通过索引来避免filesort。举个例子一个订单列表页前端传user_id等值、status等值、create_time排序那联合索引设计成(user_id, status, create_time)就是很自然的方案。优化器可以先用前两列做等值定位再按第三列的有序性直接返回排序结果一条索引同时解决了过滤和排序两个问题。3.4 冗余索引与过度索引隐形成本索引不是免费的。每建一个索引写入的时候就要多维护一棵 B 树。对一张高频插入、更新的表来说索引数量从 3 个涨到 10 个写入性能会有肉眼可见的下降。我遇到过一张线上日志表因为历史遗留问题攒了 12 个索引每秒写入从 2000 条掉到 700 条。所以索引越多越好是绝对的反模式。冗余索引是另一个隐蔽的问题。联合索引(a, b)建好之后单独的索引a实际上就是冗余的因为(a, b)已经能覆盖所有以 a 为最左列的查询。发现冗余索引可以通过查询所有索引的信息来排查SELECT table_name, index_name, GROUP_CONCAT(column_name ORDER BY seq_in_index) FROM information_schema.statistics WHERE table_schema 你的库名 AND table_name orders GROUP BY table_name, index_name;然后对比哪些索引的最左列是另一个索引的子集就可以安全删掉冗余的那个。但这步操作要特别谨慎因为删除索引之前必须用EXPLAIN验证所有相关 SQL 仍然走预期的索引。注意生产环境删索引一定要收敛在低峰期操作并且先备份、先 EXPLAIN 验证、分批执行。删之前再问自己一遍这个索引真的没人用了吗很多线上事故都源于我以为这个索引没人用。4. 实操案例一个慢查询从发现到优化的完整过程4.1 问题复现与慢查询日志定位某天线上反馈一个订单列表接口响应变慢页面从原来的 300ms 涨到了 3 秒以上。我先登录数据库打开慢查询日志的开关确认当前阈值设置SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1;运行一段时间后从慢查询日志里捞出了这个高频慢 SQL# Query_time: 2.87s Lock_time: 0.0002s Rows_sent: 20 Rows_examined: 482934 SELECT order_id, user_id, status, amount FROM orders WHERE user_id 12345 AND status 2 ORDER BY create_time DESC LIMIT 20;注意Rows_examined: 482934意思是这条查询为了返回 20 行扫描了 48 万行。这是典型的查得少、看得多问题问题就出在数据访问路径上。我们先冷静分析一下这个表的结构orders 表数据量约 300 万行现有的索引是idx_user_id单列索引和idx_create_time单列索引。WHERE条件里有user_id和status两个等值条件排序用create_time。看起来三个字段都有索引怎么还会扫 48 万行这正是执行计划能告诉我们答案的地方。4.2 执行计划分析定位根因直接在慢 SQL 前面加 EXPLAINEXPLAIN SELECT order_id, user_id, status, amount FROM orders WHERE user_id 12345 AND status 2 ORDER BY create_time DESC LIMIT 20;关键输出如下字段值typerefkeyidx_user_idrows482934ExtraUsing where; Using filesort看这三行信息问题就清楚了。key用的是idx_user_id说明优化器选择了 user_id 这个单列索引但user_id 12345这个过滤条件只把范围缩小到了该用户的全部订单而该用户有 48 万条订单记录。status 2这个条件没法在索引内部过滤只能靠Using where逐行判断排序字段create_time也不在索引里所以出现了Using filesort拿到匹配数据后还要额外排序。这里有两个优化方向一是让status也在索引里参与过滤二是让排序走索引避免 filesort。最直接的办法就是建一个联合索引(user_id, status, create_time)同时覆盖三个关键操作。4.3 索引方案设计与验证我的设计方案是CREATE INDEX idx_user_status_time ON orders (user_id, status, create_time);这个联合索引从左到右依次处理了等值条件 user_id、等值条件 status 和排序字段 create_time。B 树会先按 user_id 分组再在组内按 status 排序最后在相同的 (user_id, status) 记录里面按 create_time 有序排列。这样优化器定位到目标用户的特定状态分支后顺着 create_time 的有序链表从尾部往前取 20 条即可不需要额外的排序动作。建完索引之后立即验证执行计划EXPLAIN SELECT order_id, user_id, status, amount FROM orders WHERE user_id 12345 AND status 2 ORDER BY create_time DESC LIMIT 20;这次输出字段值typerefkeyidx_user_status_timerows246ExtraUsing index conditionrows从 48 万降到了 246Using filesort消失了。这里我顺手确认了一件事amount字段不在索引里所以 Extra 里没有出现Using index查询定位到目标行之后仍需要回表取 amount。考虑到这个接口单次返回只有 20 行回表 20 次的开销完全可以接受没必要为了追求完美覆盖索引把 amount 也塞进去——那样会让索引体积变大反而得不偿失。4.4 优化前后数据对比索引生效后的实测数据如下指标优化前优化后Rows_examined482934约 250查询耗时2.87s28ms接口 P95 耗时3.2s180ms执行计划里预估扫描 246 行实际扫描约 250 行和预估基本一致。查询速度提升了两个数量级。整个优化过程其实只做了一件事把用户查询的过滤路径从单列索引定位大范围逐行过滤外部排序改成了联合索引直接精确定位有序扫描。这就是索引设计最核心的价值——让查询的每一步都在索引结构里完成而不是靠回表硬扛。这个案例还有一个值得说的点之前idx_user_id和idx_create_time两个单列索引其实是历史遗留的随手索引。在联合索引上线后idx_user_id作为idx_user_status_time的最左前缀已经属于冗余索引我后续在低峰期将其删除。idx_create_time则被保留因为还有其他的时间范围查询场景需要单独使用它。5. 常见问题与排查技巧实录5.1 索引失效排查速查表实践里遇到各种奇怪问题有时候明明有索引优化器就是不走。我把高频问题整理成一张速查表方便你排查时对照症状可能原因排查/解决方案明明建了索引EXPLAIN 显示全表扫描查询条件对索引列用了函数改成范围条件或建函数索引等值查询中索引没用上隐式类型转换核对列类型与参数类型是否一致LIKE 查询很慢通配符在开头确认是否可以用右侧通配符或全文索引联合索引只命中部分条件查询跳过了最左列调整查询条件或按最左前缀重建索引查询很快但 ORDER BY 慢排序字段不在索引中把排序列纳入联合索引查询走了索引但还是慢回表行数太多用覆盖索引或缩小查询范围优化器放弃索引表太小或返回行占比太高确认统计信息是否过期必要时 ANALYZE TABLE5.2 索引碎片与维护策略索引用久了会出现碎片。原因是 InnoDB 的 B 树频繁插入、删除时会发生页分裂和页合并导致数据页在物理上不连续叶子节点的链表面临更多随机 IO。实际表现就是索引的逻辑结构没变但扫描效率下降了。判断碎片程度可以查表的统计信息SHOW TABLE STATUS LIKE orders\G看Data_free字段这个数值代表表中空闲的碎片空间。如果Data_free相对表大小占比很高比如超过 10%就该考虑整理一下了。常见的碎片整理手段是ALTER TABLE orders ENGINE InnoDB;或者OPTIMIZE TABLE orders;这两种方式本质都是重建表会重新整理数据和索引消除碎片。但要注意它们都会锁表大表执行时会阻塞读写。在 MySQL 8.0 中ALTER TABLE ... ENGINEInnoDB配合在线 DDL 的ALGORITHMINPLACE已经可以做到一定程度的在线重建但生产环境仍然建议放在低峰期。如果是千万级以上的大表更稳妥的做法是使用在线工具分批次迁移避免一次性锁表太久。我的建议是碎片整理不要按固定周期无脑做。先通过Data_free观察实际状态同时关注是否真的出现了性能退化。有时候整理完碎片效果不明显反而白白承担了锁表风险。这个操作应该由数据说话而不是由每隔三个月做一次的惯例驱动。5.3 几条值得长期坚持的索引规范做完多个项目的索引优化之后我总结了几条自己一直在用的规范就当作分享给大家的实践清单。第一联合索引设计前先列出业务里 Top 20 的高频查询 SQL把WHERE、ORDER BY、GROUP BY涉及的列全部标记出来再进行合并设计。不要每看到一个 SQL 就加一个索引那只会让索引越堆越多。第二每一个索引都要能说清楚它服务了哪几条查询。如果说不清楚大概率是冗余索引。我在代码评审时经常问这一句能挡住不少无效索引的产生。第三写 SQL 时保持简单直白。避免在索引列上做函数运算、避免隐式类型转换、避免 OR 强行连接多个不相关的过滤条件、避免前模糊匹配。这些习惯一旦养成很多性能问题根本不会出现。第四索引变更一定要走先 EXPLAIN、再变更、再 EXPLAIN、再压测的流程。上线前用模拟数据量验证执行计划是很多人忽略的一步——测试库数据量太小优化器可能选择全表扫描导致你根本无法判断索引是否真的生效。最好的做法是在测试环境灌入和生产规模相近的数据或者至少在压测阶段使用接近真实的数据量。第五定期清理无效索引和碎片。每季度做一次索引盘点结合慢查询日志和 information_schema.statistics 检查是否存在冗余索引、是否有索引长期未被使用。监控慢查询日志的变化趋势比定期扫描所有 SQL 更高效。老实说索引这门功夫没什么高深莫测的秘诀核心就是理解数据结构、理解优化器、理解数据分布然后在三者之间找到一个平衡点。我见过不少团队把索引文档写得天花乱坠实际线上还是一堆慢查询问题往往出在设计靠脑补、验证靠感觉。如果你能养成每个慢查询都打开 EXPLAIN 看一眼的习惯把每个索引都当作有名字、有职责、有生命周期的数据对象来管理MySQL 索引这块基本上就不会再给你添乱了。