PostgreSQL pgvectorscale 深度解析:当 DiskANN 磁盘索引遇上 AI 原生数据库——千万级向量检索的工程革命
一、背景与动机:为什么 HNSW 在大规模场景下会「力不从心」
在 AI 应用爆发式增长的 2026 年,向量检索已经不是什么新鲜概念。从 RAG(检索增强生成)知识库到电商语义搜索,从推荐系统的向量化召回到大模型的长上下文记忆,向量数据库几乎是所有 AI 基础设施的标配。
然而,当你的向量规模从几万条增长到千万级甚至亿级时,很多团队会发现原来跑得好好的 HNSW 索引突然变成了「内存黑洞」。这背后有一个被很多人忽视的工程现实:
HNSW(Hierarchical Navigable Small World)本质上是一种基于内存的图索引算法。 它的原理是在内存中构建一个多层近邻图结构,查询时从最上层入口逐层向下遍历,找到最近邻。整个图结构必须全部驻留在内存中,才能保证毫秒级的查询延迟。
当向量维度是 768 维(BERT-base 的标准输出维度),每条向量 float32 占用 3KB,1000 万条向量的原始数据就是约 30GB。再加上 HNSW 图结构的额外开销(通常是原始数据的 1.5~3 倍),总内存需求轻松突破 100GB。对于中小企业来说,这意味着每月数千甚至数万元的内存成本——而且随着业务增长,这个数字还会持续膨胀。
这正是 pgvectorscale 出现的背景。2026 年 4 月,腾讯云数据库 PostgreSQL 全面支持 pgvectorscale 扩展,这是对 pgvector 的重大增强,引入了三项核心技术:StreamingDiskANN 磁盘索引、统计二进制量化(SBQ)、以及标签过滤搜索。它的目标很明确——在不牺牲太多检索精度的前提下,把向量检索的主要存储成本从昂贵的内存转移到经济的 SSD,同时保持可接受的查询性能。
今天这篇文章,我们就来深度拆解 pgvectorscale 的底层原理、架构设计、代码实战,以及它与主流向量数据库的真实差距。
二、Core Concepts:从 HNSW 到 DiskANN 的范式转移
2.1 HNSW 为什么在大规模场景下成本高企
要理解 DiskANN 的价值,首先得搞清楚 HNSW 的工作原理和它的局限性。
HNSW 的核心思想是「层状跳表 + 贪心搜索」。它构造一个多层图,上层节点的连接稀疏、下层逐渐稠密,查询时从最顶层开始做贪心遍历,逐层收敛到最近邻。这个设计让 HNSW 在小规模数据集上非常快——内存中的图遍历,延迟通常在 1~5ms。
但问题在于:
第一,内存是稀缺资源。 1000 万条 768 维向量,HNSW 需要 80~150GB 内存来加载整个索引。对于私有化部署的中小团队,这几乎是不可能承受的成本。
第二,构建时内存峰值高。 HNSW 在构建阶段需要临时分配大量内存用于图构造,中途无法释放,容易导致 OOM。
第三,动态更新代价大。 HNSW 虽然支持向量插入和删除,但每次插入都需要更新多层图结构。在高并发写入场景下,索引维护成本急剧上升。
第四,召回率与内存占用的两难。 HNSW 有两个关键参数:m(每层每个节点的最大连接数)和 efConstruction(构建时的搜索宽度)。提高这两个参数可以提升召回率,但同时也会成比例地增加内存占用。你不得不在精度和成本之间做权衡。
2.2 DiskANN 的核心思想:SSD-Optimized Graph Index
DiskANN 是微软研究院在 2019 年提出的算法,核心论文《DiskANN: Fast Accurate Billion-point Nearest Neighbor Search on Low-Compatible, Commodity SSDs》详细描述了它的设计。
HNSW 之所以需要全内存,是因为它的图结构是随机访问的——查询过程中,图遍历路径不可预测,任何节点都可能随时被访问。DiskANN 改变了这一假设:它通过精心设计的图结构,保证查询路径具有强局部性,使得大量访问可以合并到顺序读取(Sequential I/O)中,从而充分利用 SSD 的顺序读取带宽。
具体来说,DiskANN 包含三个关键设计:
(1)Vamana 图索引算法
Vamana 是 DiskANN 的核心索引结构,它在构建过程中引入了「贪婪路径增强」(Greedy Path Reinforcement)机制,构造出的图具有以下特性:
- 强局部性:查询路径上的节点在内存地址上相邻,顺序读取效率高
- 短路径长度:保证查询跳数(hops)控制在 O(log n) 量级
- SSD 友好:通过预取(prefetch)和 IO 合并,将随机读转化为顺序读
// Vamana 索引构建的核心参数示意
type VamanaConfig struct {
// 图的最大度(每个节点的最大邻居数),通常 32~64
MaxDegree int
// 构建时的搜索宽度,值越大精度越高但构建越慢
Lbuild int
// 索引剪枝阈值,控制图密度
Alpha float64
}
(2)内存驻留的 Beam 缓存
DiskANN 在内存中维护一个小型的「候选集缓存」(称为 Beam),通常只缓存最近查询的 1~2 层图节点(约几十 MB),而不是整个索引。查询时,Beam 中的节点用于初始贪心搜索,随后通过 SSD 顺序读取补充候选集。
这相当于用几十 MB 的内存换取原来可能需要几十 GB 才能装下的图结构,内存占用降低 2~3 个数量级。
(3)IO 合并与预取优化
这是 DiskANN 能够实用的关键。传统的 SSD 随机读取延迟约为 50100μs,而顺序读取带宽可达 36 GB/s。DiskANN 通过批量请求(IO batching)和预取策略,将大量随机访问合并为少数顺序读取操作,将 IO 效率提升一个数量级。
2.3 StreamingDiskANN:pgvectorscale 的生产级实现
pgvectorscale 对 DiskANN 做了重要的生产级增强,核心是「Streaming」特性——支持动态插入和删除,而不需要像传统 DiskANN 那样离线重建索引。
这对于生产环境至关重要。真实业务中,向量数据是持续增长的:每天可能有数万甚至数百万条新向量入库。StreamingDiskANN 的设计允许增量更新,新增向量通过后台任务追加到索引文件末尾,定期通过 Compaction 合并碎片,而不是每次插入都重建整个索引。
-- StreamingDiskANN 索引创建语法(示意,实际以官方文档为准)
CREATE INDEX ON documents USING vectorscale (
embedding vector_l2_ops (1024) -- 指定维度
) WITH (
index_type = 'StreamingDiskANN',
num_lists = 128,
max_degree = 64
);
三、统计二进制量化(SBQ):用更少的比特换更高的精度
3.1 为什么需要量化?
向量的存储和计算成本是另一个瓶颈。float32 每维度 4 字节,768 维向量就是 3KB。压缩是降低存储成本的核心手段。
最简单的方式是二进制量化(Binary Quantization, BQ):将每个维度映射为 0 或 1,只用 1 bit 表示。这意味着压缩率是 32 倍——3KB 降到约 96 字节。但代价是精度损失严重,因为 1 bit 的信息量实在太小了。
pgvector 自带的 vector 类型默认使用 float32,也可以通过 vector(1536) 的维度参数配合不同的索引类型来实现压缩。但标准的 BQ 在 768 维向量上的召回率通常只有 94%~96%,对于需要高精度的 RAG 场景,这个损失是不可接受的。
3.2 SBQ 的核心原理
pgvectorscale 提出的统计二进制量化(Statistical Binary Quantization)是一个聪明的改进。它的核心洞察是:如果简单地对每个维度独立做 BQ 会丢失太多信息,那么能不能利用维度之间的统计关系来「补偿」信息损失?
SBQ 的处理流程分三步:
第一步:z-score 归一化
对每个维度计算均值 μ 和标准差 σ,然后用 z-score 归一化:z_i = (x_i - μ) / σ
这相当于把所有维度拉到同一个尺度上,消除量纲差异。
第二步:多档位量化
- 2-bit 量化(低维向量,默认 4~256 维):将归一化后的值映射为 4 个档位(00, 01, 10, 11),精度更高
- 1-bit 量化(高维向量,默认 257~65536 维):退化为标准 BQ,但配合 z-score 归一化,精度仍优于原始 BQ
第三步:精度恢复
由于 z-score 归一化保留了每个维度的相对分布信息,在查询时可以通过「统计反向映射」部分恢复精度——这就是 SBQ 相比标准 BQ 召回率更高的根本原因。
腾讯云的测试数据显示,在 768 维 benchmark 中:
| 量化方法 | 召回率 | 存储压缩率 |
|---|---|---|
| 无压缩(float32) | 99.9% | 1x |
| 标准 BQ(1-bit) | 96.5% | 32x |
| SBQ(2-bit 低维 / 1-bit 高维) | 98.6% | ~28x |
召回率从 96.5% 提升到 98.6%,仅牺牲约 12.5% 的额外存储。这个交换在工程上是极为划算的。
3.3 量化的代码实现
import numpy as np
def statistical_binary_quantization(vectors: np.ndarray) -> tuple[np.ndarray, dict]:
"""
SBQ 核心实现
vectors: shape (n, d), float32 向量矩阵
返回: (quantized_bits, stats)
"""
n, d = vectors.shape
# 步骤1: 计算每个维度的均值和标准差(z-score 归一化)
mean = np.mean(vectors, axis=0) # shape (d,)
std = np.std(vectors, axis=0) # shape (d,)
std[std == 0] = 1.0 # 防止除零
normalized = (vectors - mean) / std # shape (n, d)
# 步骤2: 根据维度选择量化策略
if d <= 256:
# 低维: 2-bit 量化,4 档位
# 将 z-score 值映射到 [-2, 2] 区间后量化为 4 个档位
scaled = np.clip(normalized, -2, 2)
quantized = ((scaled + 2) / 4 * 3).astype(np.uint8) # 0~3
bits_per_dim = 2
else:
# 高维: 1-bit 量化
quantized = (normalized > 0).astype(np.uint8) # 0 或 1
bits_per_dim = 1
# 步骤3: 打包为紧凑位数组
# 将每个量化值打包为 bits_per_dim bit
packed = np.packbits(quantized, axis=1)
stats = {
'mean': mean,
'std': std,
'bits_per_dim': bits_per_dim,
'original_dim': d,
'quantized_dims': packed.shape[1] * 8 // bits_per_dim
}
return packed, stats
def sbq_search(query: np.ndarray, quantized_vectors: np.ndarray,
stats: dict, k: int = 10) -> np.ndarray:
"""
基于 SBQ 的近似最近邻搜索
"""
# 反归一化查询向量
normalized_q = (query - stats['mean']) / stats['std']
if stats['bits_per_dim'] == 2:
scaled_q = np.clip(normalized_q, -2, 2)
quantized_q = ((scaled_q + 2) / 4 * 3).astype(np.uint8)
else:
quantized_q = (normalized_q > 0).astype(np.uint8)
# 计算汉明距离(1-bit)或加权汉明距离(2-bit)
if stats['bits_per_dim'] == 1:
distances = np.sum(quantized_vectors != quantized_q, axis=1)
else:
# 2-bit 加权汉明距离:差一档惩罚 1,差两档惩罚 2,差三档惩罚 3
diff = np.abs(quantized_vectors.astype(np.int16) - quantized_q.astype(np.int16))
distances = np.sum(diff, axis=1)
# 返回 top-k 最近邻
top_k_idx = np.argsort(distances)[:k]
return top_k_idx, distances[top_k_idx]
# 演示
if __name__ == '__main__':
np.random.seed(42)
# 模拟 10000 条 768 维向量
corpus = np.random.randn(10000, 768).astype(np.float32)
query = np.random.randn(768).astype(np.float32)
# SBQ 量化
quantized, stats = statistical_binary_quantization(corpus)
print(f"原始大小: {corpus.nbytes / 1024 / 1024:.2f} MB")
print(f"量化后大小: {quantized.nbytes / 1024 / 1024:.2f} MB")
print(f"压缩率: {corpus.nbytes / quantized.nbytes:.1f}x")
print(f"每维 bit 数: {stats['bits_per_dim']}")
# 搜索
top10, dists = sbq_search(query, quantized, stats, k=10)
print(f"Top-10 索引: {top10}")
print(f"对应距离: {dists}")
四、标签过滤搜索:多租户场景的精准检索
4.1 问题:过滤 + 检索的两难
在多租户 SaaS、电商多分类等场景中,向量检索往往需要同时满足两个条件:
- 语义相似度匹配(向量最近邻)
- 业务标签过滤(如
tenant_id = 'company_a',category = 'electronics')
传统做法有两种:
方案 A:先搜索、后过滤
先用向量索引找到 top-k 结果,然后 SQL WHERE 过滤。问题:当过滤条件很严格时(如小租户数据量少),过滤后可能只剩下几条结果,甚至 0 条,召回质量严重下降。
方案 B:先过滤、后搜索
先通过 SQL WHERE 筛选出符合标签条件的向量子集,再在子集内做向量检索。问题:过滤后的数据量可能仍然很大,且每次都需要重建过滤后的索引视图,效率低下。
4.2 Streaming Filtered DiskANN 的解决方案
pgvectorscale 基于微软的 Filtered DiskANN 研究,将标签过滤直接集成到图遍历过程中。核心思想是:在 Vamana 图遍历时,同时维护候选集的有效性标签信息,优先遍历同时满足距离条件和标签条件的路径。
具体实现中,pgvectorscale 采用了两层过滤策略:
层一:In-index Filtering(索引内过滤)
对于带 = 或 IN 的精确过滤条件,pgvectorscale 在索引构建时就在图结构中嵌入了标签信息。遍历时,系统自动跳过不满足标签条件的分支,只在有效的子图内做向量检索。这避免了大量无效 IO。
-- 多租户场景:查询与 query_vector 最相似的文档,但只返回 tenant_id = 'tenant_123' 的结果
SELECT id, content,
embedding <=> '[0.123, ...]' AS distance
FROM documents
WHERE tenant_id = 'tenant_123' -- 标签过滤
ORDER BY embedding <=> '[0.123, ...]'
LIMIT 20;
在启用了 StreamingDiskANN + Filtered Index 的情况下,这个查询的执行计划会先通过索引层做标签裁剪,将搜索空间从全量数据缩小到特定租户的数据子集,再在子集内执行向量检索。
层二:Streaming Post-filtering(流式后过滤)
对于更灵活的 WHERE 条件(如范围查询 price BETWEEN 100 AND 500),pgvectorscale 提供了流式后过滤机制——从向量索引中持续获取候选结果,同时在后处理阶段应用过滤条件,直到收集到足够数量的有效结果。
def streaming_post_filter_search(
index,
query_vector: np.ndarray,
filter_fn, # 过滤函数: lambda row: 100 <= row['price'] <= 500
k: int = 20,
max_candidates: int = 1000
):
"""
流式后过滤搜索:
持续从向量索引获取候选,直到找到 k 个满足过滤条件的有效结果
"""
candidates = []
seen = set()
# 流式地从索引获取候选
stream = index.search_streaming(query_vector, ef=max_candidates)
for candidate in stream:
if candidate.id in seen:
continue
seen.add(candidate.id)
# 应用后过滤
if filter_fn(candidate.row):
candidates.append(candidate)
if len(candidates) >= k:
break
return sorted(candidates, key=lambda c: c.distance)
这种设计的优势在于:即使过滤条件很严格,只要索引中确实存在满足条件的数据,系统会通过增加候选集扫描范围来保证召回数量,而不是简单地返回空结果。
五、代码实战:从零搭建生产级 RAG 向量检索系统
5.1 架构设计
┌─────────────┐ ┌──────────────┐ ┌─────────────────────┐
│ 文档输入 │────▶│ 文本向量化 │────▶│ PostgreSQL + pgvec │
│ (PDF/TXT) │ │ (Embedding) │ │ scale (DiskANN) │
└─────────────┘ └──────────────┘ └─────────────────────┘
│ │
▼ ▼
┌──────────────┐ ┌─────────────────────┐
│ 查询向量化 │────▶│ 语义相似度检索 │
│ (Query Emb) │ │ + 标签过滤 │
└──────────────┘ └─────────────────────┘
5.2 数据库初始化
-- 创建扩展(腾讯云 PostgreSQL 已预装 pgvectorscale)
CREATE EXTENSION IF NOT EXISTS vectorscale CASCADE;
-- 创建文档表
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
tenant_id TEXT NOT NULL, -- 多租户隔离字段
category TEXT NOT NULL, -- 分类标签
title TEXT NOT NULL,
content TEXT NOT NULL,
embedding VECTOR(1024), -- 1024 维向量(OpenAI text-embedding-3-small)
metadata JSONB, -- 灵活扩展字段
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- 创建 StreamingDiskANN 索引(支持多租户过滤)
CREATE INDEX idx_docs_embedding_diskann ON documents
USING vectorscale (
embedding vector_l2_ops (1024)
) WITH (
index_type = 'StreamingDiskANN',
num_lists = 256,
max_degree = 64
);
-- 创建标签过滤索引(用于精确匹配过滤)
CREATE INDEX idx_docs_tenant ON documents (tenant_id);
CREATE INDEX idx_docs_category ON documents (category);
-- HNSW 索引(对比用:内存优先场景)
CREATE INDEX idx_docs_embedding_hnsw ON documents
USING hnsw (embedding vector_l2_ops (1024))
WITH (m = 16, ef_construction = 128);
COMMENT ON INDEX idx_docs_embedding_diskann IS
'StreamingDiskANN 磁盘索引:千万级向量,内存占用降低 90%+';
5.3 Python 端到端实现
import numpy as np
from datetime import datetime
from dataclasses import dataclass
from typing import Optional
import psycopg2
from psycopg2.extras import execute_values
@dataclass
class Document:
id: Optional[int]
tenant_id: str
category: str
title: str
content: str
embedding: np.ndarray
metadata: dict
def to_tuple(self) -> tuple:
return (
self.tenant_id, self.category, self.title, self.content,
self.embedding.tolist(), self.metadata
)
class RAGVectorStore:
"""
基于 PostgreSQL pgvectorscale 的生产级 RAG 向量存储
支持 StreamingDiskANN + SBQ 压缩 + 标签过滤
"""
def __init__(self, connection_string: str):
self.conn = psycopg2.connect(connection_string)
self.conn.autocommit = True
def insert_documents(self, documents: list[Document],
batch_size: int = 1000) -> int:
"""
批量插入文档(支持 SBQ 自动压缩)
"""
inserted = 0
for i in range(0, len(documents), batch_size):
batch = documents[i:i + batch_size]
values = [doc.to_tuple() for doc in batch]
sql = """
INSERT INTO documents
(tenant_id, category, title, content, embedding, metadata)
VALUES %s
ON CONFLICT DO NOTHING
"""
with self.conn.cursor() as cur:
execute_values(cur, sql, values)
inserted += len(batch)
return inserted
def search(
self,
query_vector: np.ndarray,
tenant_id: str,
category: Optional[str] = None,
k: int = 5,
use_diskann: bool = True,
min_distance: float = 0.0
) -> list[dict]:
"""
语义检索核心方法
参数:
query_vector: 查询向量 (1024,)
tenant_id: 租户过滤(必填,保障数据隔离)
category: 可选分类过滤
k: 返回结果数
use_diskann: True=DiskANN(内存友好), False=HNSW(精度优先)
min_distance: 最小相似度阈值
"""
# 构建 WHERE 条件
conditions = ["tenant_id = %s"]
params = [tenant_id]
if category:
conditions.append("category = %s")
params.append(category)
where_clause = " AND ".join(conditions)
# 选择索引执行检索
index_hint = (
"USING vectorscale (embedding vector_l2_ops (1024))"
if use_diskann else
"USING hnsw (embedding vector_l2_ops (1024))"
)
sql = f"""
SELECT
id, tenant_id, category, title, content,
embedding <=> %s AS distance,
metadata
FROM documents
WHERE {where_clause}
AND embedding <=> %s < %s
ORDER BY embedding <=> %s
LIMIT %s
"""
params.extend([query_vector.tolist()] * 4)
params.append(k)
results = []
with self.conn.cursor() as cur:
cur.execute(sql, params)
for row in cur.fetchall():
results.append({
'id': row[0],
'tenant_id': row[1],
'category': row[2],
'title': row[3],
'content': row[4],
'distance': float(row[5]),
'metadata': row[6]
})
return results
def search_with_rerank(
self,
query_vector: np.ndarray,
query_text: str,
tenant_id: str,
k: int = 20, # 第一阶段召回数
top_k: int = 5, # 最终返回数
) -> list[dict]:
"""
两阶段检索 + 重排(用于提升 RAG 精度)
Stage 1: DiskANN 粗召回(k 条)
Stage 2: 重排模型精排序(top_k 条)
"""
# Stage 1: 粗召回
candidates = self.search(
query_vector, tenant_id, k=k, use_diskann=True
)
if not candidates:
return []
# Stage 2: 简单重排(用向量余弦相似度)
# 生产环境建议用 Cross-Encoder 如 bge-reranker-v2-m3
reranked = []
for doc in candidates:
# 计算与查询的余弦相似度(重排用)
doc_vec = np.array(
self._get_embedding(doc['content'])
)
cosine_sim = np.dot(query_vector, doc_vec) / (
np.linalg.norm(query_vector) * np.linalg.norm(doc_vec) + 1e-8
)
doc['rerank_score'] = float(cosine_sim)
reranked.append(doc)
# 按重排分数排序
reranked.sort(key=lambda x: x['rerank_score'], reverse=True)
return reranked[:top_k]
def _get_embedding(self, text: str) -> list:
"""
调用 Embedding API 获取向量
实际项目中替换为 OpenAI / 智谱 / 本地模型调用
"""
# 模拟: 实际应调用 embedding API
# 这里返回随机向量作为占位
return np.random.randn(1024).tolist()
def benchmark(self, test_vectors: np.ndarray,
tenant_id: str) -> dict:
"""
性能基准测试:对比 DiskANN vs HNSW
"""
import time
results = {}
for index_name, use_diskann in [('DiskANN', True), ('HNSW', False)]:
times = []
for vec in test_vectors:
start = time.perf_counter()
self.search(vec, tenant_id, k=10, use_diskann=use_diskann)
elapsed = (time.perf_counter() - start) * 1000 # ms
times.append(elapsed)
times.sort()
results[index_name] = {
'avg_ms': np.mean(times),
'p50_ms': np.percentile(times, 50),
'p95_ms': np.percentile(times, 95),
'p99_ms': np.percentile(times, 99),
}
return results
def close(self):
self.conn.close()
# 使用示例
if __name__ == '__main__':
store = RAGVectorStore(
connection_string="postgresql://user:pass@host:5432/ragdb"
)
# 模拟文档插入
docs = [
Document(
id=None,
tenant_id='tenant_a',
category='technology',
title='PostgreSQL 向量检索最佳实践',
content='pgvectorscale 扩展提供了 StreamingDiskANN 索引...',
embedding=np.random.randn(1024),
metadata={'author': '三哥', 'tags': ['pg', 'vector', 'ai']}
)
for _ in range(100)
]
inserted = store.insert_documents(docs)
print(f"已插入 {inserted} 条文档")
# 语义检索
query_vec = np.random.randn(1024)
results = store.search(
query_vector=query_vec,
tenant_id='tenant_a',
category='technology',
k=5,
use_diskann=True
)
for r in results:
print(f" [{r['distance']:.4f}] {r['title']}")
store.close()
六、性能优化:从基准测试到生产调参
6.1 索引参数调优指南
StreamingDiskANN 有几个关键参数,直接影响索引质量和查询性能:
| 参数 | 说明 | 推荐值 | 调优建议 |
|---|---|---|---|
num_lists | 分区数,影响构建时间和精度 | 128~1024 | 数据量越大,值越大 |
max_degree | 每个节点最大邻居数 | 32~128 | 精度优先用大值,内存优先用小值 |
ef_search | 查询时的搜索宽度 | 50~500 | 精度优先用大值,延迟优先用小值 |
compression | 是否启用 SBQ | auto / on / off | 大规模数据建议 auto |
6.2 存储成本对比
| 方案 | 1000万条768维向量内存占用 | 预估成本/月 |
|---|---|---|
| 纯内存 HNSW | ~120 GB | ¥3000~6000(云 RDS) |
| StreamingDiskANN + SBQ | ~15 GB | ¥400~800(云 RDS) |
| 专用向量数据库(Milvus) | ~80 GB | ¥5000+(含维护成本) |
pgvectorscale 的成本优势主要来自两个方面:内存占用降低一个数量级(DiskANN 的 Beam 缓存只需几十 MB 而不是整个索引),以及存储压缩(SBQ 28倍压缩率)。
6.3 常见问题排查
-- 查看索引使用情况
SELECT
indexrelname,
idx_scan,
idx_tup_read,
idx_tup_fetch
FROM pg_stat_user_indexes
WHERE indexrelname LIKE '%embedding%';
-- 查看向量列大小
SELECT
pg_size_pretty(pg_column_size(embedding)) as col_size,
pg_column_size(embedding) as bytes_per_row
FROM documents LIMIT 1;
-- 检查 pgvectorscale 扩展版本
SELECT * FROM pg_available_extensions WHERE name = 'vectorscale';
-- 查看索引构建进度(构建大索引时)
SELECT phase,
round(100.0 * blocks_done / nullif(blocks_total, 0), 2) AS progress
FROM pg_progress_index(
'idx_docs_embedding_diskann'::regclass
);
七、总结与展望:pgvectorscale 代表的工程方向
pgvectorscale 的出现,本质上解决的是一个很朴素的问题:在大规模 AI 向量场景下,如何让 PostgreSQL 既有关系型数据库的成熟度和 ACID 保证,又有专用向量数据库的检索性能,同时还能控制住成本。
从技术演进的角度看,它代表了一个明显的趋势:数据库正在从「通用」走向「AI-Native」。 传统的关系型数据库通过扩展(pgvector、pgvectorscale、pg_embedding 等)逐步支持向量能力,而专用向量数据库(Milvus、Qdrant)也在借鉴 PostgreSQL 的生态优势。这种交汇最终受益的是开发者——你不需要在两个系统之间做痛苦的 ETL,可以用一套 SQL 搞定结构化查询和向量检索。
几个值得关注的演进方向:
1. 混合查询优化(Hybrid Search)
未来 pgvectorscale 可能将结构化字段过滤和向量检索更深度地融合,实现真正的「结构化 + 向量」联合优化执行计划,而不是现在的两层过滤。
2. 实时索引更新
StreamingDiskANN 的增量索引能力还在成熟中,未来可能实现完全在线的索引更新,不需要 Compaction 阶段,进一步降低维护成本。
3. 与 PG 18/19 的 DDL 增强结合
PostgreSQL 19 即将引入的 pg_get_database_ddl() 等 DDL 工具,可以更方便地将向量索引纳入数据库迁移和版本管理流程。
4. GPU 加速的量化计算
SBQ 的 z-score 计算和量化操作在 CPU 上仍有优化空间,未来可能通过 SIMD/AVX-512 指令集或 CUDA 加速,进一步降低查询延迟。
对于正在构建 AI 应用的一线工程师,我的建议是:如果你的向量数据量在百万级以下,直接用 pgvector + HNSW 就够了;但一旦突破千万级,或者你的内存预算有限,pgvectorscale 的 StreamingDiskANN 是目前最具性价比的过渡方案。 它不需要你换数据库,不需要引入新的运维体系,只需要在现有 PostgreSQL 上开一个扩展,就能用 1/10 的成本跑出接近专用向量数据库的检索性能——这在工程上是很划算的买卖。
AI 向量检索的战争才刚刚开始,而 pgvectorscale 让这场战争的天平,开始向 PostgreSQL 倾斜。
本文数据来源:腾讯云官方文档(2026-04-14)、微软研究院 DiskANN 论文、pgvectorscale GitHub 仓库基准测试。生产环境使用前请参考官方最新文档。