PostgreSQL 索引里到底存了什么?
2026/6/29 18:44:06 网站建设 项目流程

很多人一听到“索引”,会自然想到:

索引是不是就像 MySQL InnoDB 主键一样,里面直接放着整行数据?

在 PostgreSQL 里,大多数情况下不是。

一句话先说结论:

PostgreSQL 的普通索引里主要存的是:索引字段的值 + 指向 heap 表中真实数据行的 TID。它通常不存完整行数据。

可以把 PostgreSQL 的索引理解成一本书的“目录”:

目录里写着:关键词在哪一页 正文内容:还在真正的书页里

PostgreSQL 里就是:

索引里写着:某个 key 对应 heap 表里的哪个位置 完整数据:还在 heap 表里

1. 先看一个简单表

假设有一张订单表:

CREATETABLEorders(idbigintPRIMARYKEY,order_novarchar(64),customer_idbigint,amountnumeric,create_timetimestamp);

再建一个普通索引:

CREATEINDEXidx_orders_customer_idONorders(customer_id);

这时 PostgreSQL 会创建一个索引结构。

这个索引不是把整行都复制一份进去,而是大概存:

customer_id -> TID

例如:

customer_id = 2001 -> (12, 5) customer_id = 2002 -> (18, 3) customer_id = 2003 -> (25, 9)

这里的(12, 5)就是一个类似地址的东西,表示这条记录在 heap 表里的位置。


2. PostgreSQL 索引里最重要的两样东西

以最常见的 B-tree 索引为例,索引里最核心的是两类信息:

1. 索引 key 2. TID

2.1 索引 key

索引 key 就是你建索引的字段值。

例如:

CREATEINDEXidx_orders_customer_idONorders(customer_id);

那么索引 key 就是:

customer_id 的值

如果是联合索引:

CREATEINDEXidx_orders_customer_timeONorders(customer_id,create_time);

那么索引 key 就是:

(customer_id, create_time)

也就是说,联合索引里不是只存第一个字段,而是按照索引定义顺序,把多个字段组合起来作为 key。


2.2 TID

TID可以理解成 PostgreSQL heap 表中某一行的物理地址。

它大概长这样:

(block number, tuple offset)

通俗点说就是:

第几个数据页 + 这个数据页里的第几条记录

比如:

(12, 5)

可以理解为:

第 12 个数据块里的第 5 条记录

所以索引项可以通俗理解为:

customer_id = 2001 的那条数据,在 heap 表第 12 页第 5 个位置

3. 查询时怎么用索引?

例如执行:

SELECT*FROMordersWHEREcustomer_id=2001;

PostgreSQL 大概会这样做:

1. 去 idx_orders_customer_id 索引里找 customer_id = 2001 2. 找到对应的 TID,例如 (12, 5) 3. 根据 TID 去 heap 表第 12 页第 5 个位置拿完整行 4. 返回 id、order_no、customer_id、amount、create_time 等字段

也就是说:

索引负责快速定位 heap 表负责保存完整数据

这就是 PostgreSQL 普通索引查询经常还要访问 heap 的原因。


4. B-tree 索引不是只有一层

PostgreSQL 最常见的索引类型是 B-tree。

它不是一个简单列表,而是类似一棵树。

可以简化理解为三类页面:

root page 根页面 internal page 中间页面 leaf page 叶子页面

大概长这样:

[root page] | ------------------------- | | [internal page] [internal page] | | -------------- -------------- | | | | [leaf] [leaf] [leaf] [leaf]

查询时,PostgreSQL 从根页面开始,根据 key 的大小一路往下找,最后找到叶子页面。


5. B-tree 的内部页面和叶子页面分别存什么?

5.1 internal page 里存什么?

internal page 可以理解成“路牌”。

它不主要负责保存最终结果,而是告诉 PostgreSQL:

小于某个值的去左边 大于某个值的去右边

比如:

customer_id < 2000 去左边页面 customer_id >= 2000 去右边页面

所以 internal page 里主要是:

分隔 key + 指向下一层 index page 的指针

5.2 leaf page 里存什么?

leaf page 才是最终定位到具体数据的位置。

叶子页面里的索引项大概是:

索引 key + TID

例如:

customer_id = 2001 -> TID (12, 5) customer_id = 2001 -> TID (13, 8) customer_id = 2002 -> TID (18, 3)

如果多个订单属于同一个customer_id,索引里就会有多个 key 相同但 TID 不同的记录。


6. 索引里会不会存完整行?

普通 PostgreSQL B-tree 索引通常不会存完整行。

例如表里有:

id, order_no, customer_id, amount, create_time

如果只建了:

CREATEINDEXidx_orders_customer_idONorders(customer_id);

那么索引里主要有:

customer_id + TID

它不会自动把下面这些字段都放进去:

id order_no amount create_time

所以执行:

SELECT*FROMordersWHEREcustomer_id=2001;

还是需要去 heap 表拿完整行。


7. INCLUDE 字段是什么?

PostgreSQL 支持覆盖索引写法:

CREATEINDEXidx_orders_customer_id_include_amountONorders(customer_id)INCLUDE(amount,order_no);

这个索引可以简单理解为:

用于排序和查找的 key:customer_id 额外顺手带上的字段:amount, order_no

也就是说,这个索引叶子项里会包含:

customer_id + amount + order_no + TID

注意:

INCLUDE 字段不是索引查找 key

它主要是为了让某些查询可以直接从索引里拿到字段,减少访问 heap 的机会。

例如:

SELECTcustomer_id,amount,order_noFROMordersWHEREcustomer_id=2001;

这个查询需要的字段都在索引里,就有机会走:

Index Only Scan

8. 有 INCLUDE 字段就一定不访问 heap 吗?

不一定。

PostgreSQL 有 MVCC 机制,它要判断一行数据对当前事务是否可见。

问题是:

可见性信息主要在 heap 表里,不完全在索引里

所以即使查询字段都在索引里,PostgreSQL 有时仍然要去 heap 看一下这条记录是否可见。

只有当 visibility map 显示对应 heap page 是all-visible时,PostgreSQL 才能更放心地不访问 heap。

看执行计划时,重点看:

EXPLAIN(ANALYZE,BUFFERS)SELECTcustomer_id,amount,order_noFROMordersWHEREcustomer_id=2001;

如果看到:

Index Only Scan Heap Fetches: 0

才说明基本没有访问 heap。


9. 索引里还会有一些“管理信息”

除了 key、TID、INCLUDE 字段,索引页里还会有一些数据库内部使用的信息。

比如:

页面头信息 页面之间的指针 索引项长度 排序相关信息 空值处理信息 重复 key 压缩或去重相关信息

这些东西一般业务开发不用关心。

你只要记住:

索引不是只存一列值,它还需要存能找到下一页、找到 heap 行、维护有序结构的一些内部信息。

10. 重复值很多时,索引怎么处理?

比如很多订单的customer_id都是2001

索引里可能逻辑上是:

customer_id = 2001 -> TID (12, 5) customer_id = 2001 -> TID (12, 6) customer_id = 2001 -> TID (13, 2) customer_id = 2001 -> TID (18, 9)

在较新的 PostgreSQL 版本里,B-tree 对重复 key 可能会做 deduplication,可以简单理解成:

customer_id = 2001 -> 一组 TID

这样可以减少索引体积。

不过从业务理解上,你仍然可以把它看成:

一个 key 对应多条 heap 记录的位置

11. 主键索引里有什么?

主键索引本质上也是一个唯一 B-tree 索引。

例如:

CREATETABLEorders(idbigintPRIMARYKEY,order_novarchar(64),amountnumeric);

PostgreSQL 自动建出来的主键索引大概是:

id -> TID

也就是:

主键值 + heap 行位置

不是:

主键值 + 完整行数据

这点和 MySQL InnoDB 非常不一样。

MySQL InnoDB 主键索引叶子节点里是完整行数据,而 PostgreSQL 主键索引叶子节点里通常还是 key + TID。


12. 不同索引类型里存的东西也不完全一样

上面主要讲的是 B-tree,因为它最常见。

PostgreSQL 还有其他索引类型。

12.1 GIN 索引

GIN 常用于数组、JSONB、全文检索。

它更像一个“倒排索引”。

例如 JSONB 里有:

{"tags":["java","postgresql"]}

GIN 索引可能更像:

java -> 哪些行包含 java postgresql -> 哪些行包含 postgresql

它适合回答:

哪些记录包含某个词 / 某个元素?

12.2 GiST 索引

GiST 常用于地理位置、范围、相似度等场景。

它不一定是简单的等值查找,而是存一些能帮助判断范围、相交、距离的信息。

可以粗略理解为:

存的是某种“范围摘要”或“空间边界”

12.3 BRIN 索引

BRIN 适合特别大的表,并且数据天然有顺序的场景,比如按时间不断追加的数据。

它不会给每一行都存一个索引项。

它更像是:

这一批数据页里,create_time 最小是多少,最大是多少

例如:

第 1 ~ 128 个 block:create_time 从 2026-01-01 到 2026-01-03 第 129 ~ 256 个 block:create_time 从 2026-01-04 到 2026-01-06

所以 BRIN 很小,但它更适合过滤大范围,不适合像 B-tree 那样精准定位单行。


13. 一张表总结 PostgreSQL 索引里有什么

内容是否常见通俗解释
索引 key常见建索引的字段值,例如customer_id
TID常见heap 表中真实数据行的位置
INCLUDE 字段可选额外放进索引里的字段,用来减少访问 heap
页面指针常见B-tree 从上一层找到下一层要用
页面/元数据常见PostgreSQL 维护索引结构需要的内部信息
完整行数据通常没有PostgreSQL 普通索引一般不存完整行
MVCC 完整可见性信息通常没有所以 Index Only Scan 有时仍要访问 heap

14. 常见误区

误区一:索引里有完整行

PostgreSQL 普通索引里通常没有完整行。

它主要是:

key + TID

完整行在 heap 表里。


误区二:主键索引就一定不用访问表

在 PostgreSQL 里不一定。

主键索引通常还是:

主键值 + TID

如果查询:

SELECT*FROMordersWHEREid=1001;

通常还要根据 TID 去 heap 表取完整行。


误区三:有覆盖索引就一定不访问 heap

也不一定。

PostgreSQL 还要考虑 MVCC 可见性。

如果 visibility map 条件不满足,即使字段都在索引里,也可能有 heap fetch。


15. 最通俗的一句话

可以这样记:

PostgreSQL 的索引大多数时候不是“数据副本”,而是“目录 + 地址”。目录里有索引字段值,地址就是 TID;真正完整的数据行还在 heap 表里。

再简单一点:

索引 key:我要找什么 TID:找到后去 heap 表哪里拿完整数据 INCLUDE 字段:为了少跑一趟,顺手放在索引里的附加字段

所以 PostgreSQL 索引里最核心的东西就是:

key + TID

如果有覆盖索引,再加上:

INCLUDE 字段

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询