数据库真正有意思的地方,并不是“知道它用了 B+Tree”,而是: 同一种数据结构,为什么不同数据库会做出完全不同的实现?
上次我们认识了 B+Tree。
今天开始从“数据结构”进入“数据库实现”。
我们重点比较三个非常经典的数据库:
- SQLite
- PostgreSQL
- MySQL InnoDB
它们都大量使用 B-Tree 家族索引,但存储模型并不一样。
一、先看一个最重要的问题
假设有一张表:
User
id name age
1 张三 20
2 李四 25
3 王五 30
现在:
SELECT *
FROM User
WHERE id = 2;
数据库最终必须完成一件事情,找到 id = 2 对应的数据,但这里存在两种完全不同的设计。
设计 A:索引只保存“数据在哪里”
例如:
B+Tree
2
|
↓
Page 10
Slot 3
找到:
Page 10 + Slot 3
然后再去读取真正的数据。
也就是:
Index
↓
Record Location
↓
Data
设计 B:索引本身就是数据
例如:
B+Tree
2
|
↓
+-------------+
| id=2 |
| name=李四 |
| age=25 |
+-------------+
找到索引节点:
数据本身也就找到了。
这两个设计看起来只是一个小区别。
实际上,它会影响整个数据库的:
- 存储结构
- 查询路径
- 主键性能
- 二级索引
- 更新方式
- Page 组织
- 磁盘空间
- Buffer Pool 使用
而 SQLite、PostgreSQL、InnoDB 恰好体现了不同的设计思想。
二、SQLite:B-Tree 就是数据组织的核心
先看最简单、最紧凑的 SQLite。
SQLite 与 MySQL、PostgreSQL 最大的不同之一:它不是一个独立的数据库服务器。
它通常直接嵌入应用程序:
Application
|
↓
SQLite
|
↓
database.db
数据库本身就是一个文件。
三、SQLite 的核心:B-Tree Page
SQLite 内部大量数据都组织在 B-Tree 中。
可以简单理解成:
Database File
+---------+
| Page 1 |
+---------+
| Page 2 |
+---------+
| Page 3 |
+---------+
| Page 4 |
+---------+
这些 Page 并不是简单的数据块。
它们可以作为:
B-Tree Internal Page
或者:
B-Tree Leaf Page
SQLite 的一个重要特点
SQLite 的表本身可以组织成一种 B-Tree。
例如:
Table B-Tree
Root
|
+-----+-----+
| |
Page A Page B
| |
Records Records
因此 SQLite 的存储模型相对紧凑。
它没有简单地采用:
Heap
+
Separate Primary Index
这种模型。
四、SQLite 为什么适合这样设计?
因为 SQLite 的目标非常明确:小型、可靠、嵌入式、零配置。
它需要:
- 一个文件
- 一个进程
- 很少的后台组件
- 尽可能简单的部署方式
所以 SQLite 的存储设计非常集中。
你可以把它理解为:
SQLite
|
+-- B-Tree
|
+-- Pager
|
+-- WAL(预写日志)
|
+-- File
它的很多数据库能力最终都会落到这个文件和 Page 管理体系上。
五、PostgreSQL:数据和索引分开
接下来来看 PostgreSQL。它采用了一个非常重要的设计:表数据和索引通常是分开的。
例如:
Table
|
↓
Heap
|
+-- Page 0
+-- Page 1
+-- Page 2
另外:
Index
|
↓
B-Tree
|
+-- Index Page
+-- Index Page
+-- Index Page
两者是独立的。
六、PostgreSQL 的索引保存什么?
假设:
User
id=100
name=张三
Heap 中:
Page 5
+----------------+
| Tuple |
| id=100 |
| name=张三 |
+----------------+
B-Tree 索引中:
100
|
↓
Tuple Location
这个位置通常通过:**TID(Tuple Identifier)**来定位。
可以把它理解为:
TID
|
+-- Block Number
|
+-- Tuple Offset
也就是:
Index
|
↓
某个 Heap Page
|
↓
某条 Tuple
七、为什么 PostgreSQL 要这样设计?
因为 PostgreSQL 的设计非常强调:表数据本身的独立性。
一张表可以拥有多个索引:
Table
|
Heap
/ | \
/ | \
↓ ↓ ↓
Index1 Index2 Index3
例如:
PRIMARY KEY(id)
INDEX(name)
INDEX(age)
INDEX(email)
这些索引都指向:
Heap Tuple
因此 PostgreSQL 可以非常灵活地增加不同类型的索引。
八、这带来一个重要问题:Index Scan 不是终点
假设:
SELECT *
FROM User
WHERE id = 100;
PostgreSQL:
SQL
↓
B-Tree Index
↓
TID
↓
Heap Page
↓
Tuple
因此一次查询可能至少涉及:
Index Page
+
Heap Page
这被称为:Index Scan + Heap Fetch
这也是 PostgreSQL 查询性能分析中非常重要的一件事情。
九、InnoDB:完全不同的思路
现在进入 MySQL 的 InnoDB。这里是今天最重要的部分。
InnoDB 使用 Clustered Index(聚簇索引),作为表数据的核心组织方式。
十、什么叫 Clustered Index?
假设:
User
id name
1 张三
2 李四
3 王五
InnoDB 可以把主键 B+Tree 组织成:
Root
|
[1 | 2 | 3]
|
↓
+-------------------------+
| id=1 | 张三 |
| id=2 | 李四 |
| id=3 | 王五 |
+-------------------------+
叶子节点:不只是索引。而是:直接保存整行数据。
所以:
Primary Key
|
↓
Clustered B+Tree
|
↓
Row Data
这就是聚簇索引。
十一、为什么 InnoDB 要这么做?
核心目标之一:让主键访问非常高效。
例如:
SELECT *
FROM User
WHERE id = 100;
InnoDB:
Root
↓
Internal Page
↓
Leaf Page
↓
Row
到了 Leaf Page,数据已经在那里,不需要再:
Index
↓
Heap
↓
Tuple
多走一步。
十二、三种数据库放在一起
现在我们可以画出一张非常重要的图。
SQLite
B-Tree
|
↓
Table Data
PostgreSQL
B-Tree Index
|
↓
TID
|
↓
Heap
|
↓
Tuple
InnoDB
Primary Key B+Tree
|
↓
Row Data
十三、那么二级索引呢?
这里 InnoDB 又出现一个非常有意思的设计。
假设:
PRIMARY KEY(id)
INDEX(name)
主键索引:
id
↓
完整Row
但是 name 索引不能直接保存整行,否则每个二级索引都会非常巨大。
因此二级索引通常保存:
name
+
Primary Key
例如:
name Index
张三 → id=100
李四 → id=200
王五 → id=300
查询:
SELECT *
FROM User
WHERE name = '李四';
路径:
Secondary Index
|
↓
Primary Key
|
↓
Clustered Index
|
↓
Row
于是发生了第二次 B+Tree 查询。
这个过程通常被称为:回表(Table Lookup)
十四、三种数据库的核心差异
现在可以正式比较:
| 特性 | SQLite | PostgreSQL | InnoDB |
|---|---|---|---|
| 数据组织 | B-Tree | Heap | Clustered B+Tree |
| 主键索引 | B-Tree结构 | 独立索引 | 聚簇索引 |
| 索引与数据 | 高度融合 | 分离 | 主键融合 |
| 二级索引 | 依实现而定 | 指向Heap Tuple | 指向主键 |
| 查询路径 | B-Tree | Index → Heap | Clustered Index |
| 设计倾向 | 简洁、嵌入式 | 灵活、扩展性 | OLTP、高效主键访问 |
这里最重要的不是记住表格。
而是理解:B+Tree 只是工具,真正决定数据库行为的是“B+Tree + 数据组织方式”。
十五、为什么 PostgreSQL 的 Heap 不像 InnoDB 那样直接放到 B+Tree?
这是一个非常值得思考的问题。
如果把数据直接放进 B+Tree:
B+Tree
|
+-- Row
+-- Row
+-- Row
看起来非常高效。
但是:
数据的位置与主键绑定得更加紧密。
而 PostgreSQL:
Heap
|
+-- Tuple
+-- Tuple
+-- Tuple
Index
|
+-- Index
+-- Index
数据和索引相对独立,这样做可以带来更大的灵活性。
例如:
一张表可以拥有:
B-Tree
Hash
GiST
GIN
BRIN
等不同索引类型。它们都可以针对同一份 Heap 数据。
十六、为什么数据库没有一种“完美”的索引设计?
因为数据库面临的需求不同。
如果目标是:主键查询极快。
Clustered Index 非常优秀。
如果目标是:一个表需要多个不同类型的索引。
Heap + Index 非常灵活。
如果目标是:极简、单文件、嵌入式。
SQLite 的设计非常合适。
所以:
数据库设计
≠
寻找最优秀的数据结构
数据库设计
=
针对目标场景做权衡
这是整个数据库内核学习过程中非常重要的思想。
十七、再看一个更新问题
假设:
id=100
name=张三
现在:
UPDATE User
SET name='Alexander'
WHERE id=100;
如果新的数据长度发生变化:
张三
变成:
Alexander
数据可能无法继续放在原来的空间。
这时候数据库需要考虑:
- 原位置是否足够?
- 是否需要移动 Tuple?
- 是否产生空闲空间?
- 索引是否需要更新?
- 其他事务看到什么版本?
于是:**存储结构开始与事务系统产生联系。**这就是下一阶段非常重要的内容。
十八、应该建立的知识体系
我们已经从:
Database
走到了:
Database
|
↓
Storage Engine
|
↓
Page
|
↓
Data Organization
|
├── Heap
|
└── B+Tree
|
↓
Index
而现在又进一步发现:
B+Tree
|
+── SQLite
|
+── PostgreSQL
|
└── InnoDB
虽然名字相似 B-Tree / B+Tree,但是背后的数据组织方式完全可以不同。
十九、重要的三个结论
① Index 不等于 Data
PostgreSQL 中:
Index → Heap → Tuple
索引只是定位工具。
② Clustered Index 可以同时承担 Data + Index
InnoDB:
B+Tree
↓
Row
索引本身就是数据组织结构。
③ 数据库设计的核心不是“选择哪个算法”
而是:根据访问模式、数据规模、写入方式、事务需求和硬件特性,决定数据应该如何组织。