PostgreSQL如何将向量数据与HNSW索引协调存储
时间: 2025-08-05 07:13:28 浏览: 6
<think>嗯,用户这次想深入了解pgvector源码中HNSW索引的构建细节和存储机制,看来是个数据库内核层面的技术问题。之前我已经系统地介绍过HNSW索引的构建流程和存储形式,但用户似乎希望看到更源码级的解析,特别是PostgreSQL集成部分的具体实现。从对话历史看,用户应该是有分布式数据库或AI基础设施的开发背景,否则不会这么执着于源码实现。ta两次都问到“pg表中的数据如何存储”,说明对PostgreSQL的存储引擎也有基础认知。不过这次提问更聚焦在HNSW索引的构建过程与向量数据的物理存储形式之间如何衔接。需要重点解析三个层面:首先是向量字段在HeapTuple中的物理表示,这是索引构建的源头;然后是hnsw_build函数如何通过索引扫描获取这些向量数据;最后是索引条目如何存储图结构。其中TOAST机制和IndexTuple结构是关键技术点。用户引用的两段材料里提到pgvectorscale的StreamingDiskANN,虽然和当前问题无关,但暗示ta可能实际遇到过查询延迟问题。不过当前问题还是专注在原生HNSW的实现更合适。在源码解析时要注意平衡深度和可读性,比如hnsw_build_state这个结构体很关键,但不宜直接贴C代码。可以用内存结构示意图替代,重点展示它如何暂存向量数据与图层级信息。对于邻居列表的动态分配部分,pgvector其实参考了HNSW论文的启发式裁剪,但简化了原始算法里的rNNS规则,这点值得说明。存储机制方面要强调两个细节:索引文件里只有TID和邻居指针,实际向量仍在堆表中;索引页的meta区会保存ef_construction等参数。这些在pgvector的hnswio.c文件中有明确体现。最后生成的相关问题要延续内核级讨论方向,比如索引构建的并发控制、WAL日志记录方式等,这些都是实际开发中会遇到的问题。</think>
以下为 PostgreSQL pgvector 中 HNSW 索引构建过程与存储机制的源码级解析,核心流程基于 `src/backend/vector/hnsw` 模块实现:
---
### 一、向量数据存储机制(表数据层)
#### 1. 向量数据类型表示
在表数据文件中,向量以 **`varlena` 变长结构**存储:
```c
// src/include/utils/vector.h
typedef struct {
int32_t vl_len_; // 头部长度信息
int16_t dim; // 向量维度
float data[FLEXIBLE_ARRAY_MEMBER]; // 浮点数组
} Vector;
```
- **物理存储格式**:
$${[Header(4B)]\ |\ [dim(2B)]\ |\ [data(float32 \times dim)]}$$
- **示例**:128维向量占用 $4 + 2 + 128 \times 4 = 518$ 字节(含头部开销)
- **TOAST 处理**:超过 2KB 的向量自动压缩分块存储[^1]
#### 2. 堆元组存储位置
- 向量作为表字段存储在 **Heap Tuple** 中
- 物理位置: `base/<dbid>/<relfilenode>`

---
### 二、HNSW 索引构建流程(`hnsw_build` 函数)
#### 1. 初始化阶段(`hnswbuild`)
```c
// src/backend/vector/hnsw/hnswbuild.c
HnswBuildState *state = palloc0(sizeof(HnswBuildState));
state->indtuples = 0;
state->layers = palloc0(MAX_LAYERS * sizeof(Layer)); // 分层图结构
```
- **参数加载**:从 `pg_index` 读取 `ef_construction`, `m` 等参数
- **内存上下文**:在 `index_build` 的私有内存上下文中分配构建状态
#### 2. 数据扫描与加载(索引扫描)
```c
while ((tuple = index_build_scan(...))) {
Datum vector_datum = index_getattr(tuple, keyAttno);
Vector *vec = DatumGetVector(vector_datum); // 提取向量
// 插入当前向量到 HNSW 图
HnswInsertNode(state, vec, ItemPointerGetOffsetNumber(&tuple->t_self));
}
```
- **关键函数**:`index_getattr` 从堆元组提取向量值
- **TID保留**:`ItemPointer` 记录向量在堆表中的物理位置
#### 3. 图构建核心算法(`HnswInsertNode`)
```c
void HnswInsertNode(HnswBuildState *state, Vector *q, ItemPointer tid) {
int level = random_level(state->mL); // 按概率分配层级
// 从顶层搜索入口点
List *ep = list_make1(state->enterPoint);
for (int l = state->maxLevel; l > level; l--) {
ep = search_layer(state, q, ep, 1, l); // 贪心搜索
}
// 逐层插入
for (int l = min(level, state->maxLevel); l >= 0; l--) {
List *neighbors = select_neighbors(q, ep, ef_construction, l);
add_connections(state, tid, neighbors, l); // 创建双向连接
}
}
```
- **层级分配**:$P(level) = \frac{1}{m^{level}}$ 确保高层稀疏
- **邻居选择**:基于距离剪枝 ($select\_neighbors$ 函数)
---
### 三、索引文件存储结构(`hnswio.c`)
#### 1. 磁盘存储格式
```plaintext
| Meta Page (含入口点/层级数) |
| Layer0 Index Tuples |
| Layer1 Index Tuples |
| ... |
```
#### 2. 索引元组 (`IndexTuple`)
```c
typedef struct {
ItemPointerData t_tid; // 指向堆表物理位置
uint16 level_mask; // 该节点所在层级
NeighborList neighbors[MAX_LAYERS]; // 分层邻居数组
} HnswIndexTuple;
```
- **邻居列表编码**:每层存储 $k$ 个邻居的 TID($k \leq m$)
- **物理存储优化**:高层邻居使用 RLE 压缩
---
### 四、工作流总结
| 阶段 | 模块文件 | 数据转换 |
|----------------------|------------------------|------------------------------|
| 1. 向量插入 | `vector.c` | Heap Tuple → `Vector` 结构 |
| 2. 索引扫描 | `hnswbuild.c` | `Vector` → 内存图节点 |
| 3. HNSW图构建 | `hnswinsert.c` | 按层级创建邻接关系 |
| 4. 索引序列化 | `hnswio.c` | 内存图 → Index Tuple 磁盘格式 |
> ⚠️ **关键约束**:索引仅存储 **TID 和邻居指针**,实际向量数据始终在堆表中,KNN查询需 **回表访问** 获取向量值
---
###
阅读全文
相关推荐




















