Skip to content

表空间 ​

删掉了表里一半的数据,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/002Undo Log

其中独立表空间是生产默认,也是最需要理解的一个,由 innodb_file_per_table 控制:

ini
# 独立表空间(默认开启)
innodb_file_per_table = 1

1.2 三级结构:段、区、页 ​

一个 .ibd 文件不是把数据平铺写进去,而是按「段 → 区 → 页」三级组织:

三个层级各解决一个问题:

  • 页(Page):16KB,是磁盘 IO 和内存管理的最小单位。Buffer Pool 按页缓存、Redo Log 按页记录、崩溃恢复按页重放。
  • 区(Extent):1MB,64 个连续页。连续是关键——B+ 树相邻的页尽量落在磁盘相邻位置,把随机 IO 变成顺序 IO。
  • 段(Segment):按索引划分,一个索引一个段。聚簇索引的叶子节点与非叶子节点分属两个段(数据段与索引段),各自独立增长,互不干扰。

为什么要多出「区」这一层?如果只有页,B+ 树每次分裂新页都可能落在磁盘任意位置,叶子节点散落各处,范围扫描会退化成随机读。区强制一次申请 1MB 连续空间,让同一个索引的数据尽量连续。

2. 空间分配与碎片 ​

2.1 空间如何分配:碎片从哪来 ​

理解了三级结构,才能理解碎片。InnoDB 申请空间以「区」为单位,但释放空间只精确到「页」,而且释放后从不把空间还给文件:

  • 删除一行,页里多出一点空闲;
  • 删完整页,页被标记为「可复用」,文件大小不变;
  • 只有当整个「区」都空了,区才可能被回收。

这就是开头现象的原因:删了数据,文件不缩小,只是内部出现了大量「已标记但未回收」的空闲空间。即便整个区被回收,也只是把空间还给表空间内部的空闲管理,.ibd 文件本身依然不变小——要让文件缩回去,只有下一节的重建表这一条路。

sql
-- 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,只能重建表:

sql
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 只会越来越大。

sql
-- 查看表空间实际占用
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. 最佳实践 ​

  1. 开启 innodb_file_per_table = 1:删表可回收空间。
  2. 碎片率超 30% 才 OPTIMIZE:低碎片重建不划算。
  3. 监控 ibdata1:它只增不减,膨胀了只能重建整个实例。
  4. 主键用自增:避免页分裂产生的碎片,见 B+ 树索引。
  5. 数据敏感时用表空间加密:ALTER TABLE users ENCRYPTION='Y'。

Released under the MIT License.