很多人聊数据库索引时,会把下面几个概念混在一起:
- B-tree / B+Tree
- 主键索引
- 聚簇索引
- 回表
- Index Only Scan
一句话先说结论:
PostgreSQL 的主键索引虽然是 B-tree,但通常仍然需要访问表数据;MySQL InnoDB 的主键索引是聚簇索引,主键 B+Tree 叶子节点存的是整行数据,所以按主键查询理论上不需要再"回表"。
1. 什么是"回表"?
通俗理解:
先通过索引找到数据位置,再去真正的数据表里把完整记录取出来,这一步就可以理解为回表。
例如查询:
sql
SELECT *
FROM orders
WHERE id = 1001;
如果索引里只有 id 和数据地址,那么数据库找到 id = 1001 后,还要再去数据表里取 order_no、amount、create_time 等其他字段,这就是"回表"。
2. PostgreSQL 主键索引:通常需要访问 heap 表
PostgreSQL 中创建主键:
sql
CREATE TABLE orders (
id bigint PRIMARY KEY,
order_no varchar(64),
amount numeric,
create_time timestamp
);
PostgreSQL 会自动创建一个唯一 B-tree 索引,大致可以理解为:
text
orders_pkey: id -> TID
这里的 TID 是 PostgreSQL heap 表里的物理位置指针,类似:
text
(block number, tuple offset)
也就是说,PostgreSQL 的主键索引叶子节点里并不保存完整行数据,而是保存:
text
主键值 + 指向 heap tuple 的位置
所以执行:
sql
SELECT *
FROM orders
WHERE id = 1001;
通常流程是:
text
1. 通过 orders_pkey 找到 id = 1001
2. 从索引项中拿到 heap TID
3. 根据 TID 访问 heap 表
4. 取出完整行
所以在 PostgreSQL 里,主键查询 SELECT * 通常仍然需要访问 heap 表。
3. PostgreSQL 的"聚簇"不等于 InnoDB 的聚簇主键
PostgreSQL 也有 CLUSTER 命令,例如:
sql
CLUSTER orders USING orders_pkey;
它的作用是:
按照某个索引的顺序,把 heap 表数据重新物理排列一遍。
但要注意,它和 MySQL InnoDB 的聚簇主键不是一回事。
PostgreSQL 的 CLUSTER 有几个特点:
- 它是一次性的表重写操作。
- 后续新增、更新的数据不会一直自动保持这个物理顺序。
- 索引叶子节点里仍然不是整行数据,仍然主要是 key + TID。
- 查询需要完整行时,仍然要访问 heap。
所以可以简单理解为:
text
PostgreSQL CLUSTER = 把表按某个索引顺序整理一下
InnoDB 聚簇主键 = 主键索引本身就是数据表
这两个不是同一个概念。
4. MySQL InnoDB 主键索引:主键 B+Tree 叶子节点存整行
MySQL InnoDB 的表是按主键组织的。
假设有表:
sql
CREATE TABLE orders (
id bigint PRIMARY KEY,
order_no varchar(64),
amount decimal(18, 2),
create_time datetime
) ENGINE = InnoDB;
InnoDB 的主键索引可以理解为:
text
PRIMARY KEY B+Tree
id = 1001 -> 完整行数据
id = 1002 -> 完整行数据
id = 1003 -> 完整行数据
也就是说,InnoDB 主键索引的叶子节点里直接存整行数据。
所以执行:
sql
SELECT *
FROM orders
WHERE id = 1001;
理论上的流程是:
text
1. 通过主键 B+Tree 找到 id = 1001
2. 叶子节点上已经有完整行
3. 直接返回数据
因此,InnoDB 按主键查询完整行时,理论上不需要再回表。
5. InnoDB 二级索引才常说"回表"
例如给 order_no 建索引:
sql
CREATE INDEX idx_orders_order_no ON orders(order_no);
InnoDB 的二级索引叶子节点里通常存的是:
text
order_no -> 主键 id
执行:
sql
SELECT *
FROM orders
WHERE order_no = 'SO-001';
流程大致是:
text
1. 先走 idx_orders_order_no 找到主键 id
2. 再用主键 id 去 PRIMARY KEY B+Tree 找完整行
这一步"再去主键索引找完整行",就是 MySQL InnoDB 里常说的回表。
6. PostgreSQL 什么时候可能不访问 heap?
PostgreSQL 有一种执行方式叫:
text
Index Only Scan
它看起来像"不回表",但需要满足条件。
例如:
sql
SELECT id
FROM orders
WHERE id = 1001;
如果只查 id,而 id 已经在主键索引里,那么 PostgreSQL 有机会只扫索引。
但是 PostgreSQL 有 MVCC,可见性信息主要在 heap 里。为了确认这条记录对当前事务是否可见,PostgreSQL 还依赖 visibility map。
只有当相关 heap page 被标记为 all-visible 时,才可能真正避免访问 heap。
看执行计划时,可以用:
sql
EXPLAIN (ANALYZE, BUFFERS)
SELECT id
FROM orders
WHERE id = 1001;
如果看到:
text
Index Only Scan using orders_pkey on orders
Heap Fetches: 0
这才说明基本没有访问 heap。
如果看到:
text
Index Scan using orders_pkey on orders
通常就说明它通过索引定位后,还访问了 heap 表。
7. 对比总结
| 数据库 | 主键索引结构 | 主键叶子节点存什么 | SELECT * WHERE pk = ? 是否需要回表 |
|---|---|---|---|
| PostgreSQL | B-tree 索引 + heap 表分离 | 主键值 + TID | 通常需要访问 heap |
PostgreSQL CLUSTER 后 |
heap 按索引顺序重排 | 主键值 + TID | 通常仍需要访问 heap |
| MySQL InnoDB | 聚簇主键 B+Tree | 完整行数据 | 理论上不需要回表 |
| MySQL InnoDB 二级索引 | 二级索引 B+Tree | 二级索引值 + 主键值 | 查询完整行通常需要回主键索引 |
8. 常见误区
误区一:主键索引都是聚簇索引
不是。
MySQL InnoDB 的主键是聚簇索引,但 PostgreSQL 的主键只是一个唯一 B-tree 索引。
误区二:PostgreSQL 有 CLUSTER,所以主键查询不需要回表
不对。
PostgreSQL 的 CLUSTER 只是把 heap 表按某个索引顺序重新排列。它不会让索引叶子节点保存完整行,也不会让表像 InnoDB 那样永久按主键自动组织。
误区三:PostgreSQL Index Only Scan 一定不访问 heap
也不一定。
还要看 visibility map。如果执行计划里 Heap Fetches 不为 0,说明仍然访问了 heap。
9. 最通俗的一句话
可以这样记:
PostgreSQL 的主键索引像"目录":目录告诉你数据在 heap 表哪一页哪一行;MySQL InnoDB 的主键索引像"目录 + 正文":通过主键找到叶子节点时,整行数据已经在那里了。
所以:
text
PostgreSQL 主键索引:通常需要根据 TID 再访问 heap。
MySQL InnoDB 主键索引:主键叶子节点就是完整数据,理论上不需要回表。