在数据库内核的深处,有一个影响着所有查询性能的根本性设计——表的存储模型。它决定了数据在磁盘上的物理排列方式,是所有索引、查询优化器乃至SQL执行效率的基石。
绝大多数主流数据库,都站在了两种设计哲学的岔路口上:
两者没有绝对的优劣,只有场景的适配。本文将深入剖析这两种存储模型的设计原理、优劣对比,并揭示它们与索引、ROWID之间的深层关系。
堆表是指数据行按插入顺序无序存储的表结构。新插入的行会放在第一个能找到的空闲位置,没有任何排序规则。
“堆”这个名字,形象地描述了数据像一堆货物一样堆放在仓库中——没有顺序,没有结构,只有位置。
堆表的数据块(Block)中,数据行存放在任意可用空间,通过行目录来跟踪每个行的物理位置。
text
数据块 (Block) 结构:
+------------------+
| 块头 (Header) |
+------------------+
| 行目录 (Row Directory) | ---- 存储指向各数据行的指针
+------------------+
| 空闲空间 |
+------------------+
| 数据行 1 |
| 数据行 2 |
| ... |
+------------------+
由于堆表数据无序,必须有一个物理地址来精确定位每一行数据。这就是ROWID——Oracle数据库的核心设计。
ROWID通常包含以下信息:
ROWID可以理解为每行数据的GPS坐标,一旦生成,在其生命周期内通常保持不变(除非发生行迁移)。
索引组织表是指数据表本身就是一棵B+树,数据行按主键顺序存储在B+树的叶子节点中。
一句话概括:表即索引,索引即表。
在索引组织表中,聚簇索引(Clustered Index)就是数据本身:
text
B+树结构(索引组织表):
[根节点]
/ | \
[分支] [分支] [分支]
/ | | \ | \
[叶子] [叶子] [叶子] [叶子] [叶子]
↓ ↓ ↓ ↓ ↓
数据行 数据行 数据行 数据行 数据行 (按主键顺序存储)
由于数据位置会随着B+树的分裂/合并而变化,索引组织表无法使用固定的物理ROWID。取而代之的是逻辑ROWID或主键值本身。
在MySQL InnoDB中:
| 对比维度 | 📦 堆表 | 🌲 索引组织表 (IOT) |
|---|---|---|
| 数据存储顺序 | 无序(插入顺序) | 按主键有序 |
| 主键索引 | 独立的B树索引,叶子存ROWID | 就是表本身,叶子存完整数据 |
| 二级索引内容 | (索引列值, ROWID) |
(索引列值, 主键值) |
| 数据访问路径 | 先查索引获取ROWID → 回表取数据 | 直接在索引上定位,无需回表 |
| ROWID性质 | 物理地址,稳定 | 逻辑地址或主键值,可变 |
| 插入效率 | ⚡ 极高(直接追加或找空闲位置) | 较慢(需维持主键顺序,可能页分裂) |
| 主键查询效率 | 较慢(需走主键索引再回表) | 🚀 极快(一次定位即可) |
| 范围查询 | 走索引回表,若需排序代价高 | 🚀 极快(数据物理有序) |
| 空间利用率 | 高(无预留空间要求) | 稍低(需预留空间减少页分裂) |
| 碎片控制 | 需要定期重建 | B+树自平衡可缓解 |
| 典型代表 | Oracle、PostgreSQL、SQL Server | MySQL InnoDB、达梦 |
sql
SELECT * FROM employees WHERE id = 1001;
id=1001 → 获得ROWIDsql
INSERT INTO employees VALUES (...);
(索引列值, ROWID)优势:插入操作几乎没有顺序限制,只需找到空闲位置即可,因此并发写入性能极高。
sql
UPDATE employees SET salary = 50000 WHERE id = 1001;
sql
SELECT * FROM employees WHERE id = 1001;
id=1001优势:全程只需一次B树遍历,无需额外的回表操作,主键查询极快。
sql
INSERT INTO employees VALUES (...);
(索引列值, 主键值))代价:如果主键是随机值(如UUID),每次插入都可能引起页分裂,严重降低写入性能。
在堆表中,二级索引的访问路径是:
text
二级索引 → 获取ROWID → 直接定位数据(一次I/O)
由于ROWID是物理地址,回表操作非常高效,可以理解为“点对点直达”。
在索引组织表中,二级索引的访问路径是:
text
二级索引 → 获取主键值 → 搜索聚簇索引 → 定位数据(最多两次I/O)
虽然多了一次B树查找,但由于聚簇索引的B树通常高度较低(2-4层),额外开销也相对可控。
覆盖索引是指查询所需的所有列都在索引中,无需访问数据表本身。
| 场景 | 堆表 | 索引组织表 |
|---|---|---|
| 覆盖索引 | 完全不需要回表,二级索引即可满足 | 完全不需要回表,二级索引即可满足 |
| 非覆盖查询 | 需要一次回表(通过ROWID) | 需要一次回表(通过主键值) |
两者在覆盖索引优化上没有本质区别,只是回表的具体实现机制不同。
✅ 高并发写入场景
✅ 二级索引丰富
✅ 需要频繁更新非索引列
✅ 典型代表
✅ 主键查询为主
✅ 主键范围查询
✅ 主键本身就是查询关键
✅ 典型代表
| 数据库 | 默认存储模型 | 是否支持另一种 | 备注 |
|---|---|---|---|
| Oracle | 堆表 | ✅ 支持IOT(需显式指定ORGANIZATION INDEX) |
两种都支持,灵活选择 |
| 达梦 | 索引组织表 | ✅ 支持堆表 | 完全兼容Oracle语法,可按需选择 |
| MySQL | 索引组织表 | ❌ 不支持堆表 | InnoDB强制使用聚簇索引 |
| PostgreSQL | 堆表 | ❌ 不支持IOT | 可通过插件或分区表模拟 |
| SQL Server | 堆表 | ✅ 支持聚集索引 | 建聚集索引后即变成索引组织表 |
sql
SELECT * FROM users WHERE id BETWEEN 1 AND 1000;
sql
INSERT INTO events (uuid, data) VALUES (UUID(), ...); -- 每秒10000次
sql
SELECT * FROM orders WHERE status = 'PAID' AND create_time > '2024-01-01';
堆表和索引组织表代表了两种不同的设计哲学:
选择建议:
达梦社区技术 https://eco.dameng.com
文章
阅读量
获赞
