未记录事实核验 · 状态:维护中
记录仅说明核验时间与维护状态,不保证适用于所有环境;使用前请留意正文中的版本、适用条件与风险提示。
warning 适用边界与维护说明
本文适合具有 PostgreSQL 运维经验的读者组织容量测量、执行计划排查、索引构建监控及扩展方案讨论,不是可整段执行的生产部署手册;本次仅作全文结构审阅,未执行代码或进行外部事实核验。索引倍数、构建耗时、QPS、延迟及参数对应召回率应作为待测假设,单机容量上限、硬件加速倍数和 StreamingDiskANN、Citus 等选型结论也不能作为已验证的生产承诺。3.2、4.3 已修正普通 HNSW 搜索与 work_mem、top-N 排序及查询并行的核心说明,但全文仍有监控视图名称错误、不可执行的批量召回率 SQL,以及缓存、在线建索引和 halfvec 总体收益的前后不一致。终止会话、删除索引和向量列的示例缺少对象确认、依赖处理及回滚保障,不宜直接用于生产。Citus 与应用层分区代码仅供架构讨论,需补齐部署及应用依赖;跨分区结果合并须使用相同查询向量、距离度量和排序规则,而当前示例仍未统一传递查询向量。两个指定阅读前置目前仅保留名称,8.1 所需过滤分区背景无法通过本文导航补足。 已取消无法确定目标的可点击引用「06-索引原理与调优」,保留原提及;Missing link target: 06-索引原理与调优 已取消无法确定目标的可点击引用「07-混合搜索与过滤优化」,保留原提及;Missing link target: 07-混合搜索与过滤优化 已取消无法确定目标的可点击引用「#六、查询参数调优:ef_search 与 probes」,保留原提及;Missing anchor: #六、查询参数调优:ef_search 与 probes
pgvector 生产运维与性能调优
abstract 本文目标
- 掌握内存配置:理解
work_mem、maintenance_work_mem、shared_buffers对向量查询的影响 - 掌握并行优化:用
max_parallel_workers_per_gather加速向量搜索 - 掌握索引构建:大表建 HNSW 索引的工程化最佳实践
- 建立监控体系:用 PostgreSQL 原生视图监控向量查询状态
- 解决常见问题:Seq Scan、索引卡住、召回率下降的根因和解法
- 了解扩展方案:何时需要 Citus 分片,如何规划容量
阅读前置:索引原理与调优 混合搜索与过滤优化 PostgreSQL 生态集成
目录
- 一、运维哲学:一个你已熟悉的老朋友
- 二、容量规划:存储与内存估算
- 三、内存配置深度解析
- 四、并行查询:让多核 CPU 真正工作
- 五、索引构建优化:大表建索引的工程实践
- #六、查询参数调优:ef_search 与 probes
- 七、监控体系:PostgreSQL 原生视图的用法
- 八、常见性能问题排查手册
- 九、扩展性上限与突破方案
- 十、生产配置速查表
一、运维哲学:一个你已熟悉的老朋友
对于已有 PostgreSQL 运维经验和基础设施的团队,pgvector 可以复用现有工具;是否比专用向量数据库更易运维,取决于规模、负载和团队经验。
不是因为 pgvector 本身有多简单,而是因为你面对的是 PostgreSQL——一个可能已经在你公司生产环境运行了多年的老朋友。它只是最近多了一个新技能:向量搜索。
你已经会的那些技能全部适用:
- 用
EXPLAIN ANALYZE分析慢查询 - 用
pg_stat_activity看当前在干什么 - 用
pg_stat_indexes看索引是否被使用 - 用
work_mem、shared_buffers调内存 - 用
vacuum、analyze维护表健康
向量查询引入的新概念不多,核心就这几个:
- HNSW / IVFFlat 索引的参数
ef_search/probes这两个搜索时的质量参数- 向量列的存储估算
一旦把这些新概念和你已有的 PostgreSQL 知识融合,你就能调出一个高性能、稳定运行的向量数据库系统。
本文就是这个融合的过程。
二、容量规划:存储与内存估算
运维的第一步永远是规划。给向量数据规划存储,比普通数据更直观——因为向量的大小是精确可算的。
2.1 向量数据的存储计算
向量存储的核心公式:
单行向量存储 = 维度数 × 每维字节数 + 元数据开销
float32(标准精度):每维 4 字节
float16(半精度):每维 2 字节(pgvector 0.7.0+ 的 halfvec 类型)
常见 Embedding 模型的存储估算:
| 模型 | 维度 | float32/行 | float16/行 | 1000 万行(float32) |
|---|---|---|---|---|
| OpenAI text-embedding-3-small | 1536 | 6.0 KB | 3.0 KB | 约 57 GB |
| OpenAI text-embedding-3-large | 3072 | 12.0 KB | 6.0 KB | 约 114 GB |
| OpenAI text-embedding-ada-002 | 1536 | 6.0 KB | 3.0 KB | 约 57 GB |
| nomic-embed-text(Ollama) | 768 | 3.0 KB | 1.5 KB | 约 28 GB |
| BGE-M3 | 1024 | 4.0 KB | 2.0 KB | 约 38 GB |
| Cohere embed-v3 | 1024 | 4.0 KB | 2.0 KB | 约 38 GB |
note 为什么 1536 维 = 6 KB
1536 维 × 4 字节(float32)= 6144 字节 ≈ 6 KB 这是最简单的线性关系。PostgreSQL 还会有每行约 23 字节的 tuple header 开销, 加上业务字段(文本内容通常是向量存储的大头),实际每行总存储通常是 10~30 KB。
2.2 索引存储估算
索引本身也占空间。HNSW 索引的存储通常是原始向量数据的 1.5~2.5 倍:
HNSW 索引大小 ≈ 向量数据大小 × 1.5 ~ 2.5
影响因素:
- m 值越大,索引越大(每个节点存更多邻居信息)
- m=16(默认):约 1.8 倍
- m=32:约 2.5 倍
- m=8:约 1.2 倍
IVFFlat 索引比 HNSW 轻量得多:
IVFFlat 索引包含聚类中心及保存向量数据的索引条目,不存 HNSW 图结构;不能仅按聚类中心估算大小,应以目标数据实际建索引后的大小进行规划。
2.3 完整容量规划示例
场景:一个 RAG 知识库,1000 万篇文档,每篇存标题(100 字节)+ 摘要(500 字节)+ 1536 维向量
向量数据:
1000万 × 1536维 × 4字节 = 61,440,000,000 字节 ≈ 57 GB
文本数据(标题 + 摘要):
1000万 × 600字节 = 6,000,000,000 字节 ≈ 6 GB
HNSW 索引(m=16):
57 GB × 1.8 ≈ 103 GB
其他字段、MVCC 开销、toast 等:
约 10 GB
总存储需求:
57 + 6 + 103 + 10 ≈ 176 GB
内存需求(HNSW 查询性能最优时):
最理想状态:HNSW 索引完全在内存中
103 GB 索引 → 需要约 120 GB RAM(留 15% 余量)
现实情况:索引部分在内存,部分在磁盘
这时 StreamingDiskANN(pgvectorscale)比 HNSW 更适合
2.4 使用 halfvec 减半存储
pgvector 0.7.0 引入了 halfvec 类型(半精度,float16)。在精度损失可接受的场景下,这是最简单的存储优化:
-- 创建 halfvec 列(精度从 float32 降到 float16)
CREATE TABLE documents_half (
id SERIAL PRIMARY KEY,
content TEXT,
-- halfvec:每维 2 字节,是 vector(1536) 的一半存储
embedding HALFVEC(1536)
);
-- 精度对比:float32 vs float16
-- 可将差异 < 0.5% 作为待验证的示例假设,但需明确评价指标并实测
-- 每维向量载荷字节数减少 50%;表、索引和查询总内存不保证减半,缓存收益需测量
-- 查询语法和 vector 完全一样
SELECT *
FROM documents_half
ORDER BY embedding <=> '[...]'::halfvec
LIMIT 10;
tip halfvec 的使用建议
- 文本 Embedding 的语义搜索:可评估 halfvec,但应使用代表性查询对照 float32 测量召回率和排序变化,再判断精度损失是否可接受
- 图像向量、精确数值计算:建议保持 float32
- 已有 vector 列迁移到 halfvec:需要重建表,成本较高;新项目直接用 halfvec
三、内存配置深度解析
内存配置是 pgvector 性能调优的核心。向量查询比普通查询更"吃内存",理解原因才能配好参数。
3.1 为什么向量查询特别耗内存
普通 SQL 查询(比如 SELECT * FROM orders WHERE user_id = 123):
- 通过 B-tree 索引定位几条记录
- 内存消耗很小,通常几 KB 到几 MB
向量查询(比如 SELECT * FROM documents ORDER BY embedding <=> '[...]' LIMIT 10):
- HNSW 搜索需要在图结构中游走,维护一个优先队列
- 优先队列的大小取决于
ef_search参数(默认 40) - 每个候选节点都需要计算距离,距离计算需要把向量加载到内存
- 即使只返回 10 条结果,中间可能要"看过"数百甚至数千个节点
内存消耗的直觉模型:
想象 HNSW 索引是一座多层大楼,你要找"距离某个点最近的 10 个房间"
传统搜索:直接去一楼前台,前台翻名单,O(log N) 的查询
HNSW 搜索:在大楼里走动,从高层开始,逐层向下缩小范围
- 每层都要"看"ef_search 个候选房间
- 这些候选房间的向量数据要加载到内存
- ef_search 越大,"看"的越多,结果越准,内存越多
3.2 work_mem:向量排序的关键参数
work_mem 控制单个查询操作(排序、哈希连接等)能使用的内存量。
-- 查看当前设置
SHOW work_mem;
-- 典型的 PostgreSQL 默认值
-- work_mem = 4MB(严重不足向量场景)
向量查询为什么需要更大的 work_mem:
当查询走 HNSW 索引时:
普通 HNSW 搜索的候选规模主要由 ef_search 控制,不能将其直接视为 work_mem 限制的排序;支持迭代扫描的版本还需另行考虑 hnsw.scan_mem_multiplier 与 work_mem 的关系
default ef_search=40, 1536维 float32
每个候选向量 = 6KB
40 个候选 × 6KB = 240KB(看起来不多)
当查询没走索引,退化到 seq scan + 排序时:
顺序扫描需要对候选行计算距离,但带 LIMIT 的查询可能使用 top-N heapsort
排序内存取决于执行计划、保留的结果数和排序元组宽度,不能按 N × 向量大小直接估算
是否溢出到磁盘应通过 EXPLAIN (ANALYZE, BUFFERS) 中的排序方法与磁盘使用信息判断
配置建议:
-- 全局设置(postgresql.conf)
-- 适合向量查询较多的场景
work_mem = 64MB
-- 原因:
-- 1. 向量索引搜索的优先队列通常 < 10MB
-- 2. 防止小范围的 seq scan + sort 溢盘
-- 3. 64MB 是一个平衡点(过大会导致多并发时 OOM)
-- 注意:work_mem 是"每个操作"的限制,不是每个连接
-- 一个复杂查询可能有多个排序/连接操作,实际使用可能是 work_mem 的倍数
-- 针对特定大查询,会话级临时提高
SET work_mem = '256MB';
SELECT * FROM documents
ORDER BY embedding <=> '[...]'::vector
LIMIT 100;
RESET work_mem; -- 记得重置
work_mem 与并发的关系:
公式:
总内存消耗上限 ≈ max_connections × 并发操作数 × work_mem
示例:
max_connections = 200
平均每个查询 2 个操作(排序 + 哈希)
work_mem = 64MB
理论上限:200 × 2 × 64MB = 25.6GB
结论:work_mem 不是越大越好,要结合 max_connections 和实际内存来设置
3.3 maintenance_work_mem:索引构建的速度开关
maintenance_work_mem 是专门为维护操作(CREATE INDEX、VACUUM、CLUSTER)准备的内存。这个参数对 HNSW 索引构建速度有决定性影响。
-- 查看当前设置
SHOW maintenance_work_mem;
-- 默认值通常是 64MB,对于大表建索引严重不足
-- 为什么 maintenance_work_mem 影响 HNSW 构建速度?
-- HNSW 构建过程:
-- 1. 逐个插入向量节点
-- 2. 为每个节点找最近的邻居(ef_construction 个候选)
-- 3. 把图结构存在内存中
-- 如果内存不足 → 频繁 I/O → 构建速度暴降
实际配置建议:
-- 临时提升:会话级,只影响当前连接的操作
SET maintenance_work_mem = '2GB';
CREATE INDEX idx_documents_hnsw ON documents
USING hnsw(embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
RESET maintenance_work_mem;
-- 或者通过 ALTER SYSTEM 全局修改(需要 reload)
ALTER SYSTEM SET maintenance_work_mem = '1GB';
SELECT pg_reload_conf();
-- 未验证的示例假设(1000万行,1536维,m=16;以下耗时不是实测结论):
-- maintenance_work_mem = 64MB:构建时间约 8 小时
-- maintenance_work_mem = 1GB:构建时间约 2 小时
-- maintenance_work_mem = 4GB:构建时间约 45 分钟
warning 设置 maintenance_work_mem 的注意事项
maintenance_work_mem 只影响维护操作(建索引、VACUUM),不影响普通查询。
临时调大后记得重置,否则并行的 VACUUM 操作也会使用这个大值,可能导致 OOM。
最安全的做法:在专用会话里 SET maintenance_work_mem = '2GB',建完索引后关闭会话,自动重置。
3.4 shared_buffers:向量索引的"内存缓存"
shared_buffers 是 PostgreSQL 的共享缓冲区,决定了多少数据页能被缓存在内存中。
工作原理:
PostgreSQL 读取数据 → 先查 shared_buffers(如果命中,不用磁盘 I/O)
→ 未命中,从磁盘读入 shared_buffers,再返回
向量场景的特殊性:
HNSW 索引的大小经常是数十到上百 GB
shared_buffers 能放下多少,就有多少是"热"的
放不下的部分 → 每次查询都要磁盘 I/O → 延迟上升
配置建议:
-- 查看当前设置
SHOW shared_buffers;
-- 通用建议:设为物理内存的 25%
-- 如果机器主要用于 PostgreSQL 向量搜索,可以设到 40%
-- 修改(需要重启 PostgreSQL)
-- 在 postgresql.conf 中:
-- shared_buffers = 32GB # 假设物理内存 128GB
-- 查看当前缓存命中率(越接近 1 越好)
SELECT
sum(heap_blks_hit) AS heap_hits,
sum(heap_blks_read) AS heap_reads,
round(
sum(heap_blks_hit)::numeric /
NULLIF(sum(heap_blks_hit) + sum(heap_blks_read), 0),
4
) AS hit_ratio
FROM pg_statio_user_tables;
-- 向量表的缓存命中率(如果这个值很低,考虑增大 shared_buffers)
SELECT
schemaname,
relname AS tablename,
heap_blks_hit,
heap_blks_read,
round(
heap_blks_hit::numeric /
NULLIF(heap_blks_hit + heap_blks_read, 0),
4
) AS hit_ratio
FROM pg_statio_user_tables
WHERE relname IN ('documents', 'articles')
ORDER BY heap_blks_read DESC;
3.5 有效缓存大小:告诉优化器内存有多大
effective_cache_size 不直接分配内存,而是告诉查询优化器"系统大概有多少内存可用于缓存"。优化器用这个值来估算走索引还是走全表扫描更划算。
-- 推荐设置:shared_buffers + OS 文件系统缓存的预估值
-- 通常设为物理内存的 50%~75%
-- 在 postgresql.conf 中:
-- effective_cache_size = 96GB # 物理内存 128GB 时
-- 为什么这对向量查询很重要?
-- 如果 effective_cache_size 设得太小,优化器会认为内存不足
-- 倾向于选择 seq scan(内存估算低 → 认为走索引还要大量磁盘 I/O)
-- 导致明明应该走 HNSW 索引的查询,走了全表扫描
四、并行查询:让多核 CPU 真正工作
PostgreSQL 支持并行查询,向量搜索可以从多核并行中获益。但并行查询的配置需要仔细调整。
4.1 并行查询的基本原理
单核查询:
1 个 Leader 进程,扫描所有向量,维护 top-K 优先队列
并行查询(4 个 Worker):
1 个 Leader + 4 个 Worker
Worker 各自扫描一部分数据,各自维护局部 top-K
Leader 汇总 4 个局部 top-K,得到全局 top-K
理论加速:约 4 倍(实际 2~3 倍,因为有协调开销)
4.2 并行查询的配置参数
-- 查看当前并行设置
SHOW max_parallel_workers_per_gather; -- 每个查询节点最多用几个 Worker
SHOW max_parallel_workers; -- 全局并行 Worker 总数上限
SHOW min_parallel_table_scan_size; -- 表多大时才考虑并行
-- 推荐配置(16 核服务器)
-- 在 postgresql.conf 中:
-- max_parallel_workers_per_gather = 4 # 每个查询节点 4 个 Worker
-- max_parallel_workers = 8 # 全局 8 个并行 Worker
-- min_parallel_table_scan_size = 8MB # 小表不走并行
-- 注意:max_parallel_workers 不要超过 CPU 核数的 50%
-- 否则并行查询会抢占其他事务的 CPU 资源
4.3 并行查询的适用场景
并行查询对向量搜索的加速效果取决于查询类型:
走 HNSW 索引的查询:
普通 HNSW 索引扫描不能因提高 max_parallel_workers_per_gather 就获得多 worker 并行搜索
应分别检查索引扫描、并行顺序扫描和并行索引构建的执行能力,不提供固定加速比例
没走索引的顺序扫描(seq scan):
✅ 并行加速效果显著
原因:顺序扫描可以完美分片
示例假设:4 并行提升约 3~3.5 倍;实际收益需依据执行计划和目标负载测量
聚合查询(如统计最相似的分类分布):
✅ 向量距离计算 + 聚合,并行效果好
4.4 强制并行执行(测试用)
-- 并行计划测试:以下设置不保证得到并行扫描或加速;force_parallel_mode 在 PostgreSQL 16 起更名为 debug_parallel_query,应使用实际版本对应的参数
SET force_parallel_mode = on;
SET max_parallel_workers_per_gather = 4;
EXPLAIN ANALYZE
SELECT id, title, 1 - (embedding <=> '[...]'::vector) AS sim
FROM documents
ORDER BY embedding <=> '[...]'::vector
LIMIT 10;
-- 在 EXPLAIN 输出中寻找:
-- "Gather" 节点 → 说明走了并行
-- "Workers Planned: 4" → 规划了 4 个 Worker
-- "Workers Launched: 4" → 实际启动了 4 个 Worker
RESET force_parallel_mode;
RESET max_parallel_workers_per_gather;
4.5 并行索引构建
pgvector 0.6.0 引入了 HNSW 的并行索引构建支持,这对大表建索引非常有用:
-- 启用并行索引构建
SET max_parallel_workers = 8;
SET max_parallel_maintenance_workers = 4; -- 专门用于维护操作的并行 Worker
-- 并行构建 HNSW 索引
CREATE INDEX CONCURRENTLY idx_documents_hnsw
ON documents
USING hnsw(embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- CONCURRENTLY:在线构建索引,不阻塞其他读写操作
-- 代价:构建时间稍长(约 1.5 倍),但生产环境必须用
-- 查看索引构建进度(PostgreSQL 12+)
SELECT
phase,
round(100.0 * blocks_done / NULLIF(blocks_total, 0), 2) AS progress_pct,
blocks_done,
blocks_total,
tuples_done,
tuples_total
FROM pg_stat_progress_create_index
WHERE relid = 'documents'::regclass;
五、索引构建优化:大表建索引的工程实践
大表建 HNSW 索引是生产运维中最常遇到的挑战之一。一张千万行的表,配置不当可能需要跑几十个小时。
5.1 建索引前的准备工作
-- 1. 确认表的数据量和向量维度
SELECT
relname AS table_name,
n_live_tup AS row_count,
pg_size_pretty(pg_total_relation_size(relid)) AS total_size
FROM pg_stat_user_tables
WHERE relname = 'documents';
-- 2. 确认没有大量空向量(空向量会导致索引效率下降)
SELECT
COUNT(*) AS total_rows,
COUNT(embedding) AS rows_with_embedding,
COUNT(*) - COUNT(embedding) AS null_embeddings
FROM documents;
-- 3. 先 VACUUM ANALYZE,让优化器有准确的统计信息
VACUUM ANALYZE documents;
5.2 推荐的建索引流程
流程一:在线建索引(推荐生产环境)
-- 步骤 1:提升当前会话的 maintenance_work_mem
SET maintenance_work_mem = '4GB';
-- 步骤 2:启用并行
SET max_parallel_maintenance_workers = 4;
-- 步骤 3:使用 CONCURRENTLY 在线建索引(允许常规读写继续,但仍会获取锁并可能等待事务;不能在事务块内执行)
CREATE INDEX CONCURRENTLY idx_documents_hnsw_cosine
ON documents
USING hnsw(embedding vector_cosine_ops)
WITH (
m = 16, -- 每节点最大连接数(默认 16,平衡点)
ef_construction = 64 -- 构建时的搜索范围(默认 64)
);
-- 步骤 4:验证索引已创建
SELECT
indexrelname AS indexname,
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
WHERE relname = 'documents' AND indexrelname = 'idx_documents_hnsw_cosine';
流程二:离线建索引(新建表,速度最快)
-- 如果是新建表或者允许短暂停机,可以不用 CONCURRENTLY
-- 速度比 CONCURRENTLY 快约 30%
-- 分步执行,便于监控和中断重试
BEGIN;
-- 临时锁表,防止写入导致索引不完整(非 CONCURRENTLY 的行为)
LOCK TABLE documents IN SHARE MODE;
SET LOCAL maintenance_work_mem = '4GB';
SET LOCAL max_parallel_maintenance_workers = 4;
CREATE INDEX idx_documents_hnsw_cosine
ON documents
USING hnsw(embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
COMMIT;
5.3 HNSW 参数选择指南
m 参数(每个节点的最大邻居连接数):
默认值:16
范围:2 ~ 100
m = 4~8: 内存最省,但召回率偏低(适合内存极度紧张的场景)
m = 16: 默认值,大多数场景的最佳平衡点
m = 32: 更高召回率,索引约大 50%(适合精度要求高的场景)
m = 64: 可作为较大连接数的测试配置,通常增大索引;不能保证召回率接近 100%
ef_construction 参数(构建时的搜索宽度):
默认值:64
范围:4 ~ 1000
ef_construction = 32: 构建快,召回率约 90%
ef_construction = 64: 默认,构建适中,召回率约 95%
ef_construction = 128: 构建慢约 2 倍,召回率约 98%
ef_construction = 256: 构建慢约 4 倍,召回率约 99%
推荐配置组合:
开发/测试:m=8, ef_construction=32(快速构建)
生产标准:m=16, ef_construction=64(默认,大多数场景)
高精度要求:m=32, ef_construction=128(推荐用于医疗、法律等场景)
5.4 分批建索引(超大表的方案)
对于超过 5000 万行的表,可以考虑先按分区建表,再逐个分区建索引:
-- 方案 1:使用 PostgreSQL 原生表分区
-- 先创建分区主表
CREATE TABLE documents_partitioned (
id BIGSERIAL,
content TEXT,
embedding VECTOR(1536),
created_at TIMESTAMP DEFAULT NOW()
) PARTITION BY RANGE (id);
-- 创建分区(每个分区 500 万行)
CREATE TABLE documents_part_0 PARTITION OF documents_partitioned
FOR VALUES FROM (1) TO (5000001);
CREATE TABLE documents_part_1 PARTITION OF documents_partitioned
FOR VALUES FROM (5000001) TO (10000001);
-- 每个分区单独建索引(并行进行,互不影响)
CREATE INDEX CONCURRENTLY idx_documents_part_0_hnsw
ON documents_part_0 USING hnsw(embedding vector_cosine_ops);
CREATE INDEX CONCURRENTLY idx_documents_part_1_hnsw
ON documents_part_1 USING hnsw(embedding vector_cosine_ops);
-- 方案 2:先用 IVFFlat 快速建索引,再逐步迁移到 HNSW
-- IVFFlat 构建速度约是 HNSW 的 10 倍
-- 第一阶段:快速上线(IVFFlat)
-- 规则:lists 值约等于 sqrt(总行数)
-- 500 万行 → lists ≈ 2236,通常取 2000
CREATE INDEX idx_documents_ivfflat
ON documents USING ivfflat(embedding vector_cosine_ops)
WITH (lists = 2000);
-- 业务跑着,同时后台构建 HNSW(可能需要几小时)
CREATE INDEX CONCURRENTLY idx_documents_hnsw_new
ON documents USING hnsw(embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- HNSW 建好后,删除 IVFFlat
DROP INDEX idx_documents_ivfflat;
六、查询参数调优:ef_search 与 probes
建好索引只是第一步。查询时的参数决定了召回率和速度之间的平衡。
6.1 HNSW 的 ef_search 参数
ef_search 控制 HNSW 查询时的搜索宽度。这是一个"召回率 vs 速度"的旋钮:
-- 查看当前设置
SHOW hnsw.ef_search;
-- 默认值:40
-- ef_search 的直觉理解:
-- 你让助手去找"最相似的 10 篇文章"
-- ef_search = 40 → 助手先找出 40 个候选,再从中选最好的 10 个
-- ef_search = 100 → 候选扩展到 100 个,结果更准,但慢
-- ef_search = 10 → 只看 10 个候选,很快但可能漏掉好的结果
-- 不同 ef_search 的影响(未验证的示例假设,1536 维,1000 万行;不构成性能或召回率承诺):
-- ef_search = 10: QPS ~2000, 召回率约 85%
-- ef_search = 40: QPS ~800, 召回率约 95%(默认)
-- ef_search = 100: QPS ~350, 召回率约 98%
-- ef_search = 200: QPS ~180, 召回率约 99%
-- 配置方式
-- 1. 全局配置(postgresql.conf)
-- hnsw.ef_search = 40
-- 2. 会话级配置(对当前连接有效)
SET hnsw.ef_search = 100;
-- 3. 事务级配置(对当前事务有效)
BEGIN;
SET LOCAL hnsw.ef_search = 200;
SELECT * FROM documents ORDER BY embedding <=> '[...]'::vector LIMIT 10;
COMMIT;
6.2 IVFFlat 的 probes 参数
IVFFlat 索引的查询参数是 probes,逻辑类似:
-- IVFFlat 的工作原理:
-- 1. 把所有向量分成 lists 个"桶"(cluster)
-- 2. 查询时,先找出最近的 probes 个桶
-- 3. 在这 probes 个桶里找最近的向量
SHOW ivfflat.probes;
-- 默认值:1(只看 1 个桶,速度最快但召回率最低)
-- probes / lists 是探测桶的比例,不等于召回率
-- 如果 lists = 100,可从 probes = 10 开始测试,但能否达到 90% 召回率需与精确搜索结果对照测量
-- 未验证的示例假设(lists = 100,1536 维;不构成性能或召回率承诺):
-- probes = 1: QPS ~5000, 召回率约 50%(基本不可用)
-- probes = 5: QPS ~2000, 召回率约 80%
-- probes = 10: QPS ~1000, 召回率约 90%
-- probes = 20: QPS ~550, 召回率约 95%
SET ivfflat.probes = 10;
6.3 如何测量召回率
-- 召回率测试:对比近似搜索和精确搜索的结果重叠程度
-- 精确搜索(顺序扫描)
SET enable_indexscan = off;
SET enable_bitmapscan = off;
SELECT id FROM documents
ORDER BY embedding <=> '[查询向量]'::vector
LIMIT 10;
-- 如需分析性能,另行执行 EXPLAIN ANALYZE;执行计划不会返回上述结果ID集合。
-- 记录精确结果的 ID 集合(假设为 {1, 3, 5, 7, 9, 11, 13, 15, 17, 19})
RESET enable_indexscan;
RESET enable_bitmapscan;
-- 近似搜索(使用 HNSW 索引)
SET hnsw.ef_search = 40;
SELECT id FROM documents
ORDER BY embedding <=> '[查询向量]'::vector
LIMIT 10;
-- 记录近似结果的 ID 集合(假设为 {1, 3, 5, 7, 9, 11, 13, 15, 21, 23})
-- 召回率 = 重叠数量 / K
-- 重叠:{1, 3, 5, 7, 9, 11, 13, 15} = 8 个
-- 召回率 = 8 / 10 = 80%
-- 批量召回率测试(生产前建议运行)
WITH test_queries AS (
-- 使用 100 个随机向量作为测试查询
SELECT embedding AS query_vec FROM documents TABLESAMPLE SYSTEM(0.01) LIMIT 100
),
exact_results AS (
-- 精确搜索(临时禁用索引)
SELECT
q.query_vec,
array_agg(d.id ORDER BY d.embedding <-> q.query_vec) AS exact_ids
FROM test_queries q
CROSS JOIN LATERAL (
SELECT id FROM documents
ORDER BY embedding <-> q.query_vec
LIMIT 10
) d
GROUP BY q.query_vec
)
SELECT
AVG(recall) AS avg_recall
FROM (
SELECT
array_length(
ARRAY(SELECT unnest(exact_ids) INTERSECT SELECT unnest(approx_ids)),
1
)::float / 10 AS recall
FROM exact_results
JOIN ... -- 近似搜索结果
) subq;
七、监控体系:PostgreSQL 原生视图的用法
pgvector 的监控可以完全复用 PostgreSQL 的原生监控工具,不需要任何额外安装。
7.1 实时查询监控:pg_stat_activity
-- 查看当前所有活跃的向量查询
SELECT
pid,
now() - pg_stat_activity.query_start AS duration,
query,
state,
wait_event_type,
wait_event,
client_addr
FROM pg_stat_activity
WHERE
state != 'idle'
AND query NOT ILIKE '%pg_stat_activity%' -- 排除监控查询本身
AND (
query ILIKE '%<=>%' -- 余弦距离查询
OR query ILIKE '%<->%' -- 欧氏距离查询
OR query ILIKE '%<#>%' -- 内积查询
)
ORDER BY duration DESC;
-- 终止长时间运行的向量查询(谨慎使用)
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE
now() - query_start > INTERVAL '30 seconds'
AND query ILIKE '%<=>%';
7.2 索引使用情况:pg_stat_user_indexes
-- 检查向量索引是否被实际使用
SELECT
schemaname,
relname AS tablename,
indexrelname AS indexname,
idx_scan AS index_scans, -- 索引被使用的次数
idx_tup_read AS tuples_read, -- 通过索引读取的 tuple 数
idx_tup_fetch AS tuples_fetched, -- 实际 fetch 的 tuple 数
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
WHERE relname IN ('documents', 'articles', 'restaurants')
ORDER BY idx_scan DESC;
-- 如果 idx_scan = 0,说明索引完全没被使用,需要调查原因
-- 常见原因:查询条件不满足、statistics 过时、enable_indexscan 被关闭
7.3 表层面的统计:pg_stat_user_tables
-- 查看向量表的读写情况
SELECT
relname AS table_name,
n_live_tup AS live_rows,
n_dead_tup AS dead_rows,
seq_scan AS seq_scans, -- 全表扫描次数
seq_tup_read AS seq_rows_read, -- 全表扫描读取的行数
idx_scan AS index_scans,
last_vacuum,
last_autovacuum,
last_analyze,
last_autoanalyze
FROM pg_stat_user_tables
WHERE relname IN ('documents', 'articles')
ORDER BY seq_scan DESC;
-- 警告:seq_scans 高说明查询经常没走索引
-- 结合 seq_tup_read / idx_scan 的比值来判断严重程度
7.4 I/O 统计:pg_statio_user_tables
-- 查看向量表的缓存命中率
SELECT
relname AS table_name,
heap_blks_hit AS cache_hits,
heap_blks_read AS disk_reads,
round(
heap_blks_hit::numeric /
NULLIF(heap_blks_hit + heap_blks_read, 0) * 100,
2
) AS cache_hit_ratio,
-- 索引的缓存命中率(向量索引特别重要)
idx_blks_hit AS index_cache_hits,
idx_blks_read AS index_disk_reads,
round(
idx_blks_hit::numeric /
NULLIF(idx_blks_hit + idx_blks_read, 0) * 100,
2
) AS index_cache_hit_ratio
FROM pg_statio_user_tables
WHERE relname IN ('documents', 'articles')
ORDER BY heap_blks_read DESC;
-- 向量索引的缓存命中率低于 80% 时,考虑:
-- 1. 增大 shared_buffers
-- 2. 使用 pgvectorscale 的 StreamingDiskANN(更适合磁盘场景)
7.5 慢查询日志配置
-- 在 postgresql.conf 中开启慢查询日志
-- log_min_duration_statement = 1000 # 记录超过 1 秒的查询(毫秒)
-- log_line_prefix = '%t [%p]: [%l-1] user=%u,db=%d,app=%a,client=%h '
-- 临时开启(会话级)
SET log_min_duration_statement = 500; -- 记录超过 500ms 的查询
-- pgBadger 分析慢查询日志(推荐)
-- pgbadger /var/log/postgresql/postgresql.log -o report.html
7.6 构建监控 Dashboard(SQL 版)
-- 向量系统健康状态一览
WITH vector_tables AS (
SELECT tablename
FROM pg_tables
WHERE schemaname = 'public'
AND tablename IN ('documents', 'articles', 'restaurants')
),
stats AS (
SELECT
t.relname AS table_name,
t.n_live_tup AS rows,
pg_size_pretty(pg_total_relation_size(t.relid)) AS total_size,
round(io.idx_blks_hit::numeric / NULLIF(io.idx_blks_hit + io.idx_blks_read, 0) * 100, 1) AS idx_cache_hit_pct,
t.seq_scan AS seq_scans_total,
t.idx_scan AS index_scans_total,
t.last_vacuum,
t.last_analyze
FROM pg_stat_user_tables t
JOIN pg_statio_user_tables io ON t.relid = io.relid
WHERE t.relname IN (SELECT tablename FROM vector_tables)
)
SELECT * FROM stats ORDER BY rows DESC;
八、常见性能问题排查手册
8.1 问题 1:查询没走索引(Seq Scan)
现象: 向量查询非常慢,EXPLAIN ANALYZE 显示 Seq Scan 而不是 Index Scan。
-- 诊断:检查执行计划
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT id, title
FROM documents
ORDER BY embedding <=> '[查询向量]'::vector
LIMIT 10;
-- 如果看到:
-- Seq Scan on documents (cost=... rows=... width=...)
-- → 没走索引,需要排查
原因和解决方案:
-- 原因 1:索引不存在或已失效
-- 检查
SELECT indexname, indexdef FROM pg_indexes WHERE tablename = 'documents';
-- 解决:重建索引
CREATE INDEX CONCURRENTLY idx_documents_hnsw ON documents
USING hnsw(embedding vector_cosine_ops);
-- 原因 2:statistics 过期,优化器估算错误
-- 检查:看 n_live_tup vs 实际行数差异
SELECT s.relname, s.n_live_tup, c.reltuples
FROM pg_stat_user_tables AS s
JOIN pg_class AS c ON c.oid = s.relid
WHERE s.relname = 'documents';
-- 解决:强制更新统计
VACUUM ANALYZE documents;
-- 原因 3:LIMIT 值太大,优化器认为走索引不划算
-- pgvector 向量索引只在 LIMIT 较小时才走索引
-- 超过某个阈值(通常是总行数的 5%),优化器会选 seq scan
-- 检查:实验不同 LIMIT 值
EXPLAIN SELECT * FROM documents ORDER BY embedding <=> '[...]'::vector LIMIT 1; -- 走索引?
EXPLAIN SELECT * FROM documents ORDER BY embedding <=> '[...]'::vector LIMIT 1000; -- 可能走 seq scan
-- 解决:如果确实需要大 LIMIT,强制走索引
SET enable_seqscan = off; -- 仅用于测试,不要在生产全局设置
SELECT * FROM documents ORDER BY embedding <=> '[...]'::vector LIMIT 1000;
RESET enable_seqscan;
-- 原因 4:operator class 不匹配
-- 索引用了 vector_cosine_ops,但查询用了 <-> 运算符(欧氏距离)
-- 解决:确保查询运算符和索引 ops 匹配
-- vector_l2_ops 对应 <->(欧氏距离)
-- vector_cosine_ops 对应 <=>(余弦距离)
-- vector_ip_ops 对应 <#>(内积)
-- 原因 5:查询带有 WHERE 子句,过滤后行数太少
-- 优化器认为过滤后的行少,seq scan 更快
-- 解决:见07-混合搜索与过滤优化的分区方案
8.2 问题 2:索引构建卡住或极慢
现象: CREATE INDEX 跑了好几个小时还没完成,或者 pg_stat_progress_create_index 没有进展。
-- 诊断:查看索引构建进度
SELECT
relid::regclass AS table_name,
index_relid::regclass AS index_name,
phase,
blocks_done,
blocks_total,
tuples_done,
tuples_total,
current_locker_pid -- 非空时,说明被锁阻塞
FROM pg_stat_progress_create_index;
-- 诊断:查看是否有锁等待
SELECT
pid,
query,
wait_event_type,
wait_event,
state
FROM pg_stat_activity
WHERE pid IN (
SELECT pid FROM pg_stat_progress_create_index
);
原因和解决方案:
-- 原因 1:maintenance_work_mem 太小,频繁溢盘
-- 症状:phase 停在 "loading tuples",blocks_done 进展很慢
-- 解决:
SET maintenance_work_mem = '4GB'; -- 在建索引的会话里设置
-- 然后重新建索引
-- 原因 2:被其他事务锁住(CONCURRENTLY 特有问题)
-- CONCURRENTLY 建索引需要等所有长事务结束
-- 检查:current_locker_pid 非空
-- 解决:等待长事务结束,或者终止阻塞事务
SELECT pg_terminate_backend(current_locker_pid)
FROM pg_stat_progress_create_index
WHERE current_locker_pid IS NOT NULL;
-- 原因 3:磁盘 I/O 成为瓶颈
-- 症状:iostat 显示磁盘使用率接近 100%
-- 解决:
-- a) 增大 maintenance_work_mem,减少 I/O 次数
-- b) 使用 SSD 替代 HDD
-- c) 调低并行 worker 数量,避免 I/O 竞争
-- 原因 4:向量维度与索引 ops 不匹配
-- 症状:ERROR: index requires vectors with 1536 dimensions
-- 解决:确保表里的向量维度统一
SELECT DISTINCT array_length(embedding::float[], 1) AS dim
FROM documents;
-- 如果有混合维度,先清理
8.3 问题 3:IVFFlat 索引召回率下降
现象: 使用 IVFFlat 索引,搜索结果与预期不符,明显遗漏了相关内容。
根本原因理解:
IVFFlat 把向量空间分成 lists 个"桶"
查询时只看最近的 probes 个桶
如果相关向量在不近不远的桶里,就会被遗漏
随着数据增长,桶的边界可能不再合理:
- 新增了很多来自特定领域的向量
- 导致某些桶极度拥挤,某些桶几乎为空
- 查询时探针指向错误的桶,召回率下降
-- 诊断:测量当前召回率(与精确搜索对比)
-- 先用精确搜索(禁用 IVFFlat 索引)
SET enable_indexscan = off;
SELECT id FROM documents
ORDER BY embedding <=> '[查询向量]'::vector
LIMIT 10;
-- 记录结果
RESET enable_indexscan;
-- 再用 IVFFlat(不同 probes 值)
SET ivfflat.probes = 10;
SELECT id FROM documents
ORDER BY embedding <=> '[查询向量]'::vector
LIMIT 10;
-- 比较重叠程度
-- 解决方案 1:增大 probes(临时方案)
SET ivfflat.probes = 20; -- 从 10 增大到 20,提升召回率
-- 解决方案 2:重建索引(根本方案)
-- 随着数据分布变化,重建索引可以重新分桶
DROP INDEX idx_documents_ivfflat;
VACUUM ANALYZE documents; -- 先更新统计
CREATE INDEX CONCURRENTLY idx_documents_ivfflat
ON documents USING ivfflat(embedding vector_cosine_ops)
WITH (lists = 200); -- 如果数据量增长了,增大 lists
-- 解决方案 3:迁移到 HNSW(长期方案)
-- HNSW 没有 IVFFlat 的"分桶"问题,召回率更稳定
CREATE INDEX CONCURRENTLY idx_documents_hnsw
ON documents USING hnsw(embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
DROP INDEX idx_documents_ivfflat;
8.4 问题 4:向量查询内存使用过高
现象: 并发向量查询导致 OOM,或者 pg_stat_activity 显示大量查询在等待内存。
-- 诊断:查看当前内存使用情况
SELECT
pid,
usename,
substring(query, 1, 80) AS query_preview,
state,
(now() - query_start) AS duration
FROM pg_stat_activity
WHERE state = 'active'
AND query ILIKE '%<=>%'
ORDER BY duration DESC;
-- 查看当前 work_mem 设置
SHOW work_mem;
-- 解决方案 1:降低 work_mem(如果太高)
-- work_mem = 64MB 通常足够 HNSW 查询
ALTER SYSTEM SET work_mem = '64MB';
SELECT pg_reload_conf();
-- 解决方案 2:限制并发向量查询数量
-- 通过连接池(pgBouncer)限制最大连接数
-- 或者通过应用层限制并发请求数
-- 解决方案 3:使用 halfvec 减少内存占用
-- 将 vector(1536) 改为 halfvec(1536),每个向量内存减半
ALTER TABLE documents ADD COLUMN embedding_half HALFVEC(1536);
UPDATE documents SET embedding_half = embedding::halfvec;
ALTER TABLE documents DROP COLUMN embedding;
ALTER TABLE documents RENAME COLUMN embedding_half TO embedding;
九、扩展性上限与突破方案
9.1 pgvector 的单机上限
单机 pgvector 的性能边界:
可靠运行的向量数量:
单机可以稳定运行 1000 万 ~ 5000 万个向量
具体上限取决于:维度大小、索引参数、硬件配置
查询性能参考(m=16, ef_search=40, 1536维, float32):
100 万向量:QPS ~2000, p99 延迟 ~5ms
1000 万向量:QPS ~800, p99 延迟 ~15ms
5000 万向量:QPS ~300, p99 延迟 ~50ms
当你遇到这些信号时,说明接近上限:
- QPS 无法满足业务需求
- p99 延迟超过业务容忍值
- HNSW 索引无法完整放入 shared_buffers
- 单次全表 VACUUM 需要几个小时
9.2 垂直扩展(Scale Up):首选方案
在尝试水平扩展之前,先穷尽垂直扩展的可能:
垂直扩展清单:
1. 增大 RAM(让 HNSW 索引完整放入内存)
1000 万 × 1536维 的 HNSW 索引约 100GB,需要 ≥ 128GB RAM
2. 使用 NVMe SSD(如果内存放不下索引,I/O 速度决定性能)
NVMe vs HDD:向量查询速度差 10~50 倍
3. 增加 CPU 核数(并行查询加速)
16 核 vs 4 核:并行效果约 3 倍提升
4. 使用 halfvec 减半内存占用
同样的硬件,支持 2 倍的向量数量
5. 使用 pgvectorscale 的 StreamingDiskANN
专为磁盘场景设计,内存不足时性能远好于 HNSW
9.3 水平扩展方案一:读写分离
架构:
主节点(写)→ 流式复制 → 从节点 1(读)
→ 从节点 2(读)
→ 从节点 3(读)
适用场景:
- 读多写少(向量数据通常是这种模式)
- 数据量在单机能处理范围内
- 需要高并发读 QPS
配置要点:
- 物理流式复制下,在主节点创建 HNSW 索引,索引变更随 WAL 复制到只读从节点
- 应用层实现读写路由(写→主,读→从)
- 注意复制延迟(向量列的数据量大,延迟可能比普通表更明显)
这是成本最低、风险最小的扩展方案,建议优先采用。
9.4 水平扩展方案二:Citus 分片(谨慎使用)
Citus 是 PostgreSQL 的分布式扩展,可以把表分片到多个节点。但 pgvector + Citus 有一些重要限制:
-- Citus 安装(简化示例)
CREATE EXTENSION citus;
-- 将向量表分布到多个节点
-- 分片键选择很重要:影响查询路由策略
SELECT create_distributed_table(
'documents',
'tenant_id', -- 按租户 ID 分片(如果有多租户需求)
shard_count => 32
);
-- 在每个 shard 上单独建索引
-- Citus 会自动在每个分片上执行
CREATE INDEX CONCURRENTLY idx_documents_hnsw
ON documents USING hnsw(embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
Citus 方案的核心限制:
问题 1:召回率下降
向量空间被切分到多个 shard
每个 shard 只有整体向量空间的 1/N
搜索时需要在每个 shard 里各取 top-K,再合并
在相同查询向量、距离度量和统一排序规则下,各 shard 的精确 top-K 合并后可得到全局精确 top-K;若局部使用近似搜索,仍需测量其遗漏对全局召回率的影响
问题 2:必须查询所有 shard
向量搜索没有有意义的"路由"方式(不像按 user_id 分片的普通查询)
每次查询都要广播到所有 shard,再汇总
shard 越多,协调开销越大
问题 3:运维复杂度大幅上升
Citus 集群的运维比单机 PostgreSQL 复杂很多
升级、备份、故障恢复都更复杂
结论:Citus 对 pgvector 的扩展效果不如预期。
适合场景:多租户架构(按 tenant_id 分片,每个租户内数据量有限)
不适合场景:单一大向量空间的横向扩展
9.5 水平扩展方案三:应用层分区(推荐)
核心思路:
在应用层把数据按业务维度分区,每个分区用独立的 PostgreSQL 数据库(或 Schema)
应用层路由:先确定目标分区,再在该分区内做向量搜索
示例:内容平台按内容类型分区
database_news:存新闻类文章的向量
database_tech:存技术文章的向量
database_products:存商品描述的向量
优点:
- 每个数据库仍然是标准 PostgreSQL,运维简单
- 向量空间不被打散,召回率不下降
- 可以按业务流量灵活分配资源
缺点:
- 跨分区查询需要应用层合并结果
- 不适合需要全局搜索的场景
# 应用层分区路由示例
PARTITION_ENGINES = {
"news": create_engine("postgresql://user:pass@host1/db_news"),
"tech": create_engine("postgresql://user:pass@host2/db_tech"),
"products": create_engine("postgresql://user:pass@host3/db_products"),
}
def search_with_partition(query_text: str, content_type: str, top_k: int = 10):
"""按内容类型路由到对应分区搜索"""
engine = PARTITION_ENGINES.get(content_type)
if not engine:
raise ValueError(f"未知的内容类型: {content_type}")
query_embedding = get_embedding(query_text)
with engine.connect() as conn:
result = conn.execute(
text("""
SELECT id, title, 1 - (embedding <=> :vec::vector) AS similarity
FROM documents
ORDER BY embedding <=> :vec::vector
LIMIT :k
"""),
{"vec": str(query_embedding), "k": top_k}
)
return [dict(row) for row in result]
def cross_partition_search(query_text: str, top_k: int = 10):
"""跨分区搜索:在所有分区搜索,汇总结果"""
import concurrent.futures
query_embedding = get_embedding(query_text)
results_per_partition = {}
# 并行查询所有分区
with concurrent.futures.ThreadPoolExecutor(max_workers=len(PARTITION_ENGINES)) as executor:
future_to_partition = {
executor.submit(search_with_partition, query_text, pt, top_k): pt
for pt in PARTITION_ENGINES
}
for future in concurrent.futures.as_completed(future_to_partition):
pt = future_to_partition[future]
results_per_partition[pt] = future.result()
# 合并并按相似度重排
all_results = []
for pt, results in results_per_partition.items():
for r in results:
r["partition"] = pt
all_results.append(r)
all_results.sort(key=lambda x: x["similarity"], reverse=True)
return all_results[:top_k]
9.6 何时考虑迁移到专用向量数据库
迁移信号(满足 3 个以上,认真考虑迁移):
性能指标:
□ 单机垂直扩展已到极限(CPU/内存/磁盘已最大化)
□ 向量数量持续超过 1 亿
□ QPS 需求超过 5000,且垂直扩展无法满足
□ p99 延迟要求 < 5ms,当前 pgvector 无法达到
业务特征:
□ 纯向量搜索,很少需要和业务数据 JOIN
□ 不需要事务保证(向量更新不需要与业务数据原子操作)
□ 团队有能力维护额外的基础设施
迁移目标推荐:
高 QPS + 低延迟:Qdrant(Rust 实现,性能极致)
超大规模 + 分布式:[[Milvus/01-Milvus概述与核心定位|Milvus]](专为分布式设计)
云原生 + 免运维:Pinecone(托管服务)
十、生产配置速查表
10.1 postgresql.conf 关键参数
# ========================================
# pgvector 生产环境配置参考
# 硬件假设:32 核 CPU,128 GB RAM,NVMe SSD
# ========================================
# --- 内存配置 ---
shared_buffers = 32GB # 物理内存的 25%
effective_cache_size = 96GB # 物理内存的 75%
work_mem = 64MB # 向量查询的工作内存
maintenance_work_mem = 2GB # 索引构建内存(可临时调大)
# --- 并行配置 ---
max_parallel_workers = 16 # 全局并行 Worker(不超过 CPU 核数的 50%)
max_parallel_workers_per_gather = 4 # 每个查询最多 4 个并行 Worker
max_parallel_maintenance_workers = 4 # 索引构建并行
# --- 连接配置 ---
max_connections = 200 # 配合 pgBouncer 使用时可以小一点
# 如果用 pgBouncer,PostgreSQL 侧 max_connections = pgBouncer pool_size × 1.5
# --- WAL 配置(对写入密集的向量应用有影响)---
wal_level = replica # 支持物理流式复制;逻辑复制需设为 logical
max_wal_size = 8GB # 防止 WAL 日志过多影响磁盘
checkpoint_completion_target = 0.9 # 平滑 checkpoint,避免 I/O 峰值
# --- 日志配置 ---
log_min_duration_statement = 1000 # 记录超过 1 秒的查询
log_checkpoints = on # 记录 checkpoint 信息
log_lock_waits = on # 记录锁等待
# --- pgvector 专属参数 ---
# 这些参数通常在应用层 SET,不在 postgresql.conf 全局设置
# hnsw.ef_search = 40 # 默认值,通常在查询时动态调整
# ivfflat.probes = 1 # 默认值,通常在查询时动态调整
10.2 索引参数速查
| 场景 | 索引类型 | m | ef_construction | ef_search | 示例召回率假设(非保证,需测量) |
|---|---|---|---|---|---|
| 开发测试 | HNSW | 8 | 32 | 20 | ~88% |
| 生产默认 | HNSW | 16 | 64 | 40 | ~95% |
| 高精度 | HNSW | 32 | 128 | 100 | ~98% |
| 超大数据 + 低内存 | IVFFlat | N/A | N/A | probes=10 | ~90% |
10.3 容量规划速查
| 向量数量 | 1536 维 float32 存储 | HNSW 索引(m=16) | 推荐 RAM | 推荐方案 |
|---|---|---|---|---|
| 100 万 | 6 GB | 11 GB | 32 GB | 单机 pgvector |
| 500 万 | 29 GB | 52 GB | 96 GB | 单机 pgvector |
| 1000 万 | 57 GB | 103 GB | 192 GB | 单机 pgvector 或 halfvec |
| 3000 万 | 172 GB | 310 GB | 512 GB | pgvectorscale 或读写分离 |
| 1 亿+ | 572 GB | 1 TB+ | — | 评估迁移 Milvus |
10.4 故障排查速查
| 现象 | 第一步检查 | 最可能原因 | 快速解决 |
|---|---|---|---|
| 查询很慢 | EXPLAIN ANALYZE |
没走索引(Seq Scan) | VACUUM ANALYZE 后重试 |
| 索引构建极慢 | pg_stat_progress_create_index |
maintenance_work_mem 太小 |
SET maintenance_work_mem = '2GB' |
| 索引构建卡住 | current_locker_pid |
被长事务锁住 | 等待或终止阻塞事务 |
| 召回率下降 | 对比精确搜索结果 | IVFFlat 需要重建 | 增大 probes 或重建索引 |
| 内存 OOM | pg_stat_activity |
并发查询 × work_mem 过大 | 降低 work_mem 或限制并发 |
| 连接耗尽 | pg_stat_activity 中 idle 连接 |
没有连接池 | 引入 pgBouncer |
小结
pgvector 生产运维的核心经验可以浓缩为几个关键认知:
内存是第一优先级: HNSW 索引能放多少在内存里,QPS 就能有多高。shared_buffers 决定缓存大小,maintenance_work_mem 决定索引构建速度,work_mem 决定查询过程中的工作内存。这三个参数是调优起点,但不能保证解决固定比例的性能问题;还需结合执行计划、过滤条件、索引参数、I/O 和并发测量。
索引构建要有工程思路: 大表建索引不是一次命令的事,要考虑 CONCURRENTLY(允许常规读写继续,但仍会获取锁并等待相关事务)、分批构建(分区表)、临时调高内存(maintenance_work_mem)、监控进度(pg_stat_progress_create_index)。
监控用原生工具就够: pg_stat_activity、pg_stat_indexes、pg_statio_user_tables——这些你已经熟悉的视图,对向量查询同样好用。
扩展要循序渐进: 垂直扩展优先(加 RAM、换 SSD),然后是读写分离,最后才考虑 Citus 或迁移到专用向量库。Citus 对 pgvector 的加速效果不如直觉预期,要慎用。
召回率要主动测量: 不要假设"走索引就等于结果准确"。定期和精确搜索对比,确认召回率在业务可接受范围内。IVFFlat 的召回率会随数据分布变化而下降,需要定期重建索引。
summary 生产优化核查清单
- [ ]
shared_buffers设为物理内存的 25% - [ ]
work_mem设为 32MB ~ 64MB(根据并发数调整) - [ ] 大表建索引时临时设
maintenance_work_mem = '2GB' - [ ]
max_parallel_workers_per_gather设为 4(16 核机器) - [ ] 使用
CREATE INDEX CONCURRENTLY减少对常规读写的阻塞,并评估锁等待与长事务影响 - [ ] 定期检查
pg_stat_indexes中idx_scan确认索引被使用 - [ ] 定期运行召回率测试,确保 HNSW 召回率 > 90%
- [ ] IVFFlat 索引每季度评估是否需要重建
- [ ] 配置
log_min_duration_statement = 1000捕获慢查询 - [ ] 向量表定期
VACUUM ANALYZE(防止统计信息过期)