MySQL 索引调优不是靠背几条规范就能掌握的技能,它要求你同时理解索引的底层存储结构、优化器的选择逻辑,以及具体 SQL 的真实执行路径。这篇文章直接把“调优”和“面试”两条线合并起来讲:先建立索引体系的完整认知,再用可复现的建表、EXPLAIN、慢查询流程走一遍优化闭环,最后落到高频面试题的回答框架上。
如果你正在准备后端面试,或者手上有一条 SQL 越跑越慢、EXPLAIN 又看不懂,这篇内容可以对照着用。文章默认你已经掌握基础 SQL,所有操作示例基于 MySQL 8.0 编写,其中绝大部分语句在 MySQL 5.7 同样适用,个别差异点我会单独标注出来。
1. 核心能力速览:这篇索引调优文章覆盖什么
| 能力项 | 说明 |
|---|---|
| 适用数据库 | MySQL 5.7 / 8.0,InnoDB 存储引擎 |
| 核心内容 | 索引类型、B+ 树原理、EXPLAIN 执行计划、索引失效场景、慢查询分析 |
| 实操工具 | mysql 命令行、EXPLAIN、慢查询日志、performance_schema、Docker |
| 面试覆盖 | 回表、覆盖索引、最左前缀原则、索引下推、深分页、大表建索引 |
| 前置要求 | 熟悉基础 SQL,能连接本地 MySQL |
| 适合读者 | 后端开发、DBA、准备数据库面试的工程师 |
从使用场景上看,这套内容既能支撑你完成一次线上慢 SQL 的排查,也能帮你把“索引相关面试题”串成体系。与其零散地记“索引失效条件”,不如先理解优化器怎么选索引,这样不管 SQL 怎么写,你都能判断它会不会走索引。
2. MySQL 索引类型与适用场景
2.1 为什么 InnoDB 索引选择 B+ 树
InnoDB 的索引底层是 B+ 树,这个结论几乎所有文章都会写,但面试真正考察的是“为什么”。
B+ 树和普通 B 树最大的区别在于:B+ 树的非叶子节点只存索引键值,不存数据行,一个 16KB 的页面能存放更多索引条目,整棵树的高度通常只有 2 到 4 层。对一次查询而言,树高基本决定了磁盘 IO 次数,树越矮,随机 IO 越少,查询越快。
同时,B+ 树的叶子节点通过双向链表串联,天然支持高效的范围扫描。数据库里WHERE id BETWEEN 100 AND 200、按索引排序这类操作非常频繁,B+ 树的这个特性正好匹配。红黑树虽然也是平衡树,但树高明显更高,节点存储密度低,磁盘 IO 次数更多,不适合作为磁盘存储结构的底层实现。
2.2 InnoDB 索引分类
MySQL 索引可以按多个维度分类,先用一张表理清基本概念:
| 分类维度 | 类型 | 说明 |
|---|---|---|
| 数据结构 | B+ 树索引 | InnoDB 默认索引结构 |
| 数据结构 | Hash 索引 | 仅 Memory 引擎默认支持,InnoDB 的 Adaptive Hash Index 是自动行为 |
| 聚簇属性 | 聚簇索引 | 主键索引,叶子节点存放整行数据 |
| 聚簇属性 | 二级索引 | 非主键索引,叶子节点存放主键值 |
| 字段数量 | 单列索引 / 联合索引 | 联合索引涉及最左前缀原则 |
| 唯一性 | 普通索引 / 唯一索引 | 唯一索引约束字段值不重复 |
| 特殊场景 | 全文索引 | 适用于全文检索,InnoDB 支持但使用限制较多 |
| 特殊场景 | 前缀索引 | 只对字符串前 N 个字符建索引 |
大多数业务场景下,我们使用最多的是 B+ 树下的聚簇索引和二级索引。聚簇索引以主键为键值,二级索引的叶子节点存的是主键值,这就引出了下图这条完整链路:通过二级索引查找数据时,先拿到主键,再到聚簇索引回表取整行。
2.3 回表与覆盖索引
假设订单表orders有字段id、user_id、order_no、amount,主键是id。我们给order_no建了一个普通索引,执行:
SELECT id, user_id, amount FROM orders WHERE order_no = 'NO20260001';这条 SQL 的查询条件走二级索引,但二级索引的叶子节点只保存order_no和主键id,不包含user_id、amount,所以 MySQL 需要先用二级索引找到主键id,再通过id去聚簇索引回表读取完整数据行,这个过程就叫回表。
回表不是错误,它是二级索引的正常工作机制。优化思路是尽可能避免回表:如果查询列本身就在索引中,那么索引扫描完成后直接返回结果,不需要再查聚簇索引,这种场景叫做覆盖索引。上面这条 SQL 如果改成只查id和order_no,二级索引就能直接覆盖,Extra 字段会显示Using index。
3. 本地测试环境准备
调优不能只在脑内推演,建议准备一个本地 MySQL 实例,用小数据量跑通 EXPLAIN 和慢查询流程。下面给出一套通用准备流程,所有命令都需要根据你本机实际情况调整端口和密码。
3.1 通过 Docker 快速启动 MySQL 8.0
如果你本机没有 MySQL,用 Docker 启动是最快的隔离方案:
docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=your_password \ mysql:8.0启动后等待几十秒,容器进入 healthy 状态就能连接。如果你不习惯用容器,也可以直接用 MySQL 官方安装包或系统包管理器安装,核心思路是一样的:拿到一个可执行 SQL 的数据库环境。
检查容器状态:
docker ps | grep mysql8连接数据库:
mysql -h127.0.0.1 -P3306 -uroot -p3.2 准备一张订单表和测试数据
为了演示索引效果,这里准备一张结构简单、但能覆盖大多数索引场景的订单表:
CREATE DATABASE IF NOT EXISTS demo_db DEFAULT CHARSET utf8mb4; USE demo_db; CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, order_no VARCHAR(64) NOT NULL, status TINYINT NOT NULL DEFAULT 0, amount DECIMAL(12, 2) NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_user_created (user_id, created_at), UNIQUE KEY uk_order_no (order_no) ) ENGINE=InnoDB;如果想观察真实的数据量和执行时间,可以用存储过程插入几十万行测试数据。注意,演示环境插入的数据分布是否均匀,会直接影响优化器的索引选择判断。
DELIMITER $$ CREATE PROCEDURE insert_orders() BEGIN DECLARE i INT DEFAULT 1; WHILE i <= 200000 DO INSERT INTO orders (user_id, order_no, status, amount, created_at) VALUES (i % 5000, CONCAT('NO', LPAD(i, 10, '0')), i % 4, i % 10000, NOW() - INTERVAL i MINUTE); SET i = i + 1; END WHILE; END$$ DELIMITER ; CALL insert_orders();这个表同时包含普通联合索引和唯一索引,足够演示最左前缀、回表、覆盖索引等核心概念。
4. 索引创建与基本操作
4.1 查看已有索引
SHOW INDEX FROM orders;执行结果里会列出索引名称、索引字段、唯一性、基数等关键信息。Cardinality表示索引的区分度估算值,数值越高,说明重复值越少,索引选择性越好。
4.2 创建相关索引
创建普通索引:
CREATE INDEX idx_status ON orders(status);创建唯一索引:
CREATE UNIQUE INDEX uk_user_order ON orders(user_id, order_no);用 ALTER TABLE 方式也可以:
ALTER TABLE orders ADD INDEX idx_amount (amount);索引不是越多越好。写操作会同时维护所有索引,索引过多会明显拖慢 INSERT、UPDATE、DELETE 的性能,同时占用更多磁盘空间。给一个字段建索引之前,先确认它是否真的处于高频查询条件或高频排序字段中。
4.3 删除索引
DROP INDEX idx_status ON orders;删除长期不用的索引属于常规清理操作。判断索引是否冗余,一是看有没有完全被别的联合索引覆盖的前缀字段,二是看真实业务查询里是否长期没有被优化器选中的日志记录。
4.4 联合索引的字段顺序
联合索引的字段顺序非常关键,它决定这个索引能覆盖哪些查询。(user_id, created_at)这个索引可以支撑:
WHERE user_id = 123 AND created_at > '2026-01-01'但它不能直接支撑只查询created_at的 SQL,因为created_at不是联合索引的最左前缀字段。后文面试部分会详细展开最左前缀原则,这里先记住:联合索引的字段顺序应该按照查询条件的频率和区分度来设计,而不是随意拼接。
5. 用 EXPLAIN 看索引是否真的生效
创建索引之后,验证生效的方法是看执行计划。EXPLAIN 能告诉我们优化器最终选择的访问路径,这是索引调优的第一步,也是最重要的一步。
5.1 最基础的调优闭环
先不加任何条件地查看一条查询的执行计划:
EXPLAIN SELECT * FROM orders WHERE order_no = 'NO2026000001';因为order_no上有唯一索引uk_order_no,执行计划里type应该是const或ref,key字段显示uk_order_no,这说明走了唯一索引,效率很高。
为了看到对比,我们可以模拟一个没有索引的查询字段。给status建索引之前,先执行:
EXPLAIN SELECT * FROM orders WHERE status = 1;此时大概率看到type = ALL,说明优化器选择了全表扫描。接着创建索引:
CREATE INDEX idx_status ON orders(status); EXPLAIN SELECT * FROM orders WHERE status = 1;创建索引后,type会从ALL变成ref,key变成idx_status。这就是一个完整的最小调优闭环:建索引前后用 EXPLAIN 对比,用执行计划验证索引是否被使用。
5.2 EXPLAIN 核心字段解读
EXPLAIN 输出字段很多,调优时重点看这几列:
| 字段 | 含义 | 重要关注点 |
|---|---|---|
| type | 访问类型 | 从好到差:system > const > eq_ref > ref > range > index > ALL |
| possible_keys | 优化器候选索引 | 表示可能有用的索引 |
| key | 最终选择的索引 | 显示 NULL 说明没走索引 |
| rows | 预估扫描行数 | 数值越小通常越好 |
| Extra | 额外信息 | 是否出现 Using filesort、Using temporary、Using index |
type是最直观的判断依据。ALL是全表扫描,通常需要优化;index表示扫描了整棵索引树,虽然比 ALL 好,但仍然可能遍历大量数据;range表示范围扫描,比如>、<、BETWEEN、IN这类查询,属于正常范围;ref和const表示高价值的等值匹配,是大多数点查能达到的理想状态。
需要注意,EXPLAIN 的rows是基于统计信息的预估值,不是实际扫描行数。如果rows与实际差异巨大,往往说明表统计信息过期,可以执行ANALYZE TABLE orders;更新统计信息后再观察。
5.3 关注 Extra 字段
Extra字段包含大量调优线索:
Using index:覆盖索引扫描,不回表。Using index condition:Index Condition Pushdown,索引下推生效,先过滤索引中已有字段,减少回表次数。Using where:存储引擎返回记录后,Server 层又做了条件过滤。Using filesort:无法利用索引完成排序,需要额外排序操作,常见于 ORDER BY 字段不在索引中。Using temporary:使用了临时表,常见于 GROUP BY、DISTINCT 等操作。Using join buffer:批量连接缓冲,常见于多表关联时没有走索引。
面试中如果被问到“这条 SQL 为什么慢”,第一条思路就是看type和Extra。ALL + Using filesort的组合,基本就是典型的全表扫描加额外排序,优化方向很明确。
6. 索引失效场景与 SQL 写法避坑
索引建了但不走,比没建索引更让人难受。下面这些场景是导致索引失效的高频原因,每一条都可以用 EXPLAIN 实测验证。
6.1 对索引列使用函数或计算
-- 索引失效 EXPLAIN SELECT * FROM orders WHERE DATE(created_at) = '2026-06-01';date()函数作用在索引列上,优化器无法直接使用 B+ 树定位,只能全量扫描。写法改成范围查询:
-- 可走索引 EXPLAIN SELECT * FROM orders WHERE created_at >= '2026-06-01 00:00:00' AND created_at < '2026-06-02 00:00:00';原则是:不要让索引列参与任何函数运算和算术运算。这包括DATE()、YEAR()、MONTH()、SUBSTRING()、LENGTH()等。
6.2 隐式类型转换
如果user_id是 BIGINT,却用字符串去匹配日期字段,MySQL 会对字段做隐式转换,导致索引失效:
-- 假设 order_no 是 VARCHAR EXPLAIN SELECT * FROM orders WHERE order_no = 2026000001;字符串字段用数字匹配时,优化器会把字符串转换成数字比较,通常在索引列上发生转换,导致索引无法正常匹配。反过来,数字字段用字符串匹配也要警惕。最稳妥的办法是保持数据类型一致,代码和 SQL 都按表结构传参。
6.3 LIKE 以通配符开头
-- 索引失效 EXPLAIN SELECT * FROM orders WHERE order_no LIKE '%NO2026%'; -- 可走索引 EXPLAIN SELECT * FROM orders WHERE order_no LIKE 'NO2026%';前缀通配会导致优化器无法从 B+ 树按顺序定位,只能扫描全部索引或全表。需要模糊搜索时,要么改成前缀匹配,要么考虑 ES 等专业检索引擎。
6.4 OR 条件包含非索引列
-- 如果 status 有索引,user_id 没有,这个查询无法充分利用索引 EXPLAIN SELECT * FROM orders WHERE status = 1 OR user_id = 123;OR 两侧只要有一个字段没有索引,优化器就只能放弃索引选择权,改为全表扫描后逐行过滤。改用 UNION 拆开:
SELECT * FROM orders WHERE status = 1 UNION ALL SELECT * FROM orders WHERE user_id = 123;当两侧都有索引时,MySQL 也可能使用 Index Merge 优化,但这依赖优化器成本判断,不能把所有希望都寄托在特殊优化上。
6.5 范围查询会导致右侧联合索引列失效
联合索引(user_id, created_at)中,如果user_id使用了>或<这类范围条件,索引列右侧的created_at往往无法继续用于精确定位。最经典的场景:
EXPLAIN SELECT * FROM orders WHERE user_id > 100 AND created_at > '2026-01-01';user_id > 100之后,created_at列只能作为 Filter 过滤条件,无法继续作为索引检索条件。所以联合索引的字段设计要把等值查询列放在前面,范围查询列放在后面。
6.6 NOT IN、NOT BETWEEN 等否定条件
NOT IN、NOT EXISTS、<>这类否定条件通常不容易走索引,原因是优化器认为需要扫描的数据范围过大,选择全表扫描成本更低。实际是否失效取决于数据分布和版本,最稳的方法是直接用 EXPLAIN 验证,不要凭经验下结论。
6.7 排序与分组字段不满足索引顺序
ORDER BY created_at能否走索引,取决于 created_at 是否出现在某个索引的最左前缀序列中。如果联合索引是(user_id, created_at),执行:
EXPLAIN SELECT * FROM orders WHERE user_id = 1 ORDER BY created_at;此时排序可以用到索引的有序性,Extra 不会出现Using filesort。但如果你直接ORDER BY created_at,由于最左前缀缺失,索引无法直接提供有序结果,就会产生文件排序。
7. 慢查询日志与性能观察
EXPLAIN 解决的是“是否走索引”的问题,慢查询日志解决的是“哪些 SQL 需要调优”的问题。
7.1 开启慢查询日志
MySQL 默认可能没有开启慢查询日志,可以动态开启:
SET GLOBAL slow_query_log = ON; SET GLOBAL long_query_time = 1; SET GLOBAL slow_query_log_file = '/var/log/mysql/mysql-slow.log';long_query_time表示超过多少秒的记录到慢日志,单位是秒。开发环境建议设置为 1 秒,生产环境则要根据业务压测结果调整,避免日志量过大。
查看是否生效:
SHOW VARIABLES LIKE 'slow_query_log'; SHOW VARIABLES LIKE 'long_query_time';7.2 分析慢查询日志
最简单的分析工具是mysqldumpslow:
mysqldumpslow -t 10 /var/log/mysql/mysql-slow.log它会输出执行次数最多或耗时最长的 Top N 条 SQL。更专业的工具是pt-query-digest,能按维度聚合 SQL,输出响应时间占比、调用次数、锁等待时间等,适合批量分析。
慢查询日志中的每一条记录包含 Query_time、Lock_time、Rows_sent、Rows_examined 等字段。判断一条慢 SQL 是否有价值,不仅要看 Query_time,还要看 Rows_examined 与 Rows_sent 的比率。扫描 10 万行返回 10 行,说明查询选择性差,需要重点优化;扫描 20 行返回 20 行,说明问题可能不在 SQL,而在整体负载。
7.3 在线查看性能视图
MySQL 提供了 performance_schema 和 sys 库,可以直接查询 SQL 统计:
SELECT SCHEMA_NAME, DIGEST_TEXT, COUNT_STAR, AVG_TIMER_WAIT / 1000000000 AS avg_ms FROM performance_schema.events_statements_summary_by_digest ORDER BY AVG_TIMER_WAIT DESC LIMIT 10;这条语句能快速找出当前实例中平均响应时间最长的 SQL 语句。观察索引调优效果时,可以用同一组 SQL 在优化前后对比COUNT_STAR、AVG_TIMER_WAIT和ROWS_EXAMINED_AVG的变化。
7.4 资源占用观察
索引调优不仅是查询速度的问题,也要关注空间与内存占用。B+ 树索引需要磁盘空间,也会占用 InnoDB Buffer Pool。索引过多时,即使查询变快,也可能因为缓冲池命中率下降导致整体性能受损。
观察索引大小可以使用:
SELECT TABLE_NAME, INDEX_LENGTH, DATA_LENGTH FROM information_schema.TABLES WHERE TABLE_SCHEMA = 'demo_db' AND TABLE_NAME = 'orders';INDEX_LENGTH是所有索引占用的空间;DATA_LENGTH是数据行占用的空间。如果索引占用已经接近甚至超过数据空间,就需要评估是否有大量冗余索引。
8. 高频 MySQL 面试题与回答思路
这一章节把 MySQL 面试里出现频率最高的索引相关问题集中梳理一遍,每道题都给出回答思路和关键得分点。
8.1 为什么 InnoDB 用 B+ 树,而不是 B 树或红黑树?
回答结构拆成三点:磁盘 IO、范围查询、树高。
第一,B+ 树非叶子节点不存数据,只存索引键,每个磁盘页能存储的索引条目远多于 B 树,树高更低。一次索引查找对应一次磁盘 IO,树高直接影响查询速度。第二,B+ 树叶子节点用链表串联,范围查询和排序可以直接沿着链表顺序扫描,B 树则可能涉及回溯到父节点或兄弟节点,效率更低。第三,红黑树虽然平衡性好,但每个节点只存一个键值,高度太高,磁盘 IO 次数不可接受,它主要适用于内存数据结构。
8.2 什么是回表?如何避免?
回表是二级索引查出主键后再去聚簇索引取完整数据行的过程。避免回表的直接手段是覆盖索引,让查询所需字段全部包含在索引中。比如查询只涉及主键和order_no,uk_order_no索引就能直接覆盖。面试时如果能补充一句“回表不是设计缺陷,而是二级索引的正常机制,目标不是消除回表,而是减少无效回表”,会很加分。
8.3 最左前缀原则是什么?
联合索引(a, b, c)相当于建立了a、(a, b)、(a, b, c)三个前缀索引。查询条件必须从最左列开始,才能利用联合索引排序和检索。WHERE b = 1 AND c = 1无法命中这个索引,因为缺失a。
回答时可以举例说明设计影响:当需要频繁用b字段单独查询时,不能只依赖(a, b, c),需要额外为b建索引,这就是“联合索引不能替代所有单列索引”的原因。
8.4 索引下推 Index Condition Pushdown 是什么?
索引下推是 MySQL 5.6 引入的优化。联合索引(user_id, created_at)查询条件包含user_id = 1 AND created_at > '2026-01-01'时,在没有 ICP 的情况下,优化器会先根据user_id = 1查出所有主键,再回表逐行过滤created_at。ICP 允许在存储引擎层直接对索引中的created_at字段进行初步过滤,只对满足条件的记录回表,减少 IO 次数。EXPLAIN Extra 显示Using index condition即代表生效。
8.5 深分页查询怎么优化?
深分页慢的根本原因是 MySQL 需要扫描并丢弃前面的大量有效行。经典写法:
SELECT * FROM orders ORDER BY id LIMIT 100000, 20;这个查询需要扫描 100020 行,然后丢弃前 100000 行。优化方式包括:使用延迟关联,或基于覆盖索引定位起始点。
基于 id 范围的分页示例:
SELECT * FROM orders WHERE id > (SELECT id FROM orders ORDER BY id LIMIT 100000, 1) ORDER BY id LIMIT 20;子查询先通过覆盖索引拿到偏移位置的 id,再用主键范围查询取数据,避免大偏移扫描。另一种思路是业务上改为游标分页,记住上一页的最后一条主键,下一次直接WHERE id > last_id。
8.6 大表加索引有什么风险?
大表加索引会面临两类风险:锁表时间和资源消耗。MySQL 8.0 支持原子 DDL,同时在很多场景下可以使用 Online DDL,不会长时间阻塞 DML,但具体是否在线取决于操作类型和字段。低峰期执行、先备份、评估磁盘空间、观察主从延迟,是上线前必做的四件事。如果表非常大,可以考虑 gh-ost、gh-ost 类工具配合业务空窗期执行。
8.7 联合索引字段顺序如何设计?
没有绝对公式,但有三个通用原则:等值查询列优先、区分度高的列靠前、考虑排序与分组字段。等值条件能稳定命中最左前缀,区分度高的字段能更快缩小扫描范围。当查询既有过滤又有排序时,可以让索引同时覆盖过滤列和排序列,避免Using filesort。真正草率的设计是把所有查询条件随机拼进联合索引,看起来很全,实际哪条查询都无法高效命中。
9. 常见问题排查对照表
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 建了索引但 EXPLAIN 显示全表扫描 | 索引列发生隐式转换或函数运算 | 查看 EXPLAIN key 字段是否为 NULL | 改写 SQL,保持字段类型一致 |
| SQL 查询时间波动大 | 统计信息过期、缓冲池冷启动、索引选择错误 | 执行 ANALYZE TABLE,对比多次执行时间 | 更新统计信息,必要时用 FORCE INDEX 验证是否索引选择问题 |
| 慢查询日志没有记录 | long_query_time 阈值过高或慢日志未开启 | SHOW VARIABLES LIKE 'slow_query_log' | 动态开启并设置合理阈值 |
| 分页很深时响应变慢 | 大偏移导致扫描大量无用行 | 查看 Rows_examined | 改为基于 id 范围或游标分页 |
| 联合索引未被使用 | 查询条件不满足最左前缀原则 | 检查查询条件的字段顺序 | 调整联合索引顺序或拆分索引 |
| 覆盖索引没有生效 | 查询列超出索引字段 | 查看 Extra 是否出现 Using index | 确认查询列是否全部在索引中 |
| 排序字段导致文件排序 | ORDER BY 字段不在索引序列内 | 查看 Extra 是否出现 Using filesort | 让排序列加入联合索引并满足前缀顺序 |
| 写入性能下降明显 | 冗余索引过多 | 查看 SHOW INDEX 重复索引 | 删除重复或长期未使用的索引 |
10. 索引调优最佳实践
第一,把 EXPLAIN 作为索引变更的验收标准。任何一次加索引、改 SQL、调整联合索引字段顺序,都要用优化前后的 EXPLAIN 和执行时间对比来验证效果。
第二,统一索引命名规范。主键索引一般不额外命名,普通索引使用idx_字段名,联合索引用idx_字段1_字段2,唯一索引使用uk_字段名。规范命名能让你半年后回看表结构时不用猜。
第三,验证索引区分度。创建索引前先算一下字段的区分度:
SELECT COUNT(DISTINCT status) / COUNT(*) AS selectivity FROM orders;选择性接近 1 说明字段值几乎全不同,适合建索引;选择性接近 0 说明大量重复,建索引的意义很小,全表扫描反而更快。例如订单状态字段通常只有几个枚举值,单独建索引的效果有限,更适合放在联合索引中。
第四,联合索引字段顺序按“等值查询列优先、区分度高优先、范围列最后”设计,同时把 ORDER BY、GROUP BY 字段纳入考虑,避免额外排序。
第五,控制单表索引数量。常规业务表建议保持 5 个以内索引,写入频繁的表更要克制。索引不是越多越好,每一次写入都要同步维护所有索引节点。
第六,测试环境验证后再上生产。先在测试库压一份接近生产数据量的数据,用 EXPLAIN 和慢查询日志对比前后差异,尽量选择业务低峰期执行 DDL。涉及线上表结构调整时要有备份和回滚方案。
11. 总结与下一步
这篇文章从索引存储结构讲到 EXPLAIN 分析,再到慢查询定位和面试答案,核心是帮你建立一条完整的索引调优链路:先看type、key、rows、Extra判断是否走索引,再根据Using filesort、Using temporary定位额外开销,最后用慢查询日志找出真正需要优先优化的 SQL。
最先应该做的事,是把文中订单表建出来,亲手执行几遍带索引和不带索引的 EXPLAIN,重点观察type从ALL到ref的变化,以及Extra中Using index condition和Using index的区别。最容易踩的坑是隐式类型转换和最左前缀失效,这两类场景在面试和线上排查中都特别高频,建议用不同字段类型反复验证。
后续可以继续拓展的方向:阅读 MySQL 官方文档中 Optimizer 章节,学习OPTIMIZER_TRACE的详细输出;实践 online DDL 在真实业务表上的操作流程;再进阶一点,可以研究 InnoDB Buffer Pool 命中率、redo log 刷盘策略对整体性能的影响。索引调优是一个需要持续用数据验证的过程,把 EXPLAIN 和慢查询日志用熟,后面的路会顺很多。