InnoDB存储结构:从索引树到回表查询的完整逻辑链

250 阅读4分钟

1.png

2.png

InnoDB存储结构:从索引树到回表查询的完整逻辑链


一、一张表创建时发生了什么?

当你在MySQL中执行CREATE TABLE语句时,InnoDB会自动生成聚簇索引。即使没有显式定义主键,它也会创建一个隐藏的自动递增行ID作为主键 。

举个例子:你创建了一个用户表:

CREATE TABLE user (
  id INT PRIMARY KEY,
  name VARCHAR(20),
  age INT,
  INDEX idx_age (age)
);

此时InnoDB做了两件事:

  1. 聚簇索引:以id为主键构建一棵B+树,叶子节点存储完整的行数据(包括idnameage)。
  2. 二级索引:以age为键值构建另一棵B+树,叶子节点只存储age值和对应的主键id

二、B+树长什么样?

1. 结构解剖

想象一棵倒置的树,由三种节点组成:

  • 根节点:树的顶端,存放目录项(比如“主键1-100在页A,101-200在页B”)。
  • 非叶子节点:中间层的导航员,存储指向子节点的指针和键值范围。
  • 叶子节点:最底层的数据仓库,存储实际数据(聚簇索引)或主键值(二级索引)

2. 关键特性

  • 高度通常为2-3层:一个三层B+树可支撑千万级数据,查询只需2-3次磁盘IO
  • 叶子节点双向链表:支持高效的范围查询(比如WHERE age BETWEEN 20 AND 30
  • 页面填充率:顺序插入时页面填充约15/16,随机插入时填充1/2到15/16

三、覆盖索引、回表、索引下推的本质

1. 回表:多跑一趟的代价

假设执行以下查询:

SELECT * FROM user WHERE age = 25;  

流程如下:

  1. 二级索引查找:通过idx_age找到age=25对应的主键id
  2. 聚簇索引回表:用id去聚簇索引中取出整行数据。

为什么叫“回表”? 因为需要从二级索引跳回聚簇索引查完整数据

2. 覆盖索引:省去回表的捷径

如果查询只需索引中的字段,比如:

SELECT id, age FROM user WHERE age = 25;  

此时idx_age的叶子节点已包含idage,无需回表直接返回结果,性能提升10倍以上 这里覆盖索引能生效的前提是 索引为(age,id)而非(id,age)。 如果你只查了age,必须保证age在最左侧。

3. 索引下推:过滤提前到引擎层

在MySQL 5.6后,如果查询条件包含索引列非索引列: 这里的age是索引列,而name也是索引列。如果数据库支持索引合并,则age和name为单独索引也可以实现索引下推。如果数据库不支持索引合并,则只支持(age,name)的联合索引。 其实是构建了一个 (name, age) 的 B+ 树。索引下推将部分 WHERE 条件下推到存储引擎层执行,利用索引中的字段提前过滤数据,减少回表次数。非索引下推时,存储引擎仅处理最左前缀条件,其他条件的过滤由服务层完成,导致更多回表操作。索引下推更通俗的理解是:一次就把联合索引中的每一个字段都使用完,而非只用最左侧字段去回表,然后在回表查到的数据里再判断第二个字段是否符合要求(这一步的过滤属于服务层)。没有使用索引下推会增加额外的回表次数

SELECT * FROM user WHERE age = 25 AND name LIKE '张%';  

传统流程:先通过age=25找到所有id,回表后再过滤name
索引下推优化:在二级索引层直接过滤name LIKE '张%',减少回表次数


四、实战:索引如何影响存储?

1. 插入一条记录时

  • 聚簇索引:数据按主键顺序插入叶子节点。若页面已满,触发页分裂(分裂后新旧页各保留约一半数据)
  • 二级索引:若页面不在内存中,写入 插入缓冲(Change Buffer) ,后续异步合并到磁盘,减少随机IO

2. 删除一条记录时

  • 记录被标记为删除,空间进入空闲链表,但物理上可能长期保留(直到页内空间不足或重建表)

五、思考题

  1. 为什么主键不宜过长?
    二级索引的叶子节点存储主键值,过长会导致二级索引占用更多空间
  2. 如何选择索引列顺序?
    联合索引(a,b)能优化WHERE a=1 AND b=2,但无法跳过a直接查b

总结:理解InnoDB存储结构的关键是将索引视为数据的入口。聚簇索引是“数据本身按主键排队”,二级索引是“快速找到主键的导航图”。通过减少回表、利用覆盖索引和下推优化,才能最大化索引的威力。