上周一个同事拿着一组慢查询日志来找我,他说:“where 里的字段我都建了索引,为什么这条 SQL 还是慢?”我打开执行计划看了一眼,问题其实很清楚:查询确实走了索引,但 Extra 里有 Using temporary 和 Using filesort,而且二级索引返回了一大批主键,后面又做了大量回表。真正的问题不是没有索引,而是索引设计和这条查询的实际访问路径根本不匹配。
这个场景其实很常见。很多人学 MySQL 索引,记住了 B+树、最左前缀、索引下推这些名词,也知道用 EXPLAIN 看执行计划,但真到线上慢 SQL 冒出来时,依然不知道从哪下手。原因不是知识量不够,而是这些知识被学成了零散的“考点”,没有连成一条完整的链路。
从 B+树到联合索引,从执行计划到 SQL 改写,再到业务场景里的取舍,这是一条完整的链路。索引不是加速一切查询的银弹,它本质上是用来减少数据扫描范围的有序结构;联合索引也不是把几个列机械地拼在一起,而是按照最左前缀原则建立的多列排序结构;EXPLAIN 不是用来背输出结果的,而是用来观察一条 SQL 真实访问路径的工具。只有把这条链路走通,才能看懂慢 SQL,也才能回答那些高频 MySQL 面试题。
接下来,我就按这条链路讲清楚。版本会迭代,参数名会变,但索引设计、访问路径、成本判断这些底层逻辑,短时间内不会变。
1. 先放下背答案,搞清楚索引到底在解决什么问题
1.1 从全表扫描到有序查找:索引的本质是减少扫描范围
一张表在没有索引的时候,要查某一条或某一个范围的数据,大概率只能把整张表扫一遍。我们把这种访问方式叫全表扫描。全表扫描不一定慢,表很小、数据量在几千行的时候,顺序扫描反而比走索引更可控,因为索引访问涉及多次随机 IO 和额外的结构读取。
但表一旦增长到百万、千万行,全表扫描的成本就会迅速放大。因为数据库并不知道你要找的数据在哪里,它只能把所有行都读出来,再逐行判断条件是否匹配。索引做的事情,是建立一个从“索引值”到“数据位置”的有序映射,让查找过程从“一行一行碰运气”变成“先缩小范围,再精准定位”。
很多人的误区是:只要建了索引,查询就一定快。实际上索引解决的是“减少扫描范围”的问题,不是“减少返回数据量”的问题。如果一条查询本身要返回全表的大部分行,优化器很可能会选择全表扫描,因为这时顺序读比通过索引多次随机读更划算。索引不是越多越好,而是要和你真实的查询模式匹配。
1.2 哈希、二叉树、红黑树、B+树:为什么 InnoDB 选择 B+树
MySQL 存储引擎可选的数据结构不止一种。常见的有哈希结构、二叉排序树、红黑树、B 树和 B+树。为什么 InnoDB 的默认索引结构是 B+树?我们可以从磁盘 IO 和查询模式两个角度理解。
哈希结构做等值查询非常快,时间复杂度接近 O(1),但它有两个明显问题:不支持范围查询,也不支持按顺序访问。比如where age >= 18 and age <= 30,哈希结构就帮不上忙。InnoDB 里的自适应哈希索引只是用来加速等值查询的辅助结构,不能替代 B+树。
普通二叉排序树在最坏情况下会退化成链表,树的高度无法控制。红黑树虽然是平衡树,能避免退化,但每个节点只保存一个键,树的高度依然随着数据量增长变得很深。对磁盘数据库来说,每往下走一层,往往就意味着一次额外的磁盘 IO。树越高,IO 次数越多。
B+树的核心优势在于:内部节点不存数据行,只存索引键和指针,单个页面能容纳更多键,树高更矮。在常见的页大小和主键长度设定下,三层到四层的 B+树就能完成千万行量级的索引定位。再加上叶子节点之间通过链表按顺序连接,范围查询和排序扫描都变得很自然。
| 结构 | 等值查询 | 范围查询 | 树高/IO 成本 | 适合场景 |
|---|---|---|---|---|
| 哈希 | 快 | 不支持 | 低 | 内存临时表、等值匹配 |
| 普通二叉树 | 不稳定 | 支持 | 可能退化 | 教学演示 |
| 红黑树 | 稳定 | 支持 | 树高仍偏大 | 内存数据结构 |
| B 树 | 支持 | 支持 | 内部节点也存数据,单页可容纳键数少一些 | 部分数据库索引 |
| B+树 | 支持 | 支持 | 内节点只存键,树矮,范围扫描友好 | InnoDB 索引 |
1.3 聚簇索引与二级索引:一张表到底是怎么“长”出来的
InnoDB 表本身就是一个按主键组织的 B+树,这个结构叫聚簇索引。聚簇索引的叶子节点直接保存整行数据。也就是说,只要通过主键定位到叶子节点,就可以一次拿到完整记录。这张表如果没有显式定义主键,InnoDB 会选择一个唯一非空索引来作为聚簇索引;如果也没有,它会生成一个不可见的内部主键。
二级索引看起来是另一棵 B+树,但它的叶子节点不保存完整行数据,只保存索引列和主键值。查询如果用了二级索引,通常还要拿着主键回到聚簇索引里取完整行,这个过程叫回表。回表的成本取决于回表次数和主键是否随机。如果一条查询通过二级索引命中了 10 万行,那大概率要回表 10 万次,性能自然好不到哪去。
这也是为什么主键设计对 InnoDB 表特别重要。主键应该尽量短、稳定、自增或有序。自增主键写入时基本是顺序追加,页分裂概率低。如果你用 UUID 或很长的随机字符串做主键,二级索引叶子节点里占用的空间会变大,写入时还可能引起随机页分裂,最终放大写放大和索引维护成本。
2. 联合索引不是“把几个字段建在一起”那么简单
2.1 最左前缀:联合索引的第一性原理
很多初学者觉得,创建联合索引(a, b, c)之后,只要 where 条件里包含 a、b、c 任意几个字段,索引就能生效。这个理解是错的。
联合索引的底层结构是多列排序。你可以把它想象成先按 a 排序,a 相同再按 b 排序,b 相同再按 c 排序。因此,只有从最左边的列开始连续匹配,索引的有序性才能被利用。这就是最左前缀原则。
举个例子,索引(user_id, created_at)可以高效支持下面这些查询:
WHERE user_id = 1001 WHERE user_id = 1001 AND created_at >= '2025-01-01'但如果查询条件是:
WHERE created_at >= '2025-01-01'它没有从user_id开始,优化器通常无法利用这个联合索引的有序前缀。在 MySQL 8.0 的某些场景下可能出现 Skip Scan,但条件限制很多,不能当作通用方案。
还有一个常见问题是范围条件。where a = 1 and b > 100 and c = 5这种写法,b 用了范围条件后,c 的有序性往往就没那么容易被利用,因为范围条件破坏了后续字段的连续定位。更稳妥的做法是把等值条件放在联合索引前面,范围条件放后面,并且提前确认执行计划。
2.2 索引下推:一次“提前过滤”的优化
索引下推(Index Condition Pushdown,ICP)是 MySQL 5.6 开始支持的一项优化。它做的事,是把 where 条件中部分能用索引列判断的过滤条件下推到存储引擎层,让存储引擎在读取二级索引时就先过滤掉不符合条件的记录,减少回表次数。
用一个例子理解。假设有联合索引(name, age),执行:
SELECT * FROM user WHERE name LIKE '张%' AND age = 20;没有索引下推时,流程大概是:先用name LIKE '张%'从索引里找到一批主键,然后回表取完整行,再在服务层用age = 20过滤。
开启索引下推后,存储引擎在二级索引上同时判断age = 20,先把明显不满足条件的记录过滤掉,回表数量会明显减少。
看执行计划时,如果 Extra 里出现Using index condition,通常说明这条 SQL 正在使用索引下推。这项优化不是所有查询都能触发,它要求过滤条件涉及的列也在索引中。但它给了我们一个启发:联合索引里多包含几个常用过滤列,不只是在帮排序和覆盖,有时也能让过滤发生得更早。
2.3 覆盖索引与回表:查询性能的差距往往藏在这里
覆盖索引指的是:某棵索引已经包含了查询需要的全部列,查询过程中不需要回表。比如 idx 是(user_id, created_at),执行:
SELECT user_id, created_at FROM orders WHERE user_id = 1001;二级索引的叶子节点里本来就有user_id和created_at,查询可以直接从索引返回结果,不再回聚簇索引取整行。这种情况下 Extra 通常会显示Using index。
覆盖索引的价值不是“少一次操作”这么简单。回表意味着按主键到聚簇索引里随机读数据行,主键分布如果比较散,IO 成本会很明显。覆盖索引让很多查询变成“只扫索引页”,尤其适合高频、固定字段的查询。
但覆盖索引不是万能的。为了覆盖更多字段,联合索引越建越宽,存储和写入维护成本也会上升。如果一个表有大量高频写入,每增加一个索引列,插入和更新时都要维护同样多的索引数据。设计时要在“查询收益”和“写入成本”之间找平衡。
2.4 设计联合索引时,先回答这四个问题
我一般会建议,设计联合索引时先不要看 SQL 技巧,而是看业务访问模式。可以按下面四个问题过一遍:
| 问题 | 目的 | 落地判断 |
|---|---|---|
| 哪些条件是等值过滤? | 等值条件通常放最前,能最直接缩小范围 | user_id = ?、status = ? |
| 哪些条件是范围或排序? | 范围列放等值条件之后,排序列尽量与索引顺序一致 | created_at >= ?、ORDER BY created_at DESC |
| 查询需要返回哪些列? | 判断能否用覆盖索引减少回表 | 高频查询只返回少量列 |
| 索引维护成本能接受吗? | 避免无脑加列和加索引 | 写入频率、更新频率、索引列长度 |
一个典型例子是订单列表查询。常见业务是“查某个用户最近的订单”,这条 SQL 通常是等值user_id,再加ORDER BY created_at DESC LIMIT n。此时联合索引(user_id, created_at)会比(created_at, user_id)更贴合业务。因为前者先用user_id等值定位到索引中的一段,段内已经按created_at排好,可以直接顺序读取;后者只能先按时间定位,还要再处理不同用户之间的混合顺序。
3. 从 EXPLAIN 看透一条 SQL 是快是慢
3.1 第一次看执行计划,先看 type、rows、Extra
EXPLAIN 是分析 SQL 最常用的工具,但很多人只看key字段,发现“有索引”就放心了。实际上,看执行计划的第一步是看访问路径,而不是看有没有使用索引。
我习惯按这几个字段先做判断:
| 字段 | 关注点 | 常见信号 |
|---|---|---|
| type | 访问类型 | const/ref/range 通常优于 index/ALL |
| key | 实际使用的索引 | 是否为空、是否与预期一致 |
| rows | 优化器估算的扫描行数 | 行数过大说明访问路径还有优化空间 |
| Extra | 额外操作 | Using index 好于回表;Using filesort 需要关注;Using temporary 更要注意 |
type 的常见顺序可以简单记成:const > eq_ref > ref > range > index > ALL,顺序越靠左,通常越精确。但 type 不是唯一标准。比如全表扫描一条小表可能只需要几十毫秒,而走一个错误的索引后反复回表可能需要几百毫秒。执行计划要结合数据量、数据分布和业务场景一起看。
还要记住一点:EXPLAIN 的结果是优化器基于统计信息和成本模型估算出来的,不是实际执行结果。统计信息过旧时,执行计划可能并不适合当前数据分布。如果遇到“执行计划看起来合理,线上就是慢”的情况,可以尝试使用EXPLAIN ANALYZE观察实际执行代价,但要先确认当前版本是否支持,并且不要在核心生产环境随意全量执行。
3.2 一条实际慢查询的优化过程
假设有一张订单表:
CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, order_no VARCHAR(64) NOT NULL, status TINYINT NOT NULL, created_at DATETIME NOT NULL ) ENGINE=InnoDB;业务 SQL 是:
SELECT id, order_no, status, created_at FROM orders WHERE user_id = 1001 ORDER BY created_at DESC LIMIT 10;在没有(user_id, created_at)索引时,执行计划大概率是全表扫描,然后在内存或磁盘里做排序,Extra 里可能会出现Using filesort。即使通过单列索引user_id能找到一批行,排序依然可能需要额外完成。
加入联合索引后:
ALTER TABLE orders ADD INDEX idx_user_created (user_id, created_at);因为索引先按user_id组织,同一个用户内部已经按created_at排好,执行这条查询时,优化器可以直接定位到该用户的索引段,从尾部或头部顺序取 10 条,既减少了过滤行数,又避免了额外排序。
优化之后,执行计划里 type 会从 ALL 变为 ref 或 range,Extra 中原来的Using filesort大概率会消失。这就是一个典型的“索引设计贴合查询模式”的例子。
但要注意,如果你再查询时需要返回的列很多,比如订单详情里的所有列,即便走了idx_user_created,也可能需要回表。此时就要权衡是否在业务可以接受的范围内,用冗余列或覆盖索引来减少回表。
3.3 那些常见的“索引失效”其实不是失效
面试里经常问索引失效场景。很多人的答案背得很顺:函数、隐式转换、前导模糊、联合索引不满足最左前缀。这些说法大方向没错,但容易让人误解成“索引一碰这些操作就完全没用”。
更准确的说法是:这些操作通常破坏了 B+树可用的有序定位条件,导致优化器无法从索引叶子节点上直接做高效的范围查找,于是只能退化成扫描索引或者扫描全表。
举几个常见例子:
- 不要在索引列上做运算。比如
WHERE id + 5 = 10,优化器很难直接按id的原始顺序定位。MySQL 8.0 支持函数索引,但前提是你基于(id + 5)显式创建了函数索引,而不是直接靠普通索引硬扛。 - 注意隐式类型转换。比如一张表的
varchar_col是字符串类型,查询写成WHERE varchar_col = 100,优化器可能会把列转换成数值进行比较,导致该列上的普通索引无法按原始字符串顺序使用。 - 前导模糊匹配。
LIKE '%keyword%'无法利用 B+树的前缀有序性,因为关键字不在开头,定位不到起点。 - 联合索引跳过最左列。这是索引设计问题,不是某一条 SQL 单方面的问题。
还有一种情况经常被误判:查询走了索引,但 Extra 里有Using where。有的人看到Using where就慌了,觉得索引失效。其实这很常见,表示存储引擎返回记录后,服务层还需要用 where 条件做二次过滤。只要过滤的行数不大、回表次数可控,这类 SQL 不一定需要改。判断标准还是要回到扫描行数、回表次数和排序成本上。
4. SQL 优化与 MySQL 调优,真正该练的是这套排查链路
4.1 慢 SQL 处理的三段式流程
慢 SQL 出现时,最忌讳的就是“直接加索引”或者“直接调大 buffer”。正确的顺序应该先定位、再分析、最后优化。
第一步,定位问题。可以开启慢查询日志,或者通过 performance_schema、监控平台找到具体 SQL。记录下它出现的时间、并发情况、响应时间、扫描行数和返回行数。
第二步,分析访问路径。拿到 SQL 后不要马上改,先做三件事:
- 确认业务场景:这个查询是等值查询、范围查询,还是报表统计?它允许的响应时间是多少?
- 确认数据分布:涉及的表有多少行?过滤条件是否存在严重倾斜?比如
status字段大量行都是同一个值,单列索引可能意义不大。 - 使用 EXPLAIN 看执行计划:看 type、key、rows、Extra,判断是否存在回表、排序、临时表、全表扫描。
第三步,制定优化方案。可以按成本从低到高排列:
- 改写 SQL:减少返回字段、调整 join 顺序、优化分页方式。
- 合理调整索引:增加联合索引、覆盖索引,或者删除冗余索引。
- 调整表结构:增加冗余字段、拆分宽表、使用汇总表。
- 调整数据库配置:如 InnoDB buffer pool、排序缓冲、临时表大小。
- 业务层兜底:引入缓存、异步化、限流。
优化后必须做回归验证。不能只跑一次 EXPLAIN 觉得没问题就上生产。先在小流量环境压测,观察扫描行数、响应时间、锁等待和慢日志变化,再逐步推广。
4.2 参数、统计信息、审计与锁:不要只盯着 SQL
有些慢查询不是 SQL 写错,而是表统计信息过旧,导致优化器选了一个错误执行计划。这种情况下,可以先执行ANALYZE TABLE更新统计信息,再重新看执行计划。但这不能频繁做,因为 ANALYZE 本身也会占用资源。
还有一些慢查询和锁竞争有关。当一张表在高并发写入时,索引页的维护、聚簇索引的页分裂都可能触发锁等待,导致某些查询的延迟突然升高。SQL 本身可能没有问题,但整体负载已经让索引访问变慢。
运维侧有一个容易被忽略的点:如果开启了数据库审计功能,在写入密集的业务里,审计日志会占用 IO 和锁资源,也可能让索引页的维护和扫描竞争加剧。这种问题从单条 SQL 执行计划里看不到,只能把排查范围从 SQL 扩大到数据库整体负载、IO 延迟、锁等待和审计策略。毕竟调优不只是语法和索引层面的工作,还要看并发模型和运维开关的取舍。
4.3 用什么不做什么:调优的边界感
优化这件事,最重要的能力是知道“该停在哪里”。
索引不是越多越好。每增加一个索引,插入、更新、删除时都要同步维护,写入放大是真实成本。尤其对高频写入的 OLTP 系统,冗余索引可能比慢查询更有破坏力。
低区分度的字段不适合单独建索引。比如一个表有 100 万行,status字段只有 3 种取值,单独建索引后,每次查询依然可能返回几十万行,优化器可能直接选全表扫描。
小表不一定需要索引。几千行的表全表扫描也许只要几毫秒,强行建索引反而增加维护成本。调优要考虑收益和成本的比值。
不要盲目相信网上流传的参数调优模板。不同业务、不同并发模型、不同硬件环境下,参数和策略可能完全不同。更稳妥的顺序是:先用最小成本跑通正确性,再用监控数据验证性能,最后才考虑工程化沉淀。这也是为什么我会把“先跑通、再优化、最后工程化”当成调优的基本原则。
5. 高频面试题背后,都在考同一件事
5.1 面试题怎么答:把机制、现象、场景串起来
MySQL 面试题看起来五花八门,但频率最高的其实是有数的:为什么 InnoDB 用 B+树、主键为什么建议自增、回表是什么、覆盖索引是什么、联合索引最左前缀、索引下推、索引失效场景、慢 SQL 怎么排查。
这些问题单独背答案不算难,难的是在面试官追问时,你能把机制和场景连起来。比如问“为什么主键建议自增”,你可以说:
InnoDB 表是聚簇索引结构,数据行按主键顺序组织。自增主键写入时,新记录基本追加在 B+树当前最大键位置附近,页分裂概率低。用 UUID 作为主键时,主键长度更长,二级索引叶子节点存储的主键也更大;写入时主键随机,会导致频繁页分裂和随机 IO。所以从存储和写入稳定性两个角度看,自增主键通常更合适。
再比如问“索引下推是什么”,只背定义不够。你可以补一个完整流程:优化器把部分 where 条件下推到存储引擎层,在二级索引上提前过滤,减少回表。关键点在于,过滤条件涉及的列必须也在索引里。这样面试官知道你不是背概念,而是理解访问路径。
| 面试题 | 考察点 | 回答主线 |
|---|---|---|
| 为什么用 B+树 | 数据结构和磁盘 IO 的关系 | 树矮、范围扫描友好、数据都在叶子节点 |
| 主键为什么建议自增 | 聚簇索引的物理组织 | 顺序写入、避免页分裂、减少二级索引存储 |
| 什么是回表 | 二级索引与聚簇索引的关系 | 二级索引只存主键,取完整行需要回聚簇索引 |
| 最左前缀 | 联合索引的有序结构 | 跳过最左列会破坏排序定位 |
| 索引下推 | 二级索引上的提前过滤 | 在存储引擎层过滤,减少回表 |
| 慢 SQL 排查 | 分析链路和优化思路 | 定位、EXPLAIN、访问路径、索引调整、验证 |
5.2 一个能复用的话术框架:数据结构、访问路径、场景取舍
应对 MySQL 高频问题,我建议你形成一个稳定的回答框架,而不是背标准答案。这个框架只有四步:
- 说数据结构:先讲清楚索引的底层组织方式,比如 B+树、联合索引排序方式、聚簇索引和二级索引的关系。
- 说访问路径:再解释一条 SQL 在索引结构上会经历哪些步骤,比如定位、范围扫描、回表、排序。
- 说优化动作:然后针对访问路径的瓶颈,说明索引怎么做调整、SQL 怎么改写。
- 说场景边界:最后补充这个方案在什么情况下适用,什么情况下不适用。
比如被问“一个 SQL 执行很慢,你怎么排查”,不要直接说“加索引”。你可以这样答:
“我先看这个查询是哪种访问模式,是主键精确查找、二级索引范围查找,还是需要排序或统计。接着用 EXPLAIN 看执行计划,关注 type、key、rows 和 Extra。如果发现走了二级索引但回表很多,我会看能不能用覆盖索引;如果发现 filesort,我会想能不能让联合索引覆盖排序字段。优化完成后,再用慢查询日志和压测验证是否真的变快,同时观察写入成本和索引占用,避免为了单条 SQL 牺牲整体稳定性。”
这套话术的好处是,任何一道 MySQL 面试题都能套上。因为它不背结论,而是模拟了一个真实开发者面对问题时的工作链路。
6. 回到工作现场:一次从慢 SQL 到索引设计的完整复盘
6.1 案例背景与初始状态
为了把前面讲的东西串起来,我模拟一个比较典型的线上场景。假设有一张订单流水表,已经累积到千万行量级,业务端有一个常见的“订单查询列表页”,筛选条件是某个用户、某个时间段、按创建时间倒序分页展示。
初始索引设计可能比较简单,只有一个主键索引。执行:
SELECT id, order_no, status, created_at, pay_amount FROM orders WHERE user_id = 1001 AND created_at >= '2025-01-01' AND created_at < '2025-02-01' ORDER BY created_at DESC LIMIT 20;在没有合适索引的情况下,这条 SQL 很可能要把整张表扫描一遍,再过滤和排序。即使查询条件里user_id和created_at都有,由于没有对应联合索引,优化器也很难快速缩小范围。
6.2 第一次优化:先解决最核心的过滤路径
第一步,我建议先确认这个查询最核心的过滤条件。对于订单列表页,user_id通常是等值条件,created_at是范围条件,排序也需要created_at DESC。这时候联合索引(user_id, created_at)是自然的起点。
ALTER TABLE orders ADD INDEX idx_user_created (user_id, created_at);加完索引后再看执行计划,通常会发现 type 从 ALL 变成 ref 或 range,rows 明显下降,Extra 里的 Using filesort 也可能消失。因为索引已经按user_id定位到某个用户的索引片段,片段内部又按created_at排序,B+树可以直接从时间范围边界开始扫描,不需要额外排序。
这一步不需要把所有字段都放进索引,先让访问路径从“全表扫”变成“索引范围扫”,收益往往已经很明显。
6.3 第二次优化:深分页和回表问题
第一次优化只解决了“过滤和排序”的基本问题,但在分页足够深的时候,还是会慢。问题出在 LIMIT 的 offset 越来越大。
比如:
SELECT id, order_no, status, created_at, pay_amount FROM orders WHERE user_id = 1001 AND created_at >= '2025-01-01' ORDER BY created_at DESC LIMIT 100000, 20;按照普通分页语义,数据库要先定位到第 100000 条,再往后取 20 条。即使走了二级索引,它也要先扫过前面 100000 条符合条件的索引项,并且为了返回完整行,可能还要做大量回表。这种场景下,单个联合索引只能改善一部分,真正的问题是“分页偏移量大 + 回表次数多”。
常见优化思路是“延迟关联”:先在二级索引上完成分页定位,只取主键,再用主键回表取完整行。写出来有点像这样:
SELECT o.* FROM orders o INNER JOIN ( SELECT id FROM orders WHERE user_id = 1001 AND created_at >= '2025-01-01' ORDER BY created_at DESC LIMIT 100000, 20 ) t ON o.id = t.id;子查询里只查id,如果联合索引能覆盖user_id、created_at和id,整个过程可以只在索引里完成,避免提前回表。外层再按这 20 个主键去取完整数据,回表次数被压到很小。
这里必须强调,这种写法不是所有场景都一定更快。如果表结构、数据分布、索引设计不同,优化器可能会有不同表现。正确做法是把优化前后的 SQL 都做 EXPLAIN,再结合实际执行时间验证。
6.4 落地点:把优化固化成流程
这个案例做完后,不要急着把旧索引删掉,也不要因为一两条 SQL 就无限加索引。更稳妥的做法是:
- 保存优化前后的执行计划和响应时间记录。
- 检查还有没有其他相似查询会命中同一个联合索引。
- 观察慢查询日志和索引使用情况,确认新索引被真实使用。
- 定期更新统计信息,防止优化器走向意外路径。
- 在代码评审和 SQL 上线环节,把“检查执行计划”变成常规动作。
这样一次优化就不再是救火,而是沉淀成团队里的索引设计清单。下次再遇到慢 SQL,你不需要从零开始试,而是先按访问路径分析,再验证方案,最后固化流程。
说到底,MySQL 索引和调优并不是一堆技巧的堆砌。真正值得花时间的,是把数据结构、访问路径、执行计划和业务场景连成一条完整的链路。你可以先建一张测试表,造几百万行数据,写几条低效 SQL,用 EXPLAIN 一条条观察访问路径,再亲手改成高效写法。等你亲眼看到优化器是怎么选的、回表为什么慢、覆盖索引为什么快,那些面试题和线上故障,本质上就变成同一个问题:你有没有真正理解数据访问路径。```