当前位置: 首页 > news >正文

PostgreSQL 索引里到底存了什么?

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

索引是不是就像 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 字段
http://www.jsqmd.com/news/1091183/

相关文章:

  • MSP430FR5969 LaunchPad开发板:FRAM与超低功耗设计实战指南
  • SpringBoot 配置文件详细指南
  • 用 OllamaHub 让 Visual Studio Copilot 可以对接任意模型
  • 超链接以字段(Field) 的形式存储。每个超链接字段包含两个核心部分:
  • 德州仪器DRV2667压电触觉驱动器评估与开发全攻略
  • 2026高考志愿资料(本科+专科)免费分享
  • 工业互联网组建与维护全域学习总结、技术体系探究与行业未来发展就业全景分析
  • 很多人一提到“省钱”,第一反应就是别用最新模型。但从一条真实的开发账单看,影响成本的关键,未必只是模型新不新,而是这次请求里有没有把缓存价值吃满。
  • Shell 脚本从入门到写出第一个自动化脚本
  • 【WorkBuddy专栏50】代码开发技术体系深度分析——前端、后端、全栈、移动端、数据工程,WB和CODEBUDDY谁更擅长?
  • Win11Debloat:如何用4个步骤让Windows 11运行速度快65%?
  • 第01篇:从一颗芯片看透智能座舱——座舱MCU的“世界观”
  • 基于物联网、时序模型、大模型和智能问数,设备预测性维护【智能体】应用案例
  • Web安全实战:路径遍历漏洞原理、复现与防御指南
  • VinXiangQi:基于YOLOv5的中国象棋智能辅助工具终极指南
  • 基于微信小程序的贵阳市特色农产品交易系统的设计与实现
  • 从多引擎探测到优化闭环
  • SpringBoot 本地项目部署云服务器 + 公网域名流程
  • 用 Claude Opus 4.8 辅助故障复盘:从告警日志到可验证 RCA 的一套工作流
  • 年薪73W,AI产品经理面经
  • API Key 泄露后会发生什么——5 个真实泄露场景和防御方案
  • 三步构建个人数字图书馆:novel-downloader完全指南
  • 电气工程考核基础
  • WSUS服务器遭CVE-2025-59287漏洞攻击后的进程行为审计与应急响应实战
  • 如何5分钟实现Windows和Office永久激活:KMS智能激活完整指南
  • DeepSeek幫我設計的會員模塊
  • OBS-ASIO插件深度解析:专业音频采集的技术实现与架构设计
  • Steam成就管理器完整指南:如何安全解锁与重置游戏成就
  • 刹那.相位宇宙
  • 渗透测试实战入门:从零到精通DC-1靶场攻防全流程解析