表空间
删掉了表里一半的数据,
data_free显示有几百 MB 碎片,磁盘上的.ibd文件却一点没变小。为什么?因为 InnoDB 从不把释放的空间还给操作系统,只把它标记为「可复用」。理解表空间,就是理解它管理磁盘空间的这套三级结构。
1. 表空间与内部结构
1.1 表空间:InnoDB 存放数据的容器
表空间(Tablespace)是 InnoDB 在磁盘上存放数据的容器。MySQL 5.6 之后,InnoDB 把数据分散到几个不同用途的表空间文件里:
| 表空间 | 文件 | 存放内容 |
|---|---|---|
| 系统表空间 | ibdata1 | 数据字典、Change Buffer、Doublewrite Buffer |
| 独立表空间 | .ibd | 每张表一个文件,存该表的数据与索引 |
| 通用表空间 | 自定义 | 用户创建、多表共享 |
| 临时表空间 | ibtmp1 | 临时表、排序中间结果 |
| Undo 表空间 | undo_001/002 | Undo Log |
其中独立表空间是生产默认,也是最需要理解的一个,由 innodb_file_per_table 控制:
# 独立表空间(默认开启)
innodb_file_per_table = 11.2 三级结构:段、区、页
一个 .ibd 文件不是把数据平铺写进去,而是按「段 → 区 → 页」三级组织:
三个层级各解决一个问题:
- 页(Page):16KB,是磁盘 IO 和内存管理的最小单位。Buffer Pool 按页缓存、Redo Log 按页记录、崩溃恢复按页重放。
- 区(Extent):1MB,64 个连续页。连续是关键——B+ 树相邻的页尽量落在磁盘相邻位置,把随机 IO 变成顺序 IO。
- 段(Segment):按索引划分,一个索引一个段。聚簇索引的叶子节点与非叶子节点分属两个段(数据段与索引段),各自独立增长,互不干扰。
为什么要多出「区」这一层?如果只有页,B+ 树每次分裂新页都可能落在磁盘任意位置,叶子节点散落各处,范围扫描会退化成随机读。区强制一次申请 1MB 连续空间,让同一个索引的数据尽量连续。
2. 空间分配与碎片
2.1 空间如何分配:碎片从哪来
理解了三级结构,才能理解碎片。InnoDB 申请空间以「区」为单位,但释放空间只精确到「页」,而且释放后从不把空间还给文件:
- 删除一行,页里多出一点空闲;
- 删完整页,页被标记为「可复用」,文件大小不变;
- 只有当整个「区」都空了,区才可能被回收。
这就是开头现象的原因:删了数据,文件不缩小,只是内部出现了大量「已标记但未回收」的空闲空间。即便整个区被回收,也只是把空间还给表空间内部的空闲管理,.ibd 文件本身依然不变小——要让文件缩回去,只有下一节的重建表这一条路。
-- data_free 是已分配但未使用的空间,即碎片
SELECT
table_name,
ROUND(data_free / 1024 / 1024, 2) AS fragment_mb
FROM information_schema.tables
WHERE table_schema = 'mydb' AND data_free > 0
ORDER BY data_free DESC;碎片主要有三个来源:
| 来源 | 原因 |
|---|---|
DELETE 留空洞 | 删除只释放页内空间,不缩文件 |
| 页分裂 | 随机主键(UUID)导致页反复分裂,空间利用率降到约 50% |
UPDATE 行迁移 | 行长变大、原页放不下,数据迁到新页,旧位置留指针 |
2.2 回收碎片:OPTIMIZE 的本质
要真正收回碎片,不能靠 DELETE,只能重建表:
OPTIMIZE TABLE users;
-- 等价于
ALTER TABLE users ENGINE=InnoDB;OPTIMIZE 不是「整理」现有数据,而是新建一张表、把数据一行行拷过去、再原子切换。所以它等价于重建,代价是:
- 需要一整张表的额外磁盘空间(新表与旧表并存一段时间);
- 大表执行时间长,期间写操作可能被阻塞。
因此碎片率不高(低于 30%)时不必执行,频繁 OPTIMIZE 反而得不偿失。
3. 表空间类型
3.1 独立表空间 vs 系统表空间
| 对比项 | 独立表空间 | 系统表空间 |
|---|---|---|
| 文件数量 | 每表一个 .ibd | 共享 ibdata1 |
| 空间回收 | DROP TABLE 直接删文件回收 | 只增不减,无法回收 |
| 备份灵活性 | 可单表备份、单表迁移 | 必须整库处理 |
| 管理复杂度 | 文件多 | 文件少 |
| 适用 | 生产默认 | 仅存公共结构 |
这就是为什么必须开启 innodb_file_per_table = 1:数据一旦放进 ibdata1,即使删表,空间也永远占着,ibdata1 只会越来越大。
-- 查看表空间实际占用
SELECT space, name, size * 16 / 1024 AS size_mb
FROM information_schema.innodb_tablespaces
WHERE name LIKE 'mydb/%';
-- MySQL 8.0 表空间加密
ALTER TABLE users ENCRYPTION='Y';4. 最佳实践
- 开启
innodb_file_per_table = 1:删表可回收空间。 - 碎片率超 30% 才
OPTIMIZE:低碎片重建不划算。 - 监控
ibdata1:它只增不减,膨胀了只能重建整个实例。 - 主键用自增:避免页分裂产生的碎片,见 B+ 树索引。
- 数据敏感时用表空间加密:
ALTER TABLE users ENCRYPTION='Y'。