ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

千万级MySQL索引调优:从B+树原理到BufferPool实战

2026/9/2 7:56:51 拓冰建站 浏览量
千万级MySQL索引调优:从B+树原理到BufferPool实战 千万级 MySQL 索引是 Java 后端面试里绕不开的硬话题。数据量一旦涨到千万行慢 SQL、索引失效、回表、BufferPool 命中率等问题就不再只是面试题而是线上事故的导火索。很多候选人能背出 B 树的结构也能写出CREATE INDEX语句但被追问“为什么联合索引必须遵循最左前缀”“为什么这个查询走了索引却还是很慢”“BufferPool 和索引查询有什么关系”时会明显卡住。本文围绕千万级 MySQL 索引这条主线从 B 树、联合索引、BufferPool 一直讲到执行计划和慢 SQL 排查把面试手撕和实际调优需要理解的知识串成一条可复用的链路。1. 面试官问“千万级 SQL 慢”时到底在考察什么1.1 千万级数据量下的性能瓶颈来自磁盘和随机 IO单表数据量小的时候全表扫描也能很快返回因为数据页大多已经缓存在内存里。但数据量到千万级后索引和数据的总体积会超过内存容量查询过程中必然会出现磁盘 IO。磁盘 IO 的延迟远高于内存和 CPU 计算所以 MySQL 性能调优的核心逻辑是尽量减少磁盘访问次数尤其是随机 IO。InnoDB 默认一页是 16KB索引和数据都以页为基本单位读入内存。一次查询要读多少页直接决定查询快慢。全表扫描可能需要读几十万页而走主键索引可能只需要读几个页。这个数量级差异就是千万级数据下索引存在的根本原因。面试时回答这个问题不要只说“索引能加速查询”而要落到 IO 成本上索引的价值在于用一个更小的树结构快速定位到目标数据所在的页避免全表扫描产生的大量磁盘 IO。1.2 索引优化的核心目标减少扫描行数和回表次数索引优化看起来复杂其实核心目标只有两个减少扫描行数。减少回表次数。扫描行数越少查询花费的时间越短回表次数越少随机 IO 越少。MySQL 优化器选择执行计划时也是在寻找扫描行数和回表代价的组合最优解。举个例子一张用户表有 1000 万行执行SELECT id, user_name FROM user WHERE id 100。如果id是主键这条查询通过主键索引直接定位扫描 1 行。如果user_name没有索引MySQL 只能做全表扫描最坏情况扫描 1000 万行。两者相差千万倍。这也是为什么面试中经常拿“覆盖索引”做文章。二级索引的叶子节点只保存索引列和主键值如果查询需要的列都在二级索引里InnoDB 就能直接返回结果不需要回主键索引查完整行。1.3 面试回答的标准链路执行计划 - 索引选择 - 存储结构 - 缓冲池面对“一条 SQL 在千万级表上很慢怎么排查”这类问题回答要体现排查顺序而不是零散地甩知识点。推荐链路拿到慢 SQL先用EXPLAIN看执行计划。看type、key、rows、Extra判断有没有走索引、扫描了多少行。如果没走索引分析是 SQL 写法问题还是索引设计问题。如果走了索引还是慢考虑回表量、排序、临时表、锁等待以及 BufferPool 命中率。最后再看硬件层磁盘类型、IOPS、内存分配。这条链路能把 SQL 优化、索引原理、InnoDB 存储结构和内存机制串起来也是本文后续章节要展开的内容。2. 先从 B 树理解 InnoDB 索引的底层设计2.1 为什么选 B 树而不是哈希或 B 树InnoDB 的索引结构是 B 树。先明确一点B 树不是一种单独的数据结构而是多路平衡搜索树的一种改进版。对比哈希索引B 树支持范围查询、排序和前缀匹配。哈希索引只能做等值查询WHERE id 1很快但WHERE id 100就只能全表扫描。实际业务中范围查询非常普遍所以 InnoDB 默认聚簇索引选择 B 树。对比 B 树B 树有两个关键差异只有叶子节点存储数据非叶子节点只存储索引键和子节点指针。叶子节点之间通过指针串联成一个有序链表。因为非叶子节点不存数据所以每个节点能存放更多索引键树的高度更低。千万级数据量下B 树通常只有 3 到 4 层查询一条记录只需要几次磁盘 IO。叶子节点有链表范围查询时顺着链表顺序读即可B 树则可能需要在中序遍历中反复回溯。MySQL 中的主键索引和普通索引底层的核心结构都是 B 树区别在于叶子节点保存的内容。2.2 B 树的层数与千万级数据的检索代价InnoDB 页默认大小是 16KB。以主键是BIGINT为例主键占 8 字节页目录中指向子节点的指针大约占 6 字节一个非叶子节点大约能存放16 * 1024 / 14也就是 1100 个左右的索引键。假设叶子节点每行数据平均占用 1KB一个叶子页能存约 16 行。三层 B 树的存储能力大约是第一层根节点约 1100 个键。第二层约 1100 个节点。第三层叶子约 1100 * 1100 * 16 1936 万行。也就是说2000 万行左右的主键索引树从根节点到叶子节点只需要 3 次磁盘 IO。如果这棵树已被缓存在 BufferPool 里实际可能只需要访问内存页性能会更快。面试中手撕“B 树查找过程”可以按下面的伪代码描述输入主键值 target 当前节点 根节点 循环 如果当前节点是叶子节点 在叶子节点的有序键数组中二分查找 target 如果找到返回对应的行数据或主键值 否则返回不存在 否则 在非叶子节点中找到大于等于 target 的最小键对应的子节点指针 当前节点 指向的子节点 继续循环这个过程的本质是不断缩小搜索范围。每一层只做一次节点内的键比较和一次指针跳转因此时间和树高成正比。2.3 聚集索引和二级索引回表与覆盖索引InnoDB 中表本身就是按主键构建的 B 树这个索引叫聚集索引。聚集索引的叶子节点保存整行数据所以通过主键查询最快直接就能拿到完整数据。非主键索引叫二级索引它的叶子节点保存的是索引列和主键值。执行SELECT * FROM user WHERE email testexample.com时如果email有二级索引MySQL 会先在二级索引中找到对应的主键再到聚集索引中回表取完整行。这个“第二次查找”就是回表。回表不是必然会发生的。如果查询的列全部出现在二级索引里比如SELECT id, email FROM user WHERE email testexample.com二级索引里已经有id和emailMySQL 不需要回表这就是覆盖索引。设计索引时尽量把高频查询需要的列放进索引能显著减少随机 IO。2.4 手撕题意向描述一次索引查找过程面试手撕不一定要求写完整代码更多是让候选人描述查找路径。推荐按“根节点 - 中间节点 - 叶子节点 - 对比数据”的顺序回答MySQL 根据索引定位到 B 树根节点所在页。在根节点页中比较键值确定下一步进入哪个子节点。反复下溯直到叶子节点。在叶子节点中的有序链表里查找目标值。如果是聚集索引直接返回完整行如果是二级索引先拿主键再回表。同时要补充一个细节B 树是页为单位加载的。一次 IO 会读入整个页而页内查找可以用二分查找所以真正需要磁盘 IO 的次数通常是树高减一而不是逐行扫描。这也是 B 树能支撑千万级数据的核心原因。3. 联合索引最左前缀、索引下推和失效场景3.1 联合索引的字段顺序为什么决定查询能力联合索引是指在多个列上建立的索引比如(user_id, status, create_time)。很多开发者只记住了“最左前缀”却没有理解顺序为什么重要。回到 B 树结构。联合索引不是把三个列分别建索引而是把三个列拼接在一个索引键里按字段顺序排序。先按user_id排序user_id相同的记录再按status排序status相同的再按create_time排序。B 树的叶子节点链表整体上有序但这个有序性来自最左侧的字段。也就是说索引有序性是“级联”的。查询条件里只有user_id可以利用索引只有status和create_time却不能直接用这个索引定位因为status在索引中的排序只存在于user_id相同的分组里单独拿status作为条件时MySQL 无法利用索引的有序性定位。所以设计联合索引字段顺序时优先考虑等值条件字段再考虑范围字段。例如订单查询经常同时带user_id和status把user_id放前面通常更合理。但如果业务上经常单独用status查询就需要单独为status建索引。3.2 最左前缀原则的底层原因最左前缀原则可以直接由 B 树的排序规则推导出来。在联合索引(a, b, c)中WHERE a ?走索引。WHERE a ? AND b ?走索引。WHERE a ? AND b ? AND c ?走索引。WHERE b ?不一定走索引即使优化器选择全索引扫描定位粒度也很差。WHERE a ? AND c ?走索引但只用到a的等值定位c无法参与索引匹配。面试时可以用一个更直观的说法联合索引就像一本书的目录按照“章节 - 小节 - 句子”组织你只知道“句子”不知道“章节”和“小节”就很难通过目录直接定位。需要注意的是MySQL 8.0 引入了索引跳跃扫描在某些条件下可以优化WHERE b ?这种查询但依赖统计信息和数据分布不能作为常规方案。生产环境仍然要按最左前缀设计索引。3.3 MySQL 5.6 索引下推如何减少回表索引下推是 MySQL 5.6 引入的优化很多人只记住了名词不清楚它解决了什么问题。看这个例子。联合索引(user_id, status, create_time)查询SELECT * FROM order_info WHERE user_id 100 AND status PAID AND create_time 2025-01-01;假设create_time是范围条件只能匹配到user_id 100 AND status PAID的部分。没有索引下推时InnoDB 引擎把所有满足user_id 100的记录都从二级索引返回给 Server 层Server 层再过滤status和create_time。中间可能涉及大量回表。有了索引下推Server 层会把status PAID这个判断下推到存储引擎存储引擎在二级索引叶子节点上直接过滤只有满足条件的记录才回表。回表次数大大减少。判断一个查询是否用了索引下推看执行计划Extra列是否出现Using index condition。面试时可以说索引下推的本质是把部分 WHERE 条件的过滤动作从 Server 层移到存储引擎层减少二级索引到聚集索引的回表次数。3.4 常见索引失效场景与检查方式索引失效是个高频考点但很多说法已经过时需要结合版本理解。下面是常见场景和原因场景原因建议对索引列使用函数或计算破坏索引有序性改写为范围条件或保留函数索引隐式类型转换字段是字符串条件传数字保证 SQL 类型与字段类型一致LIKE 通配符在开头无法确定前缀使用前缀匹配或全文索引联合索引不满足最左前缀没有从最左列开始重排字段顺序或补索引使用 OR 连接非索引列优化器可能选择全表扫描拆分为 UNION 或加索引字符串列使用FIND_IN_SET函数无法走普通索引改表结构或用 JOIN以FIND_IN_SET为例即使索引列是普通字段WHERE FIND_IN_SET(id, 1,2,3)也会索引失效因为优化器无法基于函数结果做有序查找。正确做法是改成id IN (1, 2, 3)。这是面试和实际开发中容易被忽略的坑。检查索引是否生效永远以EXPLAIN为准不要靠经验猜测。即使某条 SQL 理论上能走索引优化器也可能因为统计信息不准确、数据量太小或回表成本高而选择全表扫描。4. BufferPool为什么说索引性能离不开内存4.1 BufferPool 是什么解决什么问题BufferPool 是 InnoDB 在内存中开辟的一块缓冲区域用于缓存数据页、索引页、undo 页等信息。MySQL 的增删改查操作并不是每次都直接读写磁盘而是先把相关页读入 BufferPool在内存里修改之后再由后台线程把脏页刷回磁盘。它解决的核心问题是磁盘 IO 和内存访问速度的巨大差距。如果每一次 B 树查找都要从磁盘读页千万级表即使有索引也会因为高并发访问而打满磁盘 IO。有了 BufferPool热点页常驻内存第二次访问相同数据时不再触发磁盘读。面试时可以这样回答BufferPool 决定了一个二级索引或主键索引页在内存命中的概率索引设计解决的是“少读页”BufferPool 解决的是“读过的页能否复用”。4.2 页、缓冲页、LRU 链表的关系InnoDB 以页为单位管理数据BufferPool 也以页为单位缓存。每个缓冲页都有一个控制块控制块用来记录页的地址、所属表空间、页号、脏页标记等信息。BufferPool 内部使用改良版 LRU最近最少使用算法管理缓冲页。传统 LRU 按访问时间排序新读入的页放在链表头部。但 MySQL 的改良 LRU 把链表分成 young 区和 old 区新读入的页先进入 old 区。只有再次被访问并且停留时间超过阈值才会晋升到 young 区。这样设计是为了防止一次全表扫描把热点数据全部挤出 BufferPool。这个细节可以直接回答“为什么 BufferPool 应该关注命中率而不是容量大小”的问题。即使 BufferPool 很大如果 LRU 策略设计不合理热点页也可能被冷数据污染。4.3 参数排查innodb_buffer_pool_size 设置与命中率计算最常见参数是innodb_buffer_pool_size。在专用 MySQL 服务器上经验值通常是物理内存的 50% 到 70%但具体多少要结合实例数量、业务类型和内存余量决定不能盲目照搬。修改配置前先看当前值SHOW VARIABLES LIKE innodb_buffer_pool_size; SHOW GLOBAL STATUS LIKE Innodb_buffer_pool_read_requests; SHOW GLOBAL STATUS LIKE Innodb_buffer_pool_reads;Innodb_buffer_pool_read_requests表示从 BufferPool 读页的次数Innodb_buffer_pool_reads表示从磁盘读页的次数。命中率计算方式命中率 (Innodb_buffer_pool_read_requests - Innodb_buffer_pool_reads) / Innodb_buffer_pool_read_requests * 100%如果命中率长期低于 95%说明 BufferPool 不足或者查询扫描的页范围过大需要扩容内存或优化 SQL。配置文件my.cnf中常见的设置[mysqld] innodb_buffer_pool_size 8G innodb_buffer_pool_instances 8 innodb_old_blocks_time 1000innodb_buffer_pool_instances用于把 BufferPool 拆成多个实例降低并发访问时的互斥竞争。innodb_old_blocks_time表示页进入 old 区后需要等待多少毫秒再次被访问才能晋升到 young 区默认值通常是 1000 毫秒可以在全表扫描场景中适当调大。4.4 BufferPool 不足时对索引查询的影响BufferPool 不足时即使 SQL 走了正确的索引也可能会出现以下现象查询第一次访问某个索引页时需要从磁盘读取页延迟明显偏高。高并发下热点页被频繁淘汰命中率下降磁盘 IO 飙升。慢日志中出现大量单次查询时间波动大的语句原因是部分页缓存未命中。因为 BufferPool 还需要缓存 undo 页和变更缓冲大事务或大批量写会进一步占用缓冲空间挤占索引页缓存。排查时先结合SHOW ENGINE INNODB STATUS查看 BufferPool 命中率再看慢 SQL 是否集中在冷数据表。不要一上来就加内存而是先确认索引是否合理再用 BufferPool 命中率数据做决策。5. 从执行计划到慢 SQL 排查一套可复用的方法5.1 使用 EXPLAIN 定位慢 SQL拿到慢 SQL 后第一步是执行EXPLAIN查看执行计划EXPLAIN SELECT order_id, user_id, status, create_time FROM order_info WHERE user_id 100 AND status PAID ORDER BY create_time DESC LIMIT 20;MySQL 8.0 也可以用EXPLAIN ANALYZE查看实际执行路径和耗时但生产环境最好不要直接跑完整语句可以把条件复制到从库或测试环境。执行计划中重点关注以下字段字段含义需要警惕的值type访问类型ALL全表扫描index全索引扫描key实际使用的索引NULL表示没用索引rows预估扫描行数与表总量接近时说明过滤性差filtered过滤比例百分比越低说明还需要大量过滤Extra附加信息Using filesort、Using temporary、Using index condition5.2 关键字段解读type、key、rows、Extratype从好到差大致是system、const、eq_ref、ref、range、index、ALL。index和ALL都意味着扫描范围很大需要重点优化。rows是优化器估算的扫描行数不是实际值。如果估算严重偏离实际通常是统计信息过期可以执行ANALYZE TABLE刷新。Extra是最容易发现问题的位置Using filesort说明排序没有用到索引可能需要额外排序操作。Using temporary说明使用了临时表常见于GROUP BY、DISTINCT、多表关联。Using index condition表示使用了索引下推。Using index表示覆盖索引不需要回表是最理想的状态之一。另一个常见现象是查询走了索引但Extra出现Using filesort。这说明索引字段顺序和ORDER BY不一致排序无法利用索引的有序性。解决办法是让索引字段顺序包含排序字段并且排序方向一致。5.3 慢查询日志配置慢查询日志是排查线上慢 SQL 的重要工具。在 MySQL 中临时开启SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1; SET GLOBAL log_queries_not_using_indexes ON;长期配置写在my.cnf[mysqld] slow_query_log 1 slow_query_log_file /var/log/mysql/mysql-slow.log long_query_time 1 log_queries_not_using_indexes 1long_query_time 1表示超过 1 秒的 SQL 会被记录。生产环境建议从 1 秒开始根据业务情况逐步调大避免日志量过大。查看慢日志可以直接读文件也可以使用mysqldumpslow汇总mysqldumpslow -s c -t 10 /var/log/mysql/mysql-slow.log这个命令会按执行次数排序展示最需要优化的 10 条慢 SQL。5.4 排查链路现象 - 语句 - 执行计划 - 索引 - 缓冲池 - 硬件实际遇到线上慢查询建议按照下面的顺序排查确认现象是偶发还是持续是单条 SQL 还是整体变慢。拿到真实 SQL检查是否包含无用查询字段、类型转换、函数。执行EXPLAIN确认执行计划是否合理。检查索引是否存在、是否被正确使用统计信息是否过期。查看 BufferPool 命中率和锁等待状态。最后看硬件指标磁盘 IOPS、CPU、内存、网络。注意偶发慢查询更可能是锁等待、日志刷盘抖动或 BufferPool 淘汰导致。持续慢查询更可能是索引和 SQL 本身的问题。两者不能混为一谈。6. 千万级索引设计的面试手撕题与最佳实践6.1 经典手撕题给出一张表如何设计索引面试中出现频率很高的一道题CREATE TABLE order_info ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, order_no VARCHAR(64) NOT NULL, status TINYINT NOT NULL, amount DECIMAL(10, 2) NOT NULL, create_time DATETIME NOT NULL, INDEX idx_user_status_create_time (user_id, status, create_time), UNIQUE KEY uk_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;要求根据以下查询设计索引。SELECT order_no, amount FROM order_info WHERE user_id 100 AND status 1 AND create_time BETWEEN 2025-01-01 AND 2025-06-30 ORDER BY create_time;这类题的重点不是直接给答案而是分析查询条件和排序user_id和status是等值条件放在联合索引前面。create_time是范围条件放在最后。查询列order_no、amount如果也能放进索引就能形成覆盖索引但当前索引里没有amount仍会回表。如果业务中单独用order_no查询保留唯一索引uk_order_no。可以回答先创建(user_id, status, create_time)联合索引然后查看执行计划确认是否出现Using filesort。如果还需要覆盖索引可以扩展为(user_id, status, create_time, amount, order_no)但索引列越多写入开销越大需要平衡。6.2 表结构和 SQL 示例下面是用于验证索引效果的完整示例CREATE TABLE order_info ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, order_no VARCHAR(64) NOT NULL, status TINYINT NOT NULL, amount DECIMAL(10, 2) NOT NULL, create_time DATETIME NOT NULL, KEY idx_user_status_create_time (user_id, status, create_time), UNIQUE KEY uk_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;插入模拟数据时可以先写一个存储过程写入几十万行再观察执行计划DELIMITER $$ CREATE PROCEDURE insert_order_data(IN total INT) BEGIN DECLARE i INT DEFAULT 1; DECLARE random_user BIGINT; DECLARE random_status TINYINT; DECLARE random_time DATETIME; WHILE i total DO SET random_user FLOOR(1 RAND() * 10000); SET random_status FLOOR(RAND() * 10); SET random_time TIMESTAMPADD(SECOND, -FLOOR(RAND() * 365 * 86400), NOW()); INSERT INTO order_info (user_id, order_no, status, amount, create_time) VALUES (random_user, CONCAT(NO, i), random_status, RAND() * 1000, random_time); SET i i 1; END WHILE; END$$ DELIMITER ;调用存储过程CALL insert_order_data(1000000);生产环境不要轻易写这类存储过程测试环境可以先删索引再造数据避免索引建立时间影响测试结果。6.3 造数据验证索引效果数据准备好后分别看两种查询的执行计划EXPLAIN SELECT order_no, amount FROM order_info WHERE user_id 100 AND status 1 AND create_time BETWEEN 2025-01-01 AND 2025-06-30 ORDER BY create_time;如果索引字段顺序合理type应该是range或refrows远小于总行数Extra不会出现Using filesort。再试验一个不满足最左前缀的查询EXPLAIN SELECT order_no, amount FROM order_info WHERE status 1 AND create_time BETWEEN 2025-01-01 AND 2025-06-30;此时status不是联合索引的最左列优化器很可能选择全表扫描type为ALL。即使极端情况下走了index全索引扫描rows也接近总行数。这就是实战中要避免的写法。6.4 索引设计最佳实践清单可复用的索引设计检查清单先列出业务中最高频的查询条件按等值、范围、排序优先级设计联合索引。等值条件放联合索引前面范围条件放后面排序字段尽量包含在索引中。优先用覆盖索引覆盖高频查询的列减少回表。不要对索引列做函数运算、隐式类型转换。控制单个索引的列数一般不建议超过 5 个否则写入代价和维护成本会明显上升。区分唯一约束和普通索引唯一性由业务保证时再建唯一索引。定期使用ANALYZE TABLE更新统计信息。大表加索引时使用在线 DDL并评估锁和磁盘空间。删除无用的冗余索引尤其是前缀相同的联合索引。所有假设都用EXPLAIN验证不要凭感觉。7. 生产环境落地学习环境设计索引与生产环境上线的差异7.1 学习环境可以怎么做学习环境的目标是快速理解原理和验证结果。可以在一台虚拟机或本地 MySQL 上建一张千万级测试表调整innodb_buffer_pool_size反复执行EXPLAIN和ANALYZE TABLE。操作时要注意不要直接用生产环境的真实数据量大表做实验。如果机器内存不够可以用小批量数据先验证结构再逐步扩大。开启慢查询日志观察不同 SQL 的耗时差异。学习阶段可以自由尝试但要注意测试环境的结果不一定能直接推广到生产环境。数据分布、并发量、硬件性能都会影响优化器选择。7.2 生产环境上线前要检查什么索引上线不是执行一条CREATE INDEX就结束。上线前至少检查是否有足够的磁盘空间存储新索引。大表建索引的耗时和锁影响是否使用ALGORITHMINPLACE。是否会影响写入性能尤其是写多读少的表。是否在从库上先验证再灰度到主库。是否记录上线前后的 SQL 耗时和监控数据便于回滚判断。MySQL 在线添加索引示例ALTER TABLE order_info ADD INDEX idx_status_create_time (status, create_time), ALGORITHMINPLACE, LOCKNONE;ALGORITHMINPLACE和LOCKNONE表示允许在线 DDL减少对读写的影响但具体是否支持取决于 MySQL 版本和表结构。生产环境执行前先在测试环境确认。7.3 索引维护统计信息、碎片、归档索引并不是创建完就一劳永逸。表数据频繁增删后B 树可能出现页分裂、页碎片导致索引查询变慢。常见维护操作ANALYZE TABLE order_info;ANALYZE TABLE会重新统计索引分布信息帮助优化器生成更准确的执行计划。如果表碎片非常严重可以考虑重建表或索引ALTER TABLE order_info FORCE;FORCE会重建表过程中需要额外的磁盘空间和锁审计建议在业务低峰期执行。对于超大表更推荐通过归档历史数据减少表体积而不是频繁重建索引。另外数据量达到千万级后即使索引合理也要关注冷热数据分离。历史订单可以按月归档到独立表或者使用分区表减少主表扫描页数。7.4 面试回答的收尾策略面试官如果继续追问“还有没有其他优化手段”可以从这几个方向扩展覆盖索引减少回表也就是减少随机 IO。索引下推让存储引擎提前过滤。排序优化让ORDER BY利用索引有序性避免Using filesort。条件改写把OR改写为UNION把FIND_IN_SET改写为IN。参数层调整innodb_buffer_pool_size提高缓存命中率。架构层读写分离、历史数据归档、分库分表。回答时不要罗列名词要把每个手段和“减少扫描行数、减少回表次数、减少磁盘 IO”这三个目标挂钩。这样既展示了底层理解也体现了工程判断力。MySQL 索引面试的核心不是背住 B 树的定义而是能把 B 树结构、联合索引规则、BufferPool 缓存机制和执行计划串联起来。遇到慢 SQL先分析执行计划再设计索引最后从缓冲池和硬件层面验证。这比死记硬背几十条优化技巧更有价值。