跳到主要内容
预计阅读 20 分钟

数据的地图 —— 表空间与数据目录

前面我们讲了数据存在”页”里,页通过链表串起来,再通过B+树索引加速查找。但这些页到底放在磁盘上的什么位置?一个.ibd文件内部是怎么组织的?今天我们来看看InnoDB表空间的”地图”。

📋 开篇自测:你已经知道多少?

  1. MySQL的数据文件默认存放在哪个目录下?这个目录叫什么?
  2. 一张InnoDB表对应磁盘上的哪些文件?
  3. 表空间内部除了”页”,还有哪些更大的管理单位?

一、数据目录——MySQL的家在哪儿

MySQL把所有的数据文件都放在一个目录里,这个目录叫数据目录(Data Directory)

-- 查看MySQL的数据目录在哪里
SHOW VARIABLES LIKE 'datadir';

常见的默认路径:

  • Linux:/var/lib/mysql/
  • macOS(Homebrew):/usr/local/var/mysql//opt/homebrew/var/mysql/
  • Windows:C:\ProgramData\MySQL\MySQL Server 8.0\Data\

让我们看看这个目录里有什么:

ls -la /var/lib/mysql/

你会看到类似这样的结构:

/var/lib/mysql/
├── mysql/                  # MySQL系统数据库
├── performance_schema/     # 性能监控数据库
├── sys/                    # 系统视图数据库
├── my_database/            # 你创建的数据库
│   ├── users.ibd           # users表的数据和索引
│   ├── orders.ibd          # orders表的数据和索引
│   └── ...
├── ibdata1                 # 系统表空间文件
├── ib_logfile0             # redo日志文件
├── ib_logfile1             # redo日志文件
├── undo_001                # undo表空间文件
├── undo_002                # undo表空间文件
├── mysql.ibd               # MySQL 8.0的数据字典
└── ...

注意关键一点:每个数据库对应一个子目录,每张InnoDB表对应该目录下的一个.ibd文件。

数据库的物理表示

当你执行CREATE DATABASE shop;时,MySQL其实就是在数据目录下创建了一个名为shop的子目录。当你执行DROP DATABASE shop;时,它就删掉了这个目录及其中的所有文件。

CREATE DATABASE shop;
-- 文件系统效果:创建了 /var/lib/mysql/shop/ 目录

USE shop;
CREATE TABLE products (id INT PRIMARY KEY, name VARCHAR(100));
-- 文件系统效果:创建了 /var/lib/mysql/shop/products.ibd 文件

DROP TABLE products;
-- 文件系统效果:删除了 products.ibd 文件

🤔 想一想 如果你直接在文件系统中删除了一个.ibd文件(而不是通过SQL的DROP TABLE),MySQL还能正常工作吗?会发生什么?


二、表空间——数据的组织层级

在InnoDB中,“表空间(Tablespace)“是一个逻辑概念,它代表了一组数据页的集合。你可以把表空间想象成一个大型仓库,里面有很多货架(区),每个货架有很多格子(页)。

系统表空间 vs 独立表空间

InnoDB有两种主要的表空间:

系统表空间(System Tablespace)

对应文件ibdata1(可能还有ibdata2等)。在MySQL早期版本中,所有表的数据都混在这一个文件里。就像把所有部门的货物都堆在同一个大仓库——管理起来很混乱。

系统表空间现在主要用来存储:

  • InnoDB的内部数据字典信息
  • 双写缓冲区(Doublewrite Buffer)
  • 变更缓冲区(Change Buffer)
  • undo日志(在某些配置下)

独立表空间(File-Per-Table Tablespace)

从MySQL 5.6.6开始默认启用innodb_file_per_table=ON。每张表的数据和索引存放在各自独立的.ibd文件中。就像给每个部门分配了独立的仓库——清晰、独立、便于管理。

-- 查看是否启用了独立表空间
SHOW VARIABLES LIKE 'innodb_file_per_table';
-- 默认值:ON

-- 查看所有表空间信息
SELECT * FROM information_schema.INNODB_TABLESPACES
WHERE NAME NOT LIKE 'mysql/%' AND NAME NOT LIKE 'sys/%' LIMIT 10\G

独立表空间的好处:

  • 删除表时磁盘空间可以真正释放(直接删除.ibd文件)
  • 可以把不同表放在不同磁盘上
  • 便于单表备份和恢复
  • 避免系统表空间无限膨胀

三、表空间的内部结构——从页到区到段

一个.ibd文件(独立表空间)内部有着清晰的层级结构。从小到大:

页(Page)→ 区(Extent)→ 段(Segment)→ 表空间(Tablespace)

页(Page)—— 最小管理单位

我们已经很熟悉了:16KB一个页。这是InnoDB进行磁盘I/O的最小单位。

页有很多种类型:

  • 数据页(INDEX Page):存储行记录,也就是B+树的节点
  • Undo页:存储undo日志
  • 系统页:存储系统信息
  • 空闲页:尚未使用的页
  • 更多类型:Insert Buffer位图页、Inode页等

区(Extent)—— 64个连续页

一个区由64个连续的页组成,大小为64 × 16KB = 1MB

为什么要有”区”这个概念?

想象你在一个停车场找车位。如果你的车队有10辆车,你肯定希望它们停在一起(连续车位),而不是分散在停车场各处。顺序排列意味着磁盘可以做顺序读取,比随机读取快得多。

InnoDB在分配空间时,会尽量以区为单位分配连续的页。这样做范围查询时(比如SELECT * FROM orders WHERE create_time > '2025-01-01'),可以顺序读取连续的数据页,充分利用磁盘的顺序I/O优势。

段(Segment)—— 逻辑分组

段是一个逻辑概念,由若干个区组成。InnoDB为每个索引分配两个段:

  • 叶子节点段(Leaf Node Segment):存放B+树的叶子节点(包含实际数据)
  • 非叶子节点段(Non-Leaf Node Segment):存放B+树的内部节点(索引导航用)

为什么要把叶子节点和非叶子节点分开存放?

因为非叶子节点是”导航用的路标”,被频繁访问,应该尽可能缓存在内存中。叶子节点是”实际的数据”,数据量大得多。把它们分开,有助于内存管理和I/O优化。

打个比方:把书的目录单独装订成一本小册子(非叶子节点段),把正文装成一本厚书(叶子节点段)。你经常翻的是目录小册子,它小巧、可以随身带(缓存在内存中);正文太厚,需要时再去书架上找(从磁盘读取)。

碎片区——小表的优待

如果每个段至少分配一个区(1MB),那么创建一张只有几条记录的小表也至少要占2MB(叶子节点段1MB + 非叶子节点段1MB)。这太浪费了。

InnoDB的解决方案是碎片区(Fragment Extent)。对于数据量很小的表,InnoDB先从碎片区中分配单独的页,而不是分配整个区。当某个段的数据量增长到一定程度(约32页),才开始以完整的区为单位分配空间。

小表(几行数据):
  叶子节点段 → 碎片区中的1-2个页
  非叶子节点段 → 碎片区中的1个页

大表(百万行数据):
  叶子节点段 → 数百个完整的区
  非叶子节点段 → 几个完整的区

⚠️ 常见误区 误区:DELETE大量数据后,.ibd文件会变小。 实际上,InnoDB删除数据只是标记删除,不会把磁盘空间归还给操作系统。.ibd文件的大小只增不减(除非你执行OPTIMIZE TABLEALTER TABLE ... ENGINE=InnoDB来重建表)。这就像你从仓库搬走了一半的货物,但仓库的建筑面积并不会缩小——只是多了很多空货架。


四、数据字典——MySQL的”户口本”

MySQL需要记录很多元信息:有哪些数据库、每个数据库有哪些表、每张表有哪些列、列的类型是什么、有哪些索引……这些元信息统称为数据字典(Data Dictionary)

MySQL 8.0之前

数据字典信息分散在多个地方:

  • .frm文件:存储表的定义(列、索引等)
  • 系统表空间(ibdata1)中的内部表
  • information_schema:一组虚拟表,从各处汇总元信息

MySQL 8.0之后

MySQL 8.0做了一个重大改进:把所有数据字典信息统一存储在InnoDB表中mysql.ibd文件)。不再有.frm文件。

-- 查看数据库列表(数据来自数据字典)
SELECT * FROM information_schema.SCHEMATA;

-- 查看某个数据库下的表
SELECT TABLE_NAME, ENGINE, TABLE_ROWS, DATA_LENGTH
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'your_database';

-- 查看某张表的列信息
SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_DEFAULT
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = 'your_database' AND TABLE_NAME = 'your_table';

-- 查看某张表的索引信息
SELECT INDEX_NAME, COLUMN_NAME, SEQ_IN_INDEX, NON_UNIQUE
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = 'your_database' AND TABLE_NAME = 'your_table';

🤔 想一想 为什么MySQL 8.0要把数据字典从文件系统(.frm文件)迁移到InnoDB内部?这种改变带来了哪些好处?


五、redo日志文件与其他系统文件

数据目录中除了表空间文件,还有一些重要的系统文件:

redo日志文件

redo日志是保障事务持久性的关键文件(后面的章节会详细讲)。

自 MySQL 8.0.30 起,InnoDB redo 日志架构发生了重大变更。文件命名从 ib_logfile0ib_logfile1 改为 #innodb_redo/ 目录下的 #ib_redoN 文件,容量由统一的 innodb_redo_log_capacity 变量控制(默认 100MB)。

# MySQL 8.0.30+
#innodb_redo/
  ├── #ib_redo0
  ├── #ib_redo1
  └── ...

# MySQL 8.0.29 及更早版本
ib_logfile0
ib_logfile1
-- MySQL 8.0.30+:查看redo日志总容量
SHOW VARIABLES LIKE 'innodb_redo_log_capacity';

-- MySQL 8.0.29 及更早版本(这两个变量在 8.0.30+ 中已废弃)
-- SHOW VARIABLES LIKE 'innodb_log_file_size';
-- SHOW VARIABLES LIKE 'innodb_log_files_in_group';

undo表空间文件

undo_001
undo_002

undo表空间存储undo日志,用于事务回滚和MVCC。MySQL 8.0默认创建两个undo表空间。

临时表空间文件

ibtmp1                    # 全局临时表空间
temp_1.ibt, temp_2.ibt   # 会话级临时表空间

临时表空间用于存储临时表和排序操作的中间结果。

二进制日志文件(binlog)

binlog.000001
binlog.000002
binlog.index

binlog记录了所有修改数据的SQL语句,用于主从复制和数据恢复。注意binlog是MySQL Server层面的,不是InnoDB特有的。

-- 查看binlog是否开启
SHOW VARIABLES LIKE 'log_bin';

-- 查看当前的binlog文件列表
SHOW BINARY LOGS;

-- 查看某个binlog文件的内容
SHOW BINLOG EVENTS IN 'binlog.000001' LIMIT 20;

慢查询日志

slow_query.log

记录执行时间超过阈值的SQL语句,是性能优化的重要参考。

-- 开启慢查询日志
SET GLOBAL slow_query_log = ON;

-- 设置慢查询阈值(单位:秒)
SET GLOBAL long_query_time = 1;

-- 查看慢查询日志路径
SHOW VARIABLES LIKE 'slow_query_log_file';

六、文件与物理布局的全景图

让我们把整个MySQL的文件体系串起来看:

数据目录 (datadir)

├── [系统文件]
│   ├── ibdata1              ← 系统表空间(双写缓冲区、变更缓冲区等)
│   ├── mysql.ibd            ← 数据字典(MySQL 8.0+)
│   ├── #innodb_redo/        ← redo日志(8.0.30+,旧版为ib_logfile0/1)
│   ├── undo_001/002         ← undo表空间
│   ├── ibtmp1               ← 临时表空间
│   ├── binlog.000001/...    ← binlog日志
│   └── slow_query.log       ← 慢查询日志

├── [数据库A的目录]
│   ├── table1.ibd           ← 表1的独立表空间(数据+索引)
│   ├── table2.ibd           ← 表2的独立表空间
│   └── ...

└── [数据库B的目录]
    ├── table3.ibd
    └── ...

每个.ibd文件内部:

.ibd文件
  └── 表空间
      ├── 段(叶子节点段)
      │   ├── 区1(64个连续页)
      │   │   ├── 页1(16KB)← 存若干行记录
      │   │   ├── 页2(16KB)
      │   │   └── ...(共64页)
      │   ├── 区2
      │   └── ...
      └── 段(非叶子节点段)
          ├── 区1
          └── ...

这就是MySQL数据从逻辑概念到物理文件的完整映射。理解了这个全景图,很多看似神秘的MySQL行为(比如为什么删除数据后磁盘空间不释放、为什么OPTIMIZE TABLE会重建表文件等)就都有了解释。

🤔 想一想 如果你需要把一张大表从一台MySQL服务器迁移到另一台,有哪些方法?其中哪种方法和我们今天学的文件结构直接相关?


📝 掌握度自测

  1. MySQL的数据目录(datadir)存放了哪些类型的文件?每种文件的作用是什么?
  2. 系统表空间(ibdata1)和独立表空间(.ibd文件)各存储什么内容?为什么推荐使用独立表空间?
  3. 表空间内部的层级结构是什么?页、区、段分别是什么大小、什么作用?
  4. 为什么InnoDB要把B+树的叶子节点和非叶子节点放在不同的段中?
  5. DELETE大量数据后,.ibd文件的大小会变化吗?如何真正回收磁盘空间?

💡 自我评估

  • 答对5题:对MySQL的物理存储有了全面理解,可以处理存储层面的运维问题了
  • 答对3-4题:核心概念清楚,建议动手在文件系统中观察一下MySQL的数据目录结构
  • 答对0-2题:建议对照全景图再梳理一遍,重点理解”逻辑概念到物理文件”的映射关系

购买课程解锁全部内容

让查询飞起来:MySQL 从索引到主从高可用

¥29.90