ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

InnoDB 数据到底怎么存?Page、Extent、Segment 与 B+Tree

InnoDB 数据到底怎么存?Page、Extent、Segment 与 B+Tree InnoDB 从磁盘读取数据时并不是一行一行读取而是以Page为基本单位把数据页加载到Buffer Pool中。这些 Page 在磁盘上到底是怎么组织起来的BTree 和 Page 又是什么关系1. 最底层先从 Row 开始我们平时操作的数据是SELECT*FROMuserWHEREid10;从业务角度看我们操作的是一条Row。例如id 10 name 张三 age 20但 InnoDB 并不是直接磁盘 ↓ 一行 ↓ 一行 ↓ 一行这样管理数据。很多 Row 会被放进一个Page中。所以物理存储层面更接近Page ├── Row 1 ├── Row 2 ├── Row 3 ├── Row 4 └── ...因此Row是我们看到的逻辑记录而Page才是 InnoDB 非常重要的存储和 I/O 单位。2. 什么是 PageInnoDB 会把表和索引的数据划分成一个个 Page。默认情况下一个 InnoDB Page 大小是16KB。MySQL 8.4 的innodb_page_size默认值仍然是16384字节而且这个值是在初始化实例时确定的。所以磁盘文件可以简单想象成Page 0 16KB Page 1 16KB Page 2 16KB Page 3 16KB Page 4 16KB ...Buffer Pool缓存的基本单位也是 Page。所以磁盘中的 Page ↓ 读取 ↓ Buffer Pool 中的 Page ↓ 查询 / 修改这就把物理存储和 Buffer Pool 联系起来了。3. Page 里面不一定只有数据行不要把 Page 简单理解成一个 Page 就是装很多 Row 的盒子。InnoDB 中实际上存在很多不同类型的 Page。例如可能有BTree索引页、Undo Log页、系统页、LOB 数据页、Extent 描述页等。MySQL 8.4 的内部信息中就可以看到INDEX、UNDO_LOG、EXTENT_DESCRIPTOR、LOB_DATA等不同 Page 类型。学习普通表数据时最需要关注的是索引 Page。因为 InnoDB 的表数据和普通索引本质上都是通过 B-tree/BTree 类的索引结构组织起来的。官方文档说明表数据和二级索引都会组织成 B-tree 索引结构。4. BTree 和 Page 到底是什么关系[20 | 50] / | \ / | \ [1~19] [20~49] [50~...]很容易把这里的每一个框理解成一个抽象节点。实际上到了 InnoDB 物理实现中可以简单理解BTree 的一个节点通常就是一个 Page。例如Page 1 [20 | 50] / | \ / | \ ↓ ↓ ↓ Page 2 Page 3 Page 4也就是说BTree Node ≈ Index PageMySQL 官方文档明确说明InnoDB 索引记录存储在 B-tree 的叶子 Page 中默认索引 Page 大小为16KB。5. 非叶子 Page 里面存什么假设有主键CREATETABLEuser(idBIGINTPRIMARYKEY,nameVARCHAR(50),ageINT);聚簇索引可能简化成Root Page [100 | 500] / | \ ↓ ↓ ↓ Page A Page B Page C上层非叶子 Page 的主要任务不是保存完整行数据。而是保存索引键 指向下一层 Page 的信息。所以可以简单理解非叶子 Page [主键范围 / 索引键] [子 Page 指针]它的作用是导航。例如查询SELECT*FROMuserWHEREid520;可以Root Page ↓ 判断 520 应该走哪个分支 ↓ 下一层 Page ↓ 继续定位最终找到叶子 Page。6. 聚簇索引的叶子 Page 存什么InnoDB 每张表都有一个Clustered Index也就是聚簇索引。通常主键索引就是聚簇索引。聚簇索引的叶子节点保存完整行数据。MySQL 官方也明确说明InnoDB 的 clustered index 存储行数据。所以PRIMARY BTree 非叶子 Page ↓ ┌──────┴──────┐ ↓ ↓ Leaf Page Leaf Page ↓ ↓ 完整 Row 完整 Row例如叶子 Page 中可能保存Page A id1 name张三 age20 id2 name李四 age21 id3 name王五 age22 ...以前说InnoDB 的数据本身就是通过聚簇索引组织的。更具体地理解为完整行数据存储在聚簇索引 BTree 的叶子 Page 中。7. 那二级索引的叶子 Page 呢假设建立CREATEINDEXidx_ageONuser(age);这时候又会产生一棵 BTree。但是二级索引叶子节点不会再保存一份完整行数据。它主要保存二级索引列 主键值。例如idx_age Leaf Page age18 → id8 age20 → id1 age21 → id2 age25 → id15所以执行SELECT*FROMuserWHEREage20;可能先idx_age BTree ↓ 找到 age 20 ↓ 获得 id 1然后再PRIMARY BTree ↓ 根据 id 1 ↓ 找到完整 Row这就是我们前面学过的回表。现在你应该发现所谓回表本质上就是从二级索引 BTree 的 Page又去访问聚簇索引 BTree 的 Page。8. 为什么 Page 不够用时会发生 Page Split现在也可以从物理角度重新理解Page Split。假设一个叶子 Page 已经快满了Page A 10 20 30 40 50 60 70 ...这时候继续插入大量记录。如果 Page 已经没有足够空间就不能无限往这个 16KB Page 中塞。于是可能需要Page Split页分裂。例如原来Page A 10 20 30 40 50 60 70 80分裂以后Page A Page B 10 20 30 40 50 60 70 80同时上层 BTree Page 也需要调整相应的索引信息。这就是为什么我们以前说主键尽量保持相对有序可以减少频繁的 Page Split 和数据移动。MySQL 官方说明在向聚簇索引插入新记录时InnoDB 默认还会尝试保留一部分 Page 空间为未来的插入和更新留出余量。9. 一个 Page 只能存有限的数据默认一个 Page 是16KB。假设一条 Row 很小那么一个 Page 可以保存很多 Row。但是如果一行特别大呢例如CREATETABLEarticle(idBIGINTPRIMARYKEY,titleVARCHAR(200),contentLONGTEXT);content可能非常大。显然不能简单理解成不管一行多大都必须完整塞进一个 16KB Page。InnoDB 支持off-page storage。也就是说对于很大的变长字段部分内容可以放到其他 Page 中当前记录中保存相应的引用信息。具体行为还会受ROW_FORMAT影响。MySQL 8.4 官方文档也明确说明当记录超过 Page 内可存储限制时会把部分可变长度列放到 Page 外部存储。所以普通 Row ↓ 主要存在索引 Page 中 超大 VARCHAR / TEXT / BLOB ↓ 可能使用额外的 Overflow / LOB Page因此一行数据和一个 Page 并不是严格的一对一关系。10. Page 再往上一层是什么如果整个表有几百万、几千万条数据就会有大量 Page。例如Page 1 Page 2 Page 3 ... Page 100000如果 InnoDB 永远以单个 Page 为单位进行磁盘空间管理一个 Page 一个 Page 地申请、分配、回收。管理成本也会很高。所以 InnoDB 在 Page 之上又引入了更大的空间管理单位Extent。中文一般叫区。11. 什么是 Extent在默认16KB Page下一个 Extent 通常大小为1MB而1MB ÷ 16KB 64所以一个 Extent 包含64 个连续 Page。可以画成Extent 1MB ├── Page 0 16KB ├── Page 1 16KB ├── Page 2 16KB ├── ... └── Page 63 16KBMySQL 8.4 官方文档明确说明在默认 16KB Page 下一个 1MB Extent 由 64 个连续 Page 组成。所以可以先记Page 是基本存储/I/O 单位Extent 是更大的空间分配单位。12. 为什么需要 Extent假设一棵 BTree 越来越大。最开始只有Page 1 Page 2 Page 3后来变成几千 Page 几万 Page 几十万 Page如果每次需要空间都申请一个 Page ↓ 再申请一个 Page ↓ 再申请一个 Page空间管理会比较零碎。所以当数据规模变大以后InnoDB 可以一次分配一整个 Extent。例如BTree 需要扩容 ↓ 申请 Extent ↓ 一次获得一批 Page这样更方便空间管理也有助于让相关 Page 在磁盘上保持较好的局部性。13. Extent 一定是 1MB 吗不能绝对这么说。如果使用默认innodb_page_size 16KB那么Extent 1MB 64 Pages这是我们最常见的情况。但是 MySQL 8.4 支持不同 Page Size。对于4KB、8KB、16KBPageExtent 大小都是1MB32KB Page 对应 2MB Extent64KB Page 对应 4MB Extent。所以学习阶段最常用的记法是默认情况下1 Page 16KB1 Extent 1MB 64 Pages。14. Extent 上面是什么继续往上就是Segment。中文通常叫段。很多文章会直接画Segment ↓ Extent ↓ Page这个理解方向没错。但是这里需要注意一个细节Segment 并不是从出生开始就一定由完整 Extent 组成。MySQL 官方说明一个 Segment 刚开始增长时前 32 个 Page 可以逐个分配之后随着 Segment 变大才开始主要按照整个 Extent 分配。所以严格来说Segment ├── 一些单独分配的 Page │ └── 多个 Extent ↓ Page会比简单画Segment ↓ Extent ↓ Page更加准确。15. Segment 到底是干什么的可以把Segment理解成InnoDB 为某一类相关数据 Page 建立的逻辑空间集合。例如 BTree 不断增长BTree ↓ 需要越来越多 PageInnoDB 就需要管理哪些 Page / Extent 属于这棵 BTree 的某个部分于是通过 Segment 进行组织。官方把 Segment 描述为 Tablespace 中的一个逻辑划分并且 Segment 可以随着数据增加继续增长。16. 一棵索引只有一个 Segment 吗这里特别容易被简化错。实际上 MySQL 官方说明InnoDB 的每个索引会分配两个 Segment。一个用于BTree 非叶子节点另一个用于BTree 叶子节点。也就是一个 BTree Index ┌────────────────────┐ │ │ ↓ ↓ 非叶子节点 Segment 叶子节点 Segment ↓ ↓ Root / Internal Page Leaf Page为什么要分开因为叶子节点的数据非常重要而且访问特点和非叶子节点并不完全一样。官方还特别提到让叶子节点尽可能具有良好的磁盘连续性有利于顺序 I/O。17. 主键索引的 Segment 又意味着什么聚簇索引叶子节点保存完整 Row。所以PRIMARY BTree ↓ Leaf Segment ↓ Leaf Pages ↓ 完整 Row也就是说表的真正行数据本质上就位于聚簇索引叶子 Page 所属的空间结构中。这也再次解释为什么 InnoDB 中“表数据”和“主键索引”不是完全分开的两个东西。因为聚簇索引本身就在组织表数据。18. Segment 的 Extent 一定连续吗不一定。这是第二个容易误解的地方。一个Extent内部的 Page 是连续的。例如默认情况下Extent A Page 0 Page 1 Page 2 ... Page 63它们是连续的一组 Page。但是一个 Segment 可能拥有Extent 1 Extent 8 Extent 20 Extent 31这些 Extent 本身不要求在整个数据文件中完全连续。所以Extent 内部强调连续 Page但组成一个 Segment 的多个 Extent 不一定彼此连续。MySQL 官方关于 InnoDB Extent 的资料也明确说明Segment 由 Extent 组成而 Segment 可以包含非连续的 Extent。19. Segment 再往上是什么继续往上就是Tablespace中文叫表空间。Tablespace 是 InnoDB 更高层次的逻辑存储空间。里面最终由大量 Page 组成。可以粗略理解Tablespace │ ├── Segment │ │ │ ├── Extent │ │ ├── Page │ │ ├── Page │ │ └── ... │ │ │ └── Extent │ └── SegmentMySQL 官方的定义也是每个 Tablespace 由数据库 Page 构成Page 再被组织为 Extent内部再通过 Segment 等结构进行空间管理。20. Tablespace 和 .ibd 文件是什么关系MySQL 现在非常常见的一种模式是File-Per-Table也就是每张 InnoDB 表拥有自己的表空间文件。MySQL 8.4 中innodb_file_per_table默认开启新建表默认会创建独立的 file-per-table tablespace对应文件通常就是.ibd。例如user 表 ↓ user.ibd可以简单理解成user.ibd └── File-Per-Table Tablespace │ ├── 各种 Segment │ ├── Extent │ ├── Page │ └── 表 / 索引相关数据但需要注意Tablespace是逻辑概念.ibd是实际的数据文件。在 file-per-table 场景下它们之间关系非常紧密因此入门阶段经常会把它们放在一起理解。另外 InnoDB 还有System Tablespace、General Tablespace、Undo Tablespace、临时表空间等不同类型并不是所有东西都一定放在某张表自己的.ibd中。21. 所以完整层级到底怎么理解现在可以把整个结构串起来Tablespace │ 空间管理范围 │ ┌──────────┴──────────┐ ↓ ↓ Segment Segment │ │ Extent │ ┌────┼────┬────┐ ↓ ↓ ↓ ↓ Page Page Page Page │ ↓ BTree Node │ ↓ Row默认配置下可以记成Tablespace ↓ Segment ↓ Extent ↓ 1MB ↓ 64 × Page ↓ 每个 Page 16KB ↓ 存放索引记录 / Row 等内容但是不要忘记Segment 前期也可能直接获得单独 Page并不是永远只按完整 Extent 分配。22. 再把 BTree 放进这张图假设我们有CREATETABLEuser(idBIGINTPRIMARYKEY,nameVARCHAR(50),ageINT,INDEXidx_age(age));那么逻辑上存在PRIMARY聚簇索引 BTree和idx_age二级索引 BTree。可以简化成user.ibd ↓ Tablespace │ ┌─────────────┴─────────────┐ ↓ ↓ PRIMARY BTree idx_age BTree │ │ ┌───┴───┐ ┌───┴───┐ ↓ ↓ ↓ ↓ 非叶子段 叶子段 非叶子段 叶子段 ↓ ↓ ↓ ↓ Page Page Page Page ↓ ↓ 完整 Row age 主键这样我们之前学过的聚簇索引二级索引回表PageBuffer Pool就全部连起来了。23. 查询一条数据到底经历了什么假设SELECT*FROMuserWHEREid100;现在可以从物理角度理解PRIMARY BTree ↓ 读取 Root Page ↓ 找到下一层 Page ↓ 继续定位 ↓ Leaf Page ↓ 找到 id 100 的完整 Row这些 Page如果已经在Buffer Pool直接内存读取如果不在磁盘 Tablespace / .ibd ↓ 读取 Page ↓ Buffer Pool ↓ 继续查询所以BTree 决定“应该找哪个 Page”Buffer Pool 决定“这个 Page 能不能直接从内存拿到”。这是一个非常重要的理解。24. 为什么 BTree 层数少很重要现在还能进一步理解为什么我们总说BTree 非常适合数据库索引。因为每访问一个 BTree 节点本质上可能就是访问一个 Page。例如Root Page ↓ Internal Page ↓ Leaf Page只有三层。如果相关 Page 都在 Buffer Pool几次内存 Page 访问就能找到数据。即使部分 Page 不在 Buffer Pool也只需要读取有限数量的数据页。而一个 16KB 非叶子 Page 可以容纳很多索引项因此一个 Page 就能指向大量下一层 Page。所以 BTree 可以在树高很低的情况下管理非常多的数据。这也是数据库索引使用 BTree 类结构的重要原因之一。25. 删除一条 Row 会立刻让 .ibd 文件变小吗通常不能这样理解。删除数据以后DELETEFROMuserWHEREid100;InnoDB 会逐渐清理相应记录并可能释放 Page 或 Extent 内部的空间。这些空间可以重新被 InnoDB 使用。但是文件内部空间被释放不代表操作系统看到的 .ibd 文件大小一定马上缩小。官方文档也说明删除数据后 B-tree 会收缩而空间能否重新成为 Tablespace 中的可用空间取决于释放的是单独 Page 还是整个 Extent 等情况。因此经常会出现大量 DELETE ↓ 表中数据少了 ↓ .ibd 文件看起来还是很大这是正常可能出现的现象。26. 最容易搞混的几个地方第一Row不是 InnoDB 磁盘 I/O 的基本单位。真正非常重要的是Page。第二Page不等于一个 Row。一个 Page 通常可以保存多条索引记录大字段还可能使用额外 Page。第三BTree Node不是纯粹抽象概念。在 InnoDB 中可以把一个 BTree 节点理解为一个索引 Page。第四Extent是一组连续 Page。默认 16KB Page 时一个 1MB Extent 有 64 个 Page。第五Segment并不是简单的一整块连续磁盘空间。一个 Segment 可以管理多个 Extent这些 Extent 不一定彼此连续而且 Segment 较小时还可能逐 Page 分配。第六一个 InnoDB 索引并不只是简单对应一个 Segment。官方实现中会为每个索引分别管理叶子节点和非叶子节点 Segment。
返回列表