主题
PgSQL 与 AI:向量不离开业务库
一句话:把向量当成普通列,让「元数据过滤 + 向量召回 + 业务写入」在同一个库、同一个事务里完成;代价是单机内存和索引构建。
适用 PostgreSQL 16/17、pgvector 0.8.x。pgvector 0.8 起支持迭代索引扫描;更早版本没有 hnsw.iterative_scan。
为什么需要
在 RAG 链路里,PostgreSQL 承担的是数据层,不是编排层:
- 存切片内容、文档元数据、租户、权限、状态。
- 存 embedding,并提供按距离的 topK 召回。
- 存
tsvector,提供关键词召回,和向量召回做融合。 - 用事务保证「切片写入 + 文档状态更新 + 计数」一致。
它不负责调用 LLM、不负责对话状态、不负责原始文件存储。会话状态放 Redis,原始文件放对象存储,事件放消息队列。这样分工的目的是:让需要事务一致的那部分状态留在一个库里,而不是把整个 AI 栈塞进一个数据库。
AI 带来的不是「多了一个 vector 类型」,而是几种访问模式同时出现:
- 写入模式:文档入库时批量切片、批量 embedding、批量 INSERT;一次可能几千到几万行,事务比典型 OLTP 长。
- 读取模式:topK 召回,带租户/权限/状态过滤;延迟敏感,p99 比平均更重要。
- 混合检索:向量召回 + 关键词召回,再按排名融合;两路召回的候选集和排序逻辑不同。
- 重建负载:换 embedding 模型后,整表回填新向量;期间旧查询不能停。
- 隔离要求:多租户共享表时,每条查询都必须带
tenant_id;漏一条就是越权。
这些负载和「按主键增删改查」不同:它们更依赖索引内存、ANN 参数、过滤选择和批量写入的 WAL 量。
核心机制
能力与参数
| 能力 | 版本/默认值 | 说明 |
|---|---|---|
vector(n) | pgvector 0.8.x | float32,4 字节/维 |
halfvec(n) | pgvector 0.7+ | float16,内存约减半 |
bit(n) / sparsevec(n) | pgvector 0.7+ | 二值量化、稀疏向量 |
| HNSW | m=16、ef_construction=64、ef_search=40 | 默认值;ef_construction=200 是常见调优值 |
| IVFFlat | lists=100、probes=1 | 需要表里先有数据才能训练 |
| 迭代索引扫描 | pgvector 0.8+ | hnsw.iterative_scan=relaxed_order,缓解过滤后召回不足 |
| JSONB + GIN | PostgreSQL 内置 | 存元数据、标签、权限快照 |
tsvector + GIN | PostgreSQL 内置 | 关键词召回;中文需 zhparser/pg_jieba |
pgai / pgvectorscale / pg_cron / pg_partman | 第三方扩展 | 按需引入,版本与兼容性以各自文档为准 |
参数口径要和 06 篇一致:ef_construction 的 64 是 pgvector 默认值,200 是常用调优值;ef_search 的 40 是默认值,100 是常见调优值。不要把「默认」和「调优」混写。
索引与召回路径
HNSW 是分层图,查询快、召回高,但索引常驻内存、构建慢;IVFFlat 先聚类再倒排,构建快、省内存,但必须先有数据训练。两者都只加速 ORDER BY embedding <距离运算符> ... LIMIT K 这类查询,距离运算符必须和索引 opclass 对上。
真正的难点是过滤:先 ANN 取 topK 再按 WHERE 过滤,条件一严可能一条不剩。pgvector 0.8 的迭代扫描会继续扫索引直到凑够 K 条,代价是延迟上升;另一种做法是给高选择性过滤列建 B-tree,让优化器先过滤再算距离,实际走哪条路径要用 EXPLAIN 确认。
混合检索不要直接把距离和 ts_rank 相加,量纲不同;用 RRF 按排名融合,见 09 篇的可执行 SQL。向量索引的更新不属于业务事务:事务提交后新数据还要等索引追上才进入检索路径。
最小上手
sql
-- PostgreSQL 16/17 + pgvector 0.8.x
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE doc (
id bigint PRIMARY KEY,
chunk_count int NOT NULL DEFAULT 0
);
CREATE TABLE doc_chunk (
id bigserial PRIMARY KEY,
tenant_id bigint NOT NULL,
doc_id bigint NOT NULL REFERENCES doc(id),
status text NOT NULL,
content text NOT NULL,
embedding vector(3) NOT NULL -- 演示用 3 维,生产按模型改
);
CREATE INDEX idx_chunk_embedding ON doc_chunk
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
INSERT INTO doc (id, chunk_count) VALUES (100, 0);
BEGIN;
INSERT INTO doc_chunk (tenant_id, doc_id, status, content, embedding)
VALUES (1, 100, 'published', '退款政策', '[0.1,0.2,0.3]'::vector);
UPDATE doc SET chunk_count = chunk_count + 1 WHERE id = 100;
COMMIT;
SET hnsw.ef_search = 100; -- 默认 40;>= LIMIT
SELECT id, content, embedding <=> '[0.1,0.2,0.3]'::vector AS distance
FROM doc_chunk
WHERE tenant_id = 1 AND status = 'published'
ORDER BY embedding <=> '[0.1,0.2,0.3]'::vector
LIMIT 5;
EXPLAIN (ANALYZE, BUFFERS)
SELECT id FROM doc_chunk
WHERE tenant_id = 1 AND status = 'published'
ORDER BY embedding <=> '[0.1,0.2,0.3]'::vector
LIMIT 5;期望看到 Index Scan using idx_chunk_embedding。如果过滤条件选择性很高,优化器也可能先走 tenant_id/status 的 B-tree 再排序;用 EXPLAIN 确认实际路径,不要假设一定走 HNSW。
生产动作
- 建索引:HNSW 构建慢且吃
maintenance_work_mem(默认 64MB);在线表用CREATE INDEX CONCURRENTLY,先在测试环境估算时间和内存。 - 过滤与召回:给高选择性过滤列建 B-tree;过滤后召回不足时再开迭代扫描,并用 recall@K 验证,不要只看单条查询。
- 写入一致性:向量写入、业务状态更新、计数放在同一事务;批量写入控制单批行数,避免长事务拖住 Vacuum 和 DDL。
- 备份恢复:向量列随表备份;恢复后校验索引和模型版本。换模型时加新列/新表,不要原地覆盖。
- 扩展升级:pgvector、zhparser、pg_partman 都要跟 PostgreSQL 大版本升级;升级前验证索引兼容性。
- 容量与监控:1536 维 float32 约 6KB/条;盯索引大小、内存余量、p99 和 recall@K,内存不足时 HNSW 会断崖式变慢。
- 多租户:每条查询强制带
tenant_id;用行级安全或统一数据访问层兜底,不能只靠调用方自觉。
边界
| 规模/约束 | 选择 | 理由 |
|---|---|---|
| 百万级以内、已有 PostgreSQL、要事务/JOIN | pgvector | 一条 SQL 完成过滤 + 召回,运维最省事 |
| 百万到千万、单机内存足够、过滤复杂 | pgvector + 分区/只读副本 | 先压测 p99 和 recall@K,确认内存余量 |
| 千万以上到亿级、高 QPS、多租户隔离 | 专用向量库(Milvus 等) | 分布式、弹性扩缩、专用索引 |
| 需要跨地域、GPU 索引、复杂多路召回 | 专用向量库 + 业务库 | 接受双写和回表,做一致性补偿 |
这些是社区常见经验分界,不是硬指标;同样 1000 万条,1536 维和 256 维的内存差好几倍。判断标准可以反过来问:删掉「AI」两个字,这套架构是否仍然成立?如果只是普通业务表加一个向量列,pgvector 的价值主要在少一套系统;如果负载的核心是亿级 ANN、弹性扩缩和多租户隔离,专用向量库才是主线。
讲清楚
AI 应用的数据层不是「把向量塞进数据库」这么简单。切片、向量、租户、权限、状态天然要一起写、一起查,分散到向量库和业务库就要处理双写、回表和一致性补偿;放在 PostgreSQL 里,这些字段在同一张表,写入是一个事务,召回是一条带过滤的 SQL。代价也很明确:HNSW 索引吃内存、构建慢,单机扩展性有限,过滤条件一严还会把 topK 筛空。所以选型看规模:百万到千万级、过滤复杂、要求事务一致,pgvector 最省事;上亿条、高 QPS、要弹性扩缩,交给 Milvus 这类专用库,并用 recall@K、p99 和真实压测而不是感觉做决定。