云计算百科
云计算领域专业知识百科平台

MySQL 复合索引深度剖析:最左匹配、ICP、回表、IO 模式底层真相

摘要:网上大量教程只讲最左匹配口诀,很少讲底层 B+ 树、索引下推 ICP、回表、顺序/随机 IO、BufferPool 之间的完整链路。本文结合 Explain 执行计划、底层存储原理,把面试高频坑一次性讲透。


前置准备

建表与复合索引:

CREATE TABLE test(
id INT PRIMARY KEY AUTO_INCREMENT,
a INT,
b INT,
c INT
);
— 创建联合索引 idx(a,b,c)
CREATE INDEX idx_abc ON test(a,b,c);

复合索引 idx(a,b,c) 的 B+ 树叶子节点,排序规则是:优先按 a 排序;a 相等,按 b 排序;b 相等,按 c 排序;叶子行末尾附带主键 id。

复合索引叶子排序规则

上图演示了复合索引叶子页的排序逻辑。从图中可以看到:a 相同的行被分到同一组;a 相同后按 b 排序;只有 (a,b) 都相同时,c 才有序。行尾的主键 id 是后续回表的“钥匙”。

因果链:因为 InnoDB 的 B+ 树叶子行按 (a,b,c) 全局有序 → 所以索引定位时可以从 a 开始二分查找 → 但因为排序优先级是 a→b→c 依次递减 → 所以一旦 a 或 b 出现范围查询,c 在叶子内部就不再有序。


一、什么是最左匹配(最左前缀原则)

核心:复合索引要从索引定义的最左侧字段开始,匹配连续前缀;SQL 的 where 条件书写顺序不影响索引命中,优化器会自动调整条件顺序。

✅ 可以有效使用索引前缀:

where a=1
where a=1 and b=2
where a=1 and b=2 and c=3
where b=2 and a=1 — where 条件顺序打乱,优化器重排,依旧命中索引

❌ 无法使用索引(缺失最左前缀):

where b=2
where b=2 and c=3
where c=5

误区:不是 where 子句写了 a/b/c 字段就一定能用索引,必须要有索引定义的最左起始列。

重点:遇到范围查询,后面字段无法做索引 seek

> < >= <= between 属于范围条件。一旦复合索引匹配中遇到范围查询,范围之后的字段不能再利用索引有序性做快速定位(seek)。

where a=10 and b>20 and c=5

  • a:等值匹配,索引 seek(B+ 树二分定位)
  • b:范围条件,索引 range 扫描
  • c:不能走索引 seek。因为 b 范围之后,c 在 B+ 树叶子节点内部是无序的

范围查询后 c 无序

上图完整演示了 where a=10 and b>20 and c=5 的执行过程:a 先等值 seek 定位;b 范围扫描命中连续 6 行;但这 6 行里的 c 值(5,12,5,3,40,5)完全无序,无法 seek 定位,只能逐行判断。

但是!c 不是完全失效,它可以交给索引下推 ICP 在二级索引页内过滤,这点是很多博客遗漏的关键点。

补充:in 算不算范围?

in(1,2,3) 不属于破坏索引的范围条件,MySQL 内部等价多个 or 等值,不会打断后面索引字段匹配,后续字段依旧可以 seek。

补充:MySQL 8.0 索引跳跃扫描(Skip Scan)

MySQL 8.0.13+ 引入 Skip Scan 优化。当缺失最左前缀时(如 where b=2 and c=3),优化器可能通过扫描 a 的所有不同值,在每个 a 值下分别做 b,c 的 seek 来利用索引,代价是扫描次数 = a 的 distinct 数量。Explain 中 type=range + Using index for skip scan。


二、索引下推 ICP(Index Condition Pushdown)

ICP 全称:索引条件下推,MySQL 5.6 之后支持,Explain Extra 字段显示 Using index condition。

没有 ICP 时代执行流程

BufferPool / 磁盘

存储引擎 InnoDB

MySQL Server 层

BufferPool / 磁盘

存储引擎 InnoDB

MySQL Server 层

#mermaid-svg-Zu1U4ObAzcPFy7j0{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Zu1U4ObAzcPFy7j0 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Zu1U4ObAzcPFy7j0 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Zu1U4ObAzcPFy7j0 .error-icon{fill:#552222;}#mermaid-svg-Zu1U4ObAzcPFy7j0 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Zu1U4ObAzcPFy7j0 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Zu1U4ObAzcPFy7j0 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Zu1U4ObAzcPFy7j0 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Zu1U4ObAzcPFy7j0 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Zu1U4ObAzcPFy7j0 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Zu1U4ObAzcPFy7j0 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Zu1U4ObAzcPFy7j0 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Zu1U4ObAzcPFy7j0 .marker.cross{stroke:#333333;}#mermaid-svg-Zu1U4ObAzcPFy7j0 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Zu1U4ObAzcPFy7j0 p{margin:0;}#mermaid-svg-Zu1U4ObAzcPFy7j0 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Zu1U4ObAzcPFy7j0 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-Zu1U4ObAzcPFy7j0 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Zu1U4ObAzcPFy7j0 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-Zu1U4ObAzcPFy7j0 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-Zu1U4ObAzcPFy7j0 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-Zu1U4ObAzcPFy7j0 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-Zu1U4ObAzcPFy7j0 .sequenceNumber{fill:white;}#mermaid-svg-Zu1U4ObAzcPFy7j0 #sequencenumber{fill:#333;}#mermaid-svg-Zu1U4ObAzcPFy7j0 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-Zu1U4ObAzcPFy7j0 .messageText{fill:#333;stroke:none;}#mermaid-svg-Zu1U4ObAzcPFy7j0 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Zu1U4ObAzcPFy7j0 .labelText,#mermaid-svg-Zu1U4ObAzcPFy7j0 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-Zu1U4ObAzcPFy7j0 .loopText,#mermaid-svg-Zu1U4ObAzcPFy7j0 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-Zu1U4ObAzcPFy7j0 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Zu1U4ObAzcPFy7j0 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-Zu1U4ObAzcPFy7j0 .noteText,#mermaid-svg-Zu1U4ObAzcPFy7j0 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-Zu1U4ObAzcPFy7j0 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Zu1U4ObAzcPFy7j0 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Zu1U4ObAzcPFy7j0 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Zu1U4ObAzcPFy7j0 .actorPopupMenu{position:absolute;}#mermaid-svg-Zu1U4ObAzcPFy7j0 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-Zu1U4ObAzcPFy7j0 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Zu1U4ObAzcPFy7j0 .actor-man circle,#mermaid-svg-Zu1U4ObAzcPFy7j0 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-Zu1U4ObAzcPFy7j0 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

根据 a、b 条件扫描二级索引,拿到主键 id 集合

1

逐个主键回表查聚簇索引完整行

2

返回整行数据

3

返回全部行数据

4

在 Server 层过滤 c=5 条件,丢弃不满足的数据

5

问题:很多主键对应的行本来就不满足 c 条件,白白执行大量回表随机 IO。

开启 ICP 后流程

BufferPool / 磁盘

存储引擎 InnoDB

MySQL Server 层

BufferPool / 磁盘

存储引擎 InnoDB

MySQL Server 层

#mermaid-svg-wnKEUkPR2XmClVRy{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wnKEUkPR2XmClVRy .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wnKEUkPR2XmClVRy .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wnKEUkPR2XmClVRy .error-icon{fill:#552222;}#mermaid-svg-wnKEUkPR2XmClVRy .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wnKEUkPR2XmClVRy .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wnKEUkPR2XmClVRy .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wnKEUkPR2XmClVRy .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wnKEUkPR2XmClVRy .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wnKEUkPR2XmClVRy .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wnKEUkPR2XmClVRy .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wnKEUkPR2XmClVRy .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wnKEUkPR2XmClVRy .marker.cross{stroke:#333333;}#mermaid-svg-wnKEUkPR2XmClVRy svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wnKEUkPR2XmClVRy p{margin:0;}#mermaid-svg-wnKEUkPR2XmClVRy .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-wnKEUkPR2XmClVRy text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-wnKEUkPR2XmClVRy .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-wnKEUkPR2XmClVRy .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-wnKEUkPR2XmClVRy .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-wnKEUkPR2XmClVRy .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-wnKEUkPR2XmClVRy #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-wnKEUkPR2XmClVRy .sequenceNumber{fill:white;}#mermaid-svg-wnKEUkPR2XmClVRy #sequencenumber{fill:#333;}#mermaid-svg-wnKEUkPR2XmClVRy #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-wnKEUkPR2XmClVRy .messageText{fill:#333;stroke:none;}#mermaid-svg-wnKEUkPR2XmClVRy .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-wnKEUkPR2XmClVRy .labelText,#mermaid-svg-wnKEUkPR2XmClVRy .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-wnKEUkPR2XmClVRy .loopText,#mermaid-svg-wnKEUkPR2XmClVRy .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-wnKEUkPR2XmClVRy .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-wnKEUkPR2XmClVRy .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-wnKEUkPR2XmClVRy .noteText,#mermaid-svg-wnKEUkPR2XmClVRy .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-wnKEUkPR2XmClVRy .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-wnKEUkPR2XmClVRy .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-wnKEUkPR2XmClVRy .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-wnKEUkPR2XmClVRy .actorPopupMenu{position:absolute;}#mermaid-svg-wnKEUkPR2XmClVRy .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-wnKEUkPR2XmClVRy .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-wnKEUkPR2XmClVRy .actor-man circle,#mermaid-svg-wnKEUkPR2XmClVRy line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-wnKEUkPR2XmClVRy :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

根据 a seek,b 做 range 扫描

1

在二级索引页内直接利用 c 列过滤,不满足直接丢弃

2

只把过滤后剩余的主键 id 回表查整行

3

返回整行数据

4

返回过滤后的行

5

✔ ICP 本质:在二级索引页过滤数据,减少需要回表的主键数量,从而减少回表 IO。 ⚠ 注意:ICP 只是过滤,不能对 c 做索引 seek,不能利用 c 的有序性快速定位区间。

ICP 有无对比

上图对比了有无 ICP 的核心差异:无 ICP 时 6 行全部回表,到 Server 层才发现 3 行不满足 c=5,回表白做;开启 ICP 后,存储引擎在索引页内先过滤 c=5,只剩 3 次回表,随机 IO 直接减半。

覆盖索引与 ICP 区分

Explain Extra:

  • Using index:覆盖索引,直接从二级索引拿到全部查询字段,完全不需要回表。
  • Using index condition:ICP,部分条件索引层过滤,仍然需要回表。

三、回表是什么?聚簇索引 vs 二级索引

InnoDB 中:

  • 聚簇索引(主键索引):叶子节点存储完整整行数据;表数据本身就是主键 B+ 树。
  • 二级索引(普通/复合索引):叶子节点存储索引列 + 主键 id,没有完整行。
  • 回表:拿到二级索引叶子的主键 id,再去主键 B+ 树查找完整行数据的过程。

    如果查询需要的全部字段都在二级索引内,不需要读取完整行,就是覆盖索引,避免回表。

    — 覆盖索引,Extra: Using index,无需回表
    select a,b,c from test where a=10 and b>20;

    key_len:判断复合索引用到多少字段

    key_len 表示实际用到索引的字节长度。以 a INT, b INT, c INT 为例:

    查询条件key_len含义
    where a=10 5 只用 a(INT 4 字节 + nullable 1 字节)
    where a=10 and b>20 10 用到 a、b(b 范围仍计入 key_len)
    where a=10 and b=2 and c=5 15 用到 a、b、c 全部

    面试技巧:看到 key_len=10 就知道只用到前两个字段;key_len=5 说明只走了 a,b 没参与索引定位。


    四、表空间与数据页组织:页从哪里来?

    在讲 IO 类型之前,必须先回答一个问题:回表时访问的"页",在磁盘上是怎么存的?

    InnoDB 的数据最终持久化在表空间(tablespace)中。开启 innodb_file_per_table=ON 时,每张表对应一个独立的 .ibd 文件,这就是该表的独立表空间。

    表空间内部是层级组织:

    层级大小作用
    页(Page) 16KB 最小读写单位,存实际数据行
    区(Extent) 1MB = 64 页 空间分配的最小单位,保证区内页物理连续
    段(Segment) 变长 逻辑概念,如叶子节点段、非叶子节点段

    因果链:

    因为 表空间按 extent 成片分配(一次分 64 页,物理连续)
    → 所以 同一段时期内分配的页,物理位置大概率相邻
    → 但因为 增删改导致页分裂、合并、回收再分配
    → 所以 逻辑上页号相邻的两页,磁盘位置可能已经分散
    → 因此 判定顺序/随机 IO 不能看物理位置,只能看页号访问次序

    关键认知:B+ 树叶子节点的"逻辑有序"≠"物理有序"。表空间决定了页的物理落脚处,而页号只是表空间内的逻辑编号。理解这一点,才能真正理解下一节的"顺序/随机 IO"。


    五、顺序 IO、随机 IO,不要再记死口诀!

    网上流传:二级索引扫描 = 顺序 IO,回表 = 随机 IO。这句话只是绝大多数场景的经验总结,不是铁律定义。

    InnoDB 判定顺序/随机访问模式

    InnoDB 看不到磁盘物理扇区,看的是表空间页号的访问序列:

  • 顺序访问模式(顺序 IO):页号持续递增向后访问;触发 InnoDB 线性预读 read-ahead。哪怕磁盘物理页不连续,只要访问次序连续向后,就视为顺序访问。
  • 随机访问模式(随机 IO):页号跳跃无序访问,无法触发预读,机械磁盘会产生昂贵寻道开销。
  • 关键点:顺序 IO、随机 IO 是访问模式,不是索引自带属性。

    示例 1:聚簇索引(主键)

    — 主键连续读取,页号递增,顺序 IO
    select * from test where id between 1000 and 2000;

    — 主键乱序跳跃读取,页号到处跳,随机 IO
    select * from test where id in (1001, 7, 3900, 56);

    普通回表为什么大多是随机 IO:

    二级索引筛选出来的主键 id 集合,排序规则跟随 (a,b,c),主键 id 是乱序打散的;拿着一堆无序 id 访问聚簇索引,页号来回跳,产生随机 IO。

    MRR 优化:把回表随机 IO 转为顺序 IO

    MRR(Multi-Range Read)多范围读优化,MySQL 官方专门解决回表大量随机 IO 的方案:

  • 先从二级索引拿到一批待回表主键 id;
  • 在内存缓冲区,把主键 id 从小到大排序;
  • 按主键升序访问聚簇索引,页号递增,随机 IO 变成顺序 IO。
  • Explain 会看到 Extra:Using MRR。MRR 充分证明,回表本身不等于随机 IO,访问主键的次序决定 IO 类型。

    MRR 随机转顺序 IO

    上图演示了 MRR 的核心机制:二级索引按 (a,b,c) 序吐出主键 57,3,812,21,406(乱序),直接回表时页号来回跳 = 随机 IO;MRR 先在缓冲区排成升序 3,21,57,406,812,再按页号递增顺序访问 → 顺序 IO + 触发预读。

    重要结论

    如果需要访问的数据页全部命中 Buffer Pool 内存,不存在磁盘 IO,顺序 IO、随机 IO 没有性能差异。顺序/随机 IO 概念只针对磁盘访问场景。


    六、延伸理解:BufferPool、脏页,帮你看懂 SQL 底层 IO 行为

    1. BufferPool LRU 冷热分区

    #mermaid-svg-aPSAqH6ZIZs0Xbke{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-aPSAqH6ZIZs0Xbke .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-aPSAqH6ZIZs0Xbke .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-aPSAqH6ZIZs0Xbke .error-icon{fill:#552222;}#mermaid-svg-aPSAqH6ZIZs0Xbke .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-aPSAqH6ZIZs0Xbke .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-aPSAqH6ZIZs0Xbke .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-aPSAqH6ZIZs0Xbke .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-aPSAqH6ZIZs0Xbke .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-aPSAqH6ZIZs0Xbke .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-aPSAqH6ZIZs0Xbke .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-aPSAqH6ZIZs0Xbke .marker{fill:#333333;stroke:#333333;}#mermaid-svg-aPSAqH6ZIZs0Xbke .marker.cross{stroke:#333333;}#mermaid-svg-aPSAqH6ZIZs0Xbke svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-aPSAqH6ZIZs0Xbke p{margin:0;}#mermaid-svg-aPSAqH6ZIZs0Xbke .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-aPSAqH6ZIZs0Xbke .cluster-label text{fill:#333;}#mermaid-svg-aPSAqH6ZIZs0Xbke .cluster-label span{color:#333;}#mermaid-svg-aPSAqH6ZIZs0Xbke .cluster-label span p{background-color:transparent;}#mermaid-svg-aPSAqH6ZIZs0Xbke .label text,#mermaid-svg-aPSAqH6ZIZs0Xbke span{fill:#333;color:#333;}#mermaid-svg-aPSAqH6ZIZs0Xbke .node rect,#mermaid-svg-aPSAqH6ZIZs0Xbke .node circle,#mermaid-svg-aPSAqH6ZIZs0Xbke .node ellipse,#mermaid-svg-aPSAqH6ZIZs0Xbke .node polygon,#mermaid-svg-aPSAqH6ZIZs0Xbke .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-aPSAqH6ZIZs0Xbke .rough-node .label text,#mermaid-svg-aPSAqH6ZIZs0Xbke .node .label text,#mermaid-svg-aPSAqH6ZIZs0Xbke .image-shape .label,#mermaid-svg-aPSAqH6ZIZs0Xbke .icon-shape .label{text-anchor:middle;}#mermaid-svg-aPSAqH6ZIZs0Xbke .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-aPSAqH6ZIZs0Xbke .rough-node .label,#mermaid-svg-aPSAqH6ZIZs0Xbke .node .label,#mermaid-svg-aPSAqH6ZIZs0Xbke .image-shape .label,#mermaid-svg-aPSAqH6ZIZs0Xbke .icon-shape .label{text-align:center;}#mermaid-svg-aPSAqH6ZIZs0Xbke .node.clickable{cursor:pointer;}#mermaid-svg-aPSAqH6ZIZs0Xbke .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-aPSAqH6ZIZs0Xbke .arrowheadPath{fill:#333333;}#mermaid-svg-aPSAqH6ZIZs0Xbke .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-aPSAqH6ZIZs0Xbke .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-aPSAqH6ZIZs0Xbke .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aPSAqH6ZIZs0Xbke .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-aPSAqH6ZIZs0Xbke .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aPSAqH6ZIZs0Xbke .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-aPSAqH6ZIZs0Xbke .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-aPSAqH6ZIZs0Xbke .cluster text{fill:#333;}#mermaid-svg-aPSAqH6ZIZs0Xbke .cluster span{color:#333;}#mermaid-svg-aPSAqH6ZIZs0Xbke div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-aPSAqH6ZIZs0Xbke .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-aPSAqH6ZIZs0Xbke rect.text{fill:none;stroke-width:0;}#mermaid-svg-aPSAqH6ZIZs0Xbke .icon-shape,#mermaid-svg-aPSAqH6ZIZs0Xbke .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aPSAqH6ZIZs0Xbke .icon-shape p,#mermaid-svg-aPSAqH6ZIZs0Xbke .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-aPSAqH6ZIZs0Xbke .icon-shape .label rect,#mermaid-svg-aPSAqH6ZIZs0Xbke .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aPSAqH6ZIZs0Xbke .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-aPSAqH6ZIZs0Xbke .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-aPSAqH6ZIZs0Xbke :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    BufferPool

    young 热区(最近频繁访问)

    old 冷区(新页默认进来)

    新页加载

    1s 内再次访问?

    停留 >1s 再访问

    淘汰冷页

    淘汰热页

    全表扫描大量新页

    InnoDB 使用改良 LRU 链表,分为 young 热区、old 冷区。新页默认进入 old 区头部;只有在 old 区停留超过 innodb_old_blocks_time(默认 1000ms)后再次被访问,才会移到 young 区头部。这避免了全表扫描一次性冲掉全部热点缓存。

    2. 脏页生命周期

    #mermaid-svg-vJbTcqFybr50Rk5b{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-vJbTcqFybr50Rk5b .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vJbTcqFybr50Rk5b .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vJbTcqFybr50Rk5b .error-icon{fill:#552222;}#mermaid-svg-vJbTcqFybr50Rk5b .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vJbTcqFybr50Rk5b .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vJbTcqFybr50Rk5b .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vJbTcqFybr50Rk5b .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vJbTcqFybr50Rk5b .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vJbTcqFybr50Rk5b .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vJbTcqFybr50Rk5b .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vJbTcqFybr50Rk5b .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vJbTcqFybr50Rk5b .marker.cross{stroke:#333333;}#mermaid-svg-vJbTcqFybr50Rk5b svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vJbTcqFybr50Rk5b p{margin:0;}#mermaid-svg-vJbTcqFybr50Rk5b defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-vJbTcqFybr50Rk5b g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-vJbTcqFybr50Rk5b g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-vJbTcqFybr50Rk5b g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-vJbTcqFybr50Rk5b g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-vJbTcqFybr50Rk5b g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-vJbTcqFybr50Rk5b .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-vJbTcqFybr50Rk5b .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-vJbTcqFybr50Rk5b .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-vJbTcqFybr50Rk5b .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-vJbTcqFybr50Rk5b .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-vJbTcqFybr50Rk5b .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-vJbTcqFybr50Rk5b .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-vJbTcqFybr50Rk5b .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vJbTcqFybr50Rk5b .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-vJbTcqFybr50Rk5b .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vJbTcqFybr50Rk5b .edgeLabel .label text{fill:#333;}#mermaid-svg-vJbTcqFybr50Rk5b .label div .edgeLabel{color:#333;}#mermaid-svg-vJbTcqFybr50Rk5b .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-vJbTcqFybr50Rk5b .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-vJbTcqFybr50Rk5b .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-vJbTcqFybr50Rk5b .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-vJbTcqFybr50Rk5b .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-vJbTcqFybr50Rk5b .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vJbTcqFybr50Rk5b .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vJbTcqFybr50Rk5b #statediagram-barbEnd{fill:#333333;}#mermaid-svg-vJbTcqFybr50Rk5b .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vJbTcqFybr50Rk5b .cluster-label,#mermaid-svg-vJbTcqFybr50Rk5b .nodeLabel{color:#131300;}#mermaid-svg-vJbTcqFybr50Rk5b .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-vJbTcqFybr50Rk5b .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-vJbTcqFybr50Rk5b .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-vJbTcqFybr50Rk5b .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-vJbTcqFybr50Rk5b .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-vJbTcqFybr50Rk5b .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-vJbTcqFybr50Rk5b .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-vJbTcqFybr50Rk5b .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-vJbTcqFybr50Rk5b .note-edge{stroke-dasharray:5;}#mermaid-svg-vJbTcqFybr50Rk5b .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-vJbTcqFybr50Rk5b .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-vJbTcqFybr50Rk5b .statediagram-note text{fill:black;}#mermaid-svg-vJbTcqFybr50Rk5b .statediagram-note .nodeLabel{color:black;}#mermaid-svg-vJbTcqFybr50Rk5b .statediagram .edgeLabel{color:red;}#mermaid-svg-vJbTcqFybr50Rk5b #dependencyStart,#mermaid-svg-vJbTcqFybr50Rk5b #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-vJbTcqFybr50Rk5b .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-vJbTcqFybr50Rk5b :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    从磁盘加载到 BufferPool

    UPDATE/INSERT/DELETE 修改数据

    Page Cleaner 异步刷脏成功

    LRU 淘汰,直接丢弃

    LRU 淘汰,先刷脏再释放

    干净页

    脏页

    3. 脏页是否可读?

    脏页完全可以对外查询。

    脏页定义:Buffer Pool 内存页被修改,内存版本 > 磁盘持久化版本;磁盘存旧数据。

    查询优先读取 Buffer Pool 内存中的页,不管它是不是脏页;后台 Page Cleaner 线程异步刷脏页,刷脏不会阻塞读写,刷盘成功脏页变成干净页,该页依旧留在 BufferPool。

    4. 脏页刷盘成功后,为什么不直接删除,要用 LRU 淘汰?

    很多人误区:脏页落盘完毕就没用了,直接清掉。

  • Redo Log 只负责崩溃恢复,业务运行时 select 不会读取 redo log 拿业务数据,redo log 只是操作流水,没有完整数据页结构。
  • BufferPool 是缓存,遵循局部性原理:刚访问过的页大概率还会再次访问。刷脏完成变成干净页,仍然是热点数据,留在内存可以避免重复从磁盘加载。
  • LRU 淘汰触发时机:BufferPool 内存用尽,要加载新的数据页时,才淘汰最久未访问的冷页。
    • 淘汰脏页:先刷脏页落盘,再释放内存;
    • 淘汰干净页:直接丢弃,磁盘已有副本。

  • 七、Explain 关键字段回顾(做索引分析必看)

    字段含义面试关注点
    type 访问类型 ref=等值索引查找;range=范围索引扫描;ALL=全表扫描
    key 实际使用索引 确认是否走了预期索引
    key_len 实际用到索引字节长度 判断复合索引用到多少字段(INT nullable = 5 字节)
    Extra 额外信息 Using index=覆盖索引无回表;Using index condition=ICP;Using MRR=多范围读优化;Using filesort=需额外排序

    八、高频踩坑总结(面试速记)

  • 最左匹配要求索引定义的连续最左前缀,where 条件书写顺序无关,优化器自动调整。
  • 遇到 > < between 范围查询,后面字段不能索引 seek,但可被 ICP 过滤;in 不会打断索引匹配。
  • ICP 减少回表数量,但不能替代索引 seek;覆盖索引直接消除回表。
  • 顺序 IO / 随机 IO 看页面访问次序,不是索引类型;MRR 可以把回表随机 IO 转为顺序 IO。
  • 全部页命中 BufferPool,磁盘 IO 消失,顺序随机 IO 性能无差别。
  • 脏页可读,刷脏不等于驱逐页面;LRU 只有内存不足才淘汰冷页;redo log 只管崩溃恢复,业务查询不会读取 redo log。
  • 复合索引设计原则:等值条件放前面,范围条件尽量放在索引最后。

  • 九、因果链总收束:一条链串起所有概念

    因为 复合索引叶子按 (a,b,c) 全局有序
    → 所以 查询可以从 a 开始二分 seek
    → 因为 排序优先级 a→b→c 依次递减
    → 所以 b 范围后 c 在叶子内部无序
    → 因为 c 无序无法 seek,只能逐行判断
    → 所以 引入 ICP 在引擎层过滤,减少回表
    → 因为 二级索引吐出主键 id 跟随 (a,b,c) 排序,主键乱序
    → 所以 回表需要访问聚簇索引页
    → 因为 聚簇索引页存储在表空间中,页号由表空间分配
    → 所以 回表默认是随机 IO(页号跳跃)
    → 因为 MRR 把主键排序后再访问
    → 所以 回表变成顺序 IO + 触发预读
    → 因为 页全部命中 BufferPool 时不存在磁盘 IO
    → 所以 顺序/随机 IO 概念只在磁盘层有意义

    这条链上的每一个环节,都是前一个环节的必然推论。拿掉任何一节,后面的结论都不成立。


    参考资料

    • 《高性能 MySQL》第 5 章 — 索引设计
    • MySQL 官方文档 — Index Condition Pushdown
    • MySQL 官方文档 — Multi-Range Read Optimization
    • MySQL 官方文档 — InnoDB Buffer Pool
    • MySQL 官方文档 — InnoDB Read-Ahead
    • MySQL 索引下推 ICP 详解 — 博客园
    • MySQL MRR 优化详解 — 博客园
    • InnoDB Buffer Pool LRU 冷热分区 — CSDN
    赞(0)
    未经允许不得转载:网硕互联帮助中心 » MySQL 复合索引深度剖析:最左匹配、ICP、回表、IO 模式底层真相
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!