MySQL的段、区、页、行 详解

一、用Excel类比快速理解

想象MySQL的存储结构就像一个Excel文件的组织方式:

复制代码
表空间(数据库)
    ↓
段(Sheet页签)→ 数据段、索引段、回滚段
    ↓
区(若干列的范围)→ 64个页的连续空间
    ↓
页(一行行的数据)→ 16KB大小,对应Excel的一个范围
    ↓
行(Excel中的一行)→ 实际的用户数据

①、段(Segment):表空间的大分类

什么是段?

段是最大的逻辑单位 ,一个表空间由多个段组成。不同的段存放不同类型的数据。

常见的三种段

复制代码
一个表 article:
┌─────────────────────┐
│  表空间              │
├─────────────────────┤
│ ┌─ 数据段            │
│ │  (存放叶子节点)    │
│ │  存储真实用户数据    │
│ │                   │
│ ├─ 索引段            │
│ │  (存放非叶节点)    │
│ │  存储B+树的树枝     │
│ │                   │
│ └─ 回滚段            │
│    (存放undo log)  │
│    事务回滚时用       │
└─────────────────────┘

具体例子理解

假设你有一个 article 表:

sql 复制代码
CREATE TABLE article (
    id INT PRIMARY KEY,
    title VARCHAR(255),
    content TEXT,
    author VARCHAR(100)
);

-- 创建索引
CREATE INDEX idx_author ON article(author);

此时MySQL会创建的段:

article表空间:
1. 数据段 - 存储所有行的实际数据

├─ id=1, title="MySQL教程", content="详细讲解", author="张三"

├─ id=2, title="Java指南", content="从入门到精通", author="李四"

└─ id=3, title="Python基础", content="快速上手", author="王五"

2. 索引段(主键索引) - 存储B+树的非叶子节点

└─ 树枝节点(不是真实数据,只是指向)

3. 索引段(idx_author索引) - 存储辅助索引的非叶子节点

└─ 树枝节点(不是真实数据,只是指向)

4. 回滚段 - 存储undo log

└─ UPDATE时的旧值记录


②、区(Extent):段内的分组单位

什么是区?

区是连续页的集合 ,一个区通常包含 64个连续的页

复制代码
计算:
1个页 = 16 KB
1个区 = 64个页 = 64 × 16KB = 1024KB = 1MB

为什么要用区?

不用区(逐个分配页)的问题:

假设你有1000万行数据要存储:

复制代码
第一次磁盘IO:读取第1个页(16KB)
第二次磁盘IO:读取第2个页(16KB)
...
第一百万次磁盘IO:读取第100万个页

总共要做100万次磁盘寻道!
效率太低,因为磁盘寻道时间很长(5-10毫秒)

用区(64个页一起分配)的好处:

复制代码
第一次磁盘IO:读取区1(64个页,1MB)
第二次磁盘IO:读取区2(64个页,1MB)
...

总共只要做1万次磁盘寻道,效率提高100倍!
这就是为什么用区而不用单独的页

区的物理结构

复制代码
磁盘上的实际分布:

段内存布局:
┌─────────────────────────────────────────────┐
│ 区1                                         │
│ ┌─────────────────────────────────────────┐ │
│ │ 页1  │ 页2  │ 页3  │ ... │ 页64        │ │
│ │16KB  │16KB  │16KB  │     │ 16KB        │ │
│ │      │      │      │     │ (共1MB)     │ │
│ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ 区2                                         │
│ ┌─────────────────────────────────────────┐ │
│ │ 页65 │ 页66 │ 页67 │ ... │ 页128       │ │
│ │16KB  │16KB  │16KB  │     │ 16KB        │ │
│ │      │      │      │     │ (共1MB)     │ │
│ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ 区3 ...                                     │
└─────────────────────────────────────────────┘

③、页(Page):数据存储的基本单元

什么是页?

页是InnoDB最小的IO单位,标准大小为 16 KB

关键点:
一次磁盘IO的最小单位就是1个页

不管你只查询1行还是1000行,如果在同一个页内,都是一次IO

如果数据跨多个页,就需要多次IO

页的内部结构

一个16KB的页分为几个部分:

复制代码
一个页(16KB = 16384字节)的结构:

┌──────────────────────────────────────────┐
│ 页头(Page Header)- 48字节              │
│ 记录页的状态信息、行数等                 │
├──────────────────────────────────────────┤
│                                          │
│ 用户数据区(User Records)- 大约15KB    │
│ 存储实际的行数据                         │
│                                          │
│ 例如:                                   │
│ ┌──────────────────────────────────┐     │
│ │行1: id=1, title="MySQL", ...     │     │
│ ├──────────────────────────────────┤     │
│ │行2: id=2, title="Java", ...      │     │
│ ├──────────────────────────────────┤     │
│ │行3: id=3, title="Python", ...    │     │
│ ├──────────────────────────────────┤     │
│ │... 直到页满(大约16KB)           │     │
│ └──────────────────────────────────┘     │
│                                          │
├──────────────────────────────────────────┤
│ 页尾(Page Trailer)- 8字节              │
│ 校验和等信息                             │
└──────────────────────────────────────────┘

一个页能存多少行数据?

取决于每行的大小:

复制代码
假设你的article表每行数据大小为500字节:

一个页(16KB)能存多少行?
16384 字节 ÷ 500字节/行 = 32行

假设你的user表每行数据大小为200字节:
16384 字节 ÷ 200字节/行 = 81行

所以行数不固定,取决于列定义的大小

磁盘IO的体现

复制代码
示例:SELECT * FROM article WHERE id = 2

执行过程:
1. MySQL找到存储id=2的页位置(假设在第5个页)
2. 从磁盘读取第5个页到内存(一次IO,读取16KB)
3. 在内存中搜索id=2的记录
4. 返回数据给应用

关键:不管这个页里有几行,一次都是读16KB!

④、行(Row):实际的用户数据

什么是行?

行是MySQL存储的最小逻辑单位,对应一条记录。

复制代码
article表的一行数据:
┌─────────────────────────────────────────┐
│ id=1                                    │
│ title="深入理解MySQL"                  │
│ content="这是一篇很长的文章..."         │
│ author="张三"                           │
└─────────────────────────────────────────┘

行格式(Row Format)

Excel的一行(用户数据)


如何拆分单元格?(行格式决定)

单元格内容存储在哪里?(行内或行外)

MySQL有多种行存储格式,MySQL 8.0默认是 DYNAMIC

复制代码
行格式对比:

COMPACT(较早版本):
┌──────────────────────────────────┐
│ 变长字段长度 │ NULL标识位 │ 字段值 │
│(一起存储)   │            │       │
└──────────────────────────────────┘
如果数据超过限制 → 在溢出页中存储指针

DYNAMIC(MySQL 8.0默认):
┌──────────────────────────────────┐
│ 变长字段长度 │ NULL标识位 │ 字段值 │
│(一起存储)   │            │       │
└──────────────────────────────────┘
更激进地将长数据存储到溢出页
比COMPACT更节省当前页的空间

MySQL行格式:可以决定内容放在当前单元格还是另开一个"备注页"

关键点:

  1. 一行中的所有列值如何组织?

  2. 变长字段(VARCHAR、TEXT)如何存储?

  3. NULL值如何处理?

  4. 行溢出(一行太大)时怎么办?

可以通过

sql 复制代码
 show table status like '%article%' 

查看行格式。

行溢出(Row Overflow)

当一行的某个字段(比如TEXT、VARCHAR)特别大时:

复制代码
一个页只有16KB,但某行数据超过16KB怎么办?

DYNAMIC格式的处理:
┌─────────────────────────────────┐
│ 当前页(16KB)                  │
├─────────────────────────────────┤
│ id=1                            │
│ title="标题"(很短)            │
│ content=指向溢出页1的指针 →    │
│ author="张三"                   │
│                                 │
│ 其他行...                       │
│                                 │
└─────────────────────────────────┘
       ↓
┌─────────────────────────────────┐
│ 溢出页(Overflow Page)         │
├─────────────────────────────────┤
│ 存储超大的content字段值         │
│ "这是一篇超级超级长的内容..."  │
│ "可以超过1MB..."               │
└─────────────────────────────────┘

DYNAMIC格式的特点:

如果一行总大小超过页的阈值(通常是8KB):

DYNAMIC的做法:整行都放溢出页?

不对!更准确地说:

TEXT/BLOB等大字段:

  • 完全存储在溢出页
  • 行内只留20字节的指针

例如一个2KB的TEXT字段:

在行内:20字节指针

在溢出页:完整的2KB数据。

示例:

sql 复制代码
CREATE TABLE blog (
    id INT PRIMARY KEY,
    title VARCHAR(255),
    content TEXT,        -- 可能很大,几MB
    summary VARCHAR(500)
) ROW_FORMAT=DYNAMIC;

-- 存储方式:
-- 行内:id、title、summary、content指针
-- 溢出页:完整的content内容

COMPACT格式的特点

TEXT/BLOB字段处理:

  • 前768字节存储在行内

  • 剩余部分存储在溢出页

  • 行内额外存储20字节指针指向剩余部分

总行内占用:768 + 20 = 788字节

示例:

sql 复制代码
-- 假设我们经常查询content的前500个字符
SELECT LEFT(content, 500) FROM blog WHERE id = 1;

-- COMPAT格式的优势:
-- 数据可能在行内(前768字节),不需要访问溢出页
-- 减少一次磁盘IO

完整的层级关系图

复制代码
表空间(整个数据库文件)
    │
    ├─ 数据段 Segment
    │   │
    │   ├─ 区1 Extent (1MB = 64 × 16KB)
    │   │   ├─ 页1 Page (16KB)
    │   │   │   ├─ 行1 Row (id=1, title="MySQL", ...)
    │   │   │   ├─ 行2 Row (id=2, title="Java", ...)
    │   │   │   ├─ 行3 Row (id=3, title="Python", ...)
    │   │   │   └─ ... 更多行直到页满
    │   │   ├─ 页2 Page (16KB)
    │   │   │   ├─ 行32 Row
    │   │   │   ├─ 行33 Row
    │   │   │   └─ ... 继续
    │   │   └─ ... 页64 Page
    │   │
    │   ├─ 区2 Extent (1MB)
    │   │   ├─ 页65 Page
    │   │   └─ ... 页128 Page
    │   │
    │   └─ 区N Extent
    │
    └─ 索引段 Segment
        ├─ 区1 Extent
        │   ├─ 页1 Page (存储B+树非叶节点)
        │   └─ ...
        └─ ...

磁盘IO成本分析

小表(< 1MB)

复制代码
表 small_user (100行,总大小10KB)

初期分配:
- 分配1个区(64个页,1MB)
- 但实际只用了1个页(16KB)
- 浪费了:1MB - 16KB = 992KB

磁盘IO:读取整个表只需1次IO

中等表(1-100MB)

复制代码
表 article (100万行,总大小50MB)

初期分配:
- 分配50个区(50 × 1MB = 50MB)
- 总页数:50 × 64 = 3200个页

扫描整个表需要多少次IO?
- 最坏情况:3200次IO
- 实际顺序扫描:会优化成连续读取,减少寻道时间

查询时的磁盘IO

查询1:SELECT * FROM article WHERE id = 5

  • 通过主键索引定位到某一页

  • 1次IO读取该页(16KB)

  • 返回数据

查询2:SELECT * FROM article WHERE id IN (1,2,3,4,5)

  • 如果这5行在同一个页 → 1次IO

  • 如果分在3个页 → 3次IO

  • 如果分在5个页 → 5次IO

所以为什么要建索引?

  • 通过索引快速定位所需的页

  • 减少需要读取的页数

  • 减少磁盘IO


Java程序员的实际应用

①、为什么批量插入要分批?

java 复制代码
// 不好的做法:逐条插入
for (int i = 0; i < 100000; i++) {
    // 每条INSERT都会涉及:
    // 1. 找到对应的页
    // 2. 修改页内数据
    // 3. 写redo log
    // 4. 提交事务
    // 100000次操作,效率很低
    insert(data);
}

// 更好的做法:批量插入
List<Data> batch = new ArrayList<>();
for (int i = 0; i < 100000; i++) {
    batch.add(data);
    if (batch.size() == 1000) {
        // 1000条一起提交
        batchInsert(batch);
        batch.clear();
    }
}

②、为什么查询的列数少更快?

sql 复制代码
-- 慢:读取整行数据
SELECT * FROM article WHERE id = 1;
-- 需要读16KB的页,虽然只需要id和title,但整行都读了

-- 快:只读需要的列
SELECT id, title FROM article WHERE id = 1;
-- 同样读16KB的页,但只解析需要的字段

③、为什么要建立合适的索引?

sql 复制代码
-- 没有索引:全表扫描
SELECT * FROM article WHERE author = '张三';
-- 需要读取所有的数据页,可能几百次IO

-- 有索引:快速定位
CREATE INDEX idx_author ON article(author);
-- 通过索引段找到相关的页,可能只需几次IO

总结记忆口诀

复制代码
一个表,分成多个段(数据段、索引段、回滚段)
一个段,分成多个区(每个区1MB,包含64个页)
一个区,分成多个页(每个页16KB)
一个页,存储多行数据(根据行大小而定)

磁盘读写的单位是页(16KB)
内存和磁盘之间的交互以页为单位
优化数据库性能的核心就是减少页的读取次数

MySQL存储结构:从物理文件到逻辑结构详解

①、物理层:数据库文件是什么?

物理文件结构

sql 复制代码
你的硬盘:
/var/lib/mysql/your_database/
    ├── your_table.ibd     # 表空间文件(物理文件)
    ├── other_table.ibd
    └── ibdata1            # 系统表空间

这个.ibd文件:
┌──────────────────────────────────────────┐
│ 一堆二进制数据                           │
│ 00010101010101010101010101010101...      │
│ 01010101010101010101010101010101...      │
│ ... 持续几MB到几GB                       │
└──────────────────────────────────────────┘

肉眼看起来:全是0和1
MySQL看起来:有组织的段、区、页、行

为什么需要逻辑结构?

sql 复制代码
如果没有逻辑结构:
文件就是一串连续的字节:0x00 0x01 0x02 ... 0xFF

问题:
1. 如何找到id=100的数据?
2. 如何知道哪些数据属于索引?
3. 如何高效管理空间?
4. 如何支持事务回滚?

答案:建立逻辑结构(段、区、页)

②、逻辑层:段是什么?

段在文件中的存在方式

sql 复制代码
表空间文件.ibd内部:

物理布局:
┌─────────────────────────────────────────┐
│ 区1 │ 区2 │ 区3 │ 区4 │ 区5 │ 区6 │ ... │
│ 1MB │ 1MB │ 1MB │ 1MB │ 1MB │ 1MB │     │
└─────────────────────────────────────────┘

逻辑布局:
数据段 = {区1, 区3, 区5}   ← 不一定连续!
索引段 = {区2, 区4}        ← 物理上分散,逻辑上统一
回滚段 = {区6}            ← 单独管理

段管理器(Segment Header)记录:
"数据段包含:区1、区3、区5..."

③、逻辑层:区是什么?

区是物理上连续的页集合

sql 复制代码
关键点:区在物理上是连续的!

物理文件.ibd:
┌─────────────────────────────────────────────────┐
│ ... 其他数据 ...                                 │
│                                                  │
│ 区边界(从这里开始连续1MB)                      │
│ ↓                                                │
│ 页1     页2     页3     ...     页64             │
│ (16KB)  (16KB)  (16KB)          (16KB)          │
│                                                  │
│ ↑                                                │
│ 这64个页在磁盘上是物理连续的!                   │
│                                                  │
│ ... 其他数据 ...                                 │
└─────────────────────────────────────────────────┘

④、逻辑层:页是什么?

页是MySQL的最小I/O单元

sql 复制代码
物理事实:
1. 磁盘一次最少读512字节(一个扇区)
2. 文件系统一次最少读4KB(一个块)
3. MySQL一次最少读16KB(一个页)

为什么是16KB?
平衡点:太大浪费内存,太小增加IO次数
经过测试,16KB在大多数场景下最优
相关推荐
天若有情67313 小时前
校园二手交易系统实战开发全记录(vue+SpringBoot+MySQL)
vue.js·spring boot·mysql
while(1){yan}13 小时前
MyBatis Generator
数据库·spring boot·java-ee·mybatis
それども13 小时前
MySQL affectedRows 计算逻辑
数据库·mysql
是小章啊13 小时前
MySQL 之SQL 执行规则及索引详解
数据库·sql·mysql
富士康质检员张全蛋14 小时前
JDBC 连接池
数据库
yangminlei14 小时前
集成Camunda到Spring Boot项目
数据库·oracle
ChineHe15 小时前
Redis数据类型篇002_详解Strings核心命令与存储结构
数据库·redis·缓存
清水白石00815 小时前
《从零到进阶:Pydantic v1 与 v2 的核心差异与零成本校验实现原理》
数据库·python
电商API&Tina15 小时前
京东 API 数据采集接口接入与行业分析
运维·服务器·网络·数据库·django·php