
一、用excel类比快速理解
想象mysql的存储结构就像一个excel文件的组织方式:
表空间(数据库)
↓
段(sheet页签)→ 数据段、索引段、回滚段
↓
区(若干列的范围)→ 64个页的连续空间
↓
页(一行行的数据)→ 16kb大小,对应excel的一个范围
↓
行(excel中的一行)→ 实际的用户数据
①、段(segment):表空间的大分类
什么是段?
段是最大的逻辑单位,一个表空间由多个段组成。不同的段存放不同类型的数据。
常见的三种段
一个表 article: ┌─────────────────────┐ │ 表空间 │ ├─────────────────────┤ │ ┌─ 数据段 │ │ │ (存放叶子节点) │ │ │ 存储真实用户数据 │ │ │ │ │ ├─ 索引段 │ │ │ (存放非叶节点) │ │ │ 存储b+树的树枝 │ │ │ │ │ └─ 回滚段 │ │ (存放undo log) │ │ 事务回滚时用 │ └─────────────────────┘
具体例子理解
假设你有一个 article 表:
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. 行溢出(一行太大)时怎么办?
可以通过
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数据。
示例:
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字节
示例:
-- 假设我们经常查询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程序员的实际应用
①、为什么批量插入要分批?
// 不好的做法:逐条插入
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();
}
}
②、为什么查询的列数少更快?
-- 慢:读取整行数据 select * from article where id = 1; -- 需要读16kb的页,虽然只需要id和title,但整行都读了 -- 快:只读需要的列 select id, title from article where id = 1; -- 同样读16kb的页,但只解析需要的字段
③、为什么要建立合适的索引?
-- 没有索引:全表扫描 select * from article where author = '张三'; -- 需要读取所有的数据页,可能几百次io -- 有索引:快速定位 create index idx_author on article(author); -- 通过索引段找到相关的页,可能只需几次io
总结记忆口诀
一个表,分成多个段(数据段、索引段、回滚段) 一个段,分成多个区(每个区1mb,包含64个页) 一个区,分成多个页(每个页16kb) 一个页,存储多行数据(根据行大小而定) 磁盘读写的单位是页(16kb) 内存和磁盘之间的交互以页为单位 优化数据库性能的核心就是减少页的读取次数
mysql存储结构:从物理文件到逻辑结构详解
①、物理层:数据库文件是什么?
物理文件结构
你的硬盘:
/var/lib/mysql/your_database/
├── your_table.ibd # 表空间文件(物理文件)
├── other_table.ibd
└── ibdata1 # 系统表空间
这个.ibd文件:
┌──────────────────────────────────────────┐
│ 一堆二进制数据 │
│ 00010101010101010101010101010101... │
│ 01010101010101010101010101010101... │
│ ... 持续几mb到几gb │
└──────────────────────────────────────────┘
肉眼看起来:全是0和1
mysql看起来:有组织的段、区、页、行为什么需要逻辑结构?
如果没有逻辑结构: 文件就是一串连续的字节:0x00 0x01 0x02 ... 0xff 问题: 1. 如何找到id=100的数据? 2. 如何知道哪些数据属于索引? 3. 如何高效管理空间? 4. 如何支持事务回滚? 答案:建立逻辑结构(段、区、页)
②、逻辑层:段是什么?
段在文件中的存在方式
表空间文件.ibd内部:
物理布局:
┌─────────────────────────────────────────┐
│ 区1 │ 区2 │ 区3 │ 区4 │ 区5 │ 区6 │ ... │
│ 1mb │ 1mb │ 1mb │ 1mb │ 1mb │ 1mb │ │
└─────────────────────────────────────────┘
逻辑布局:
数据段 = {区1, 区3, 区5} ← 不一定连续!
索引段 = {区2, 区4} ← 物理上分散,逻辑上统一
回滚段 = {区6} ← 单独管理
段管理器(segment header)记录:
"数据段包含:区1、区3、区5..."③、逻辑层:区是什么?
区是物理上连续的页集合
关键点:区在物理上是连续的! 物理文件.ibd: ┌─────────────────────────────────────────────────┐ │ ... 其他数据 ... │ │ │ │ 区边界(从这里开始连续1mb) │ │ ↓ │ │ 页1 页2 页3 ... 页64 │ │ (16kb) (16kb) (16kb) (16kb) │ │ │ │ ↑ │ │ 这64个页在磁盘上是物理连续的! │ │ │ │ ... 其他数据 ... │ └─────────────────────────────────────────────────┘
④、逻辑层:页是什么?
页是mysql的最小i/o单元
物理事实: 1. 磁盘一次最少读512字节(一个扇区) 2. 文件系统一次最少读4kb(一个块) 3. mysql一次最少读16kb(一个页) 为什么是16kb? 平衡点:太大浪费内存,太小增加io次数 经过测试,16kb在大多数场景下最优
总结
到此这篇关于mysql数据库的段、区、页、行的文章就介绍到这了,更多相关mysql段、区、页、行内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论