未记录事实核验 · 状态:维护中

记录仅说明核验时间与维护状态,不保证适用于所有环境;使用前请留意正文中的版本、适用条件与风险提示。

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 生态集成


目录


一、运维哲学:一个你已熟悉的老朋友

对于已有 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 控制 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(防止统计信息过期)