创建日期:2026-09-23 | 最近更新:2026-09-23 实测环境:PostgreSQL 17.11 + pgvector 0.8.6(容器
pgvector/pgvector:pg17;脚本source/pgvector-lab/run.sh,输出run.log)。 规范条款为 pgvector 官方 README 逐字原文(抓取日期 2026-09-23,留档official-sources.md)。 本文是上手与调优篇;"该不该选向量库、选哪家"的横向比较见 向量数据库全景。
pgvector:为什么选它,怎么用
一句话:pgvector 是 PostgreSQL 的一个扩展——它给 Postgres 加了一个
vector类型、几个距离算子和两种索引。 于是"存向量、按相似度检索"就变成了普通的 SQL,你不需要为此多运维一套数据库。
这就是它最大的卖点,也是最容易被说反的地方:选 pgvector 通常不是因为它快,而是因为它"不用多一个组件"。
1. 为什么选它:四条理由,都能落到 SQL 上
1.1 它不是一个新数据库,而是一个 CREATE EXTENSION
CREATE EXTENSION vector;
一句话装完。之后 Postgres 里就多了 vector 类型。没有新进程、没有新端口、没有新的备份策略、没有新的权限体系——
你现有的连接池、监控、迁移工具、ORM、备份恢复流程全都照旧。
这不是"省事一点",而是量级差别:独立向量库意味着多一套要部署、要升级、要监控、要备份、要排障的系统。 本站 向量数据库全景 里统计过:主流 RAG 平台里 pgvector 系与独立向量库是 6 : 2—— 原因就一句:它们本来就要一个关系库存元数据、存用户、存会话,pgvector 让它们不用再运维第二个组件。
1.2 向量和业务数据在同一个事务里(这是别人给不了的)
独立向量库最难受的地方:你的业务数据在 Postgres,向量在别处。两边没法原子提交—— 业务行插进去了、向量写失败,就留下一个孤儿;删了业务行忘了删向量,就留下一个永远检索得到的幽灵。
pgvector 里这就是一个普通事务:
BEGIN;
INSERT INTO biz VALUES (1, '业务行');
INSERT INTO items(category, content, embedding) VALUES ('tx', '事务里的向量', '[...]');
COMMIT; -- 要么都成,要么都不成
【本人实测】事务中两边都可见(业务行 1 / 向量行 1);
ROLLBACK之后两边一起消失(遗留向量行 0)。 向量不会成为孤儿——这条用独立向量库要么做不到,要么得自己写对账补偿。
1.3 能用 SQL 的全部表达力(过滤、JOIN、聚合、全文检索一起上)
向量检索经常不是孤立动作,而是"在满足一堆业务条件的数据里找最相似的":
SELECT i.id, i.title
FROM items i
JOIN tenants t ON t.id = i.tenant_id -- 业务 JOIN
WHERE i.status = 'published' -- 业务过滤
AND i.created_at > now() - interval '30 days'
AND i.content_tsv @@ plainto_tsquery('jiebacfg', '检索词') -- 全文检索
ORDER BY i.embedding <=> $1 -- 向量排序
LIMIT 10;
在独立向量库里,这些条件要么塞进它自己的过滤 DSL(能力受限),要么只能"先向量召回、再回关系库过滤"(多一次往返,且召回数要放大)。
1.4 混合检索不用拼两个系统
稠密向量 + 全文关键词 + RRF 融合,一条 SQL 就能算完(下文 §6 有实测)。
代价呢? 它就是个 Postgres 类型,所以受宿主引擎的一切约束:索引构建吃内存、更新向量要走 MVCC、水平扩展得靠分区/Citus、没有 GPU 索引。这些在 §2 和 §7 展开。
2. 什么时候别选它
| 情况 | 更该考虑 |
|---|---|
| 向量规模到亿级、且要求高 QPS 低延迟 | 专用向量库(见 Milvus 企业级方案) |
| 需要多副本水平扩展、独立伸缩检索层 | 专用向量库 / 分布式方案 |
| 索引构建时间/内存是硬约束,或需要 GPU 建索引 | 专用向量库(或 pgvectorscale 这类扩展) |
| 检索只是副业,主库已经被事务压满 | 把向量单独放一套(别拖累主库) |
| 过滤条件很窄(命中不到 10%)且要求高召回 | 见 §5——这是 pgvector 明确的后过滤短板,需要额外设计 |
一句话判据:"你已经有一个 Postgres,且向量规模在千万级以内、检索不是唯一负载" → pgvector 几乎总是最省事的选择; 反过来如果检索是核心业务且规模很大,专用库的复杂度是值得付的。
3. 上手:类型 + 三种距离算子
CREATE EXTENSION vector;
CREATE TABLE items (
id bigserial PRIMARY KEY,
category text NOT NULL,
content text NOT NULL,
embedding vector(768) NOT NULL -- 维度写在类型里,写错维度会直接报错
);
三个算子(这是 pgvector 最容易用错的地方的开始):
| 算子 | 含义 | 索引 opclass |
|---|---|---|
<-> | L2 欧氏距离 | vector_l2_ops |
<#> | 负内积(越小越相似) | vector_ip_ops |
<=> | 余弦距离 | vector_cosine_ops |
【本人实测】同一对向量的三种距离:L2 22.87 / 内积 2.67 / 余弦 1.0103。 注意内积返回的是负数(pgvector 用"负内积"以便统一成"越小越近"),第一次用会以为算错了。
默认是精确检索——官方原文:
【官方原文】"By default, pgvector performs exact nearest neighbor search, which provides perfect recall." "You can add an index to use approximate nearest neighbor search, which trades some recall for speed. Unlike typical indexes, you will see different results for queries after adding an approximate index."
这句很重要:加索引不是"加速",而是"换成另一种回答"。加了索引之后结果和之前不一样是正常的,不是 bug。
4. 索引:HNSW 还是 IVFFlat
-- HNSW:查询快、构建慢、占内存
CREATE INDEX ON items USING hnsw (embedding vector_l2_ops) WITH (m = 16, ef_construction = 64);
-- IVFFlat:构建快、占内存少、但召回依赖 probes
CREATE INDEX ON items USING ivfflat (embedding vector_l2_ops) WITH (lists = 100);
**【本人实测,20k 行 × 768 维】**同一查询:顺序扫描 165.2 ms → HNSW 索引扫描 3.64 ms。
建索引的真实代价(这次实测很有代表性):
NOTICE: hnsw graph no longer fits into maintenance_work_mem after 17702 tuples
DETAIL: Building will take significantly more time.
HINT: Increase maintenance_work_mem to speed up builds.
**【本人实测】**HNSW 建了 ~32 秒,而且 Postgres 明确提示图中的一部分已经溢出到磁盘。 处置:建索引前把
maintenance_work_mem调大(这是"建索引慢"最常见的唯一原因)。 想盯进度用官方给的这句:SELECT phase, round(100.0 * blocks_done / nullif(blocks_total, 0), 1) AS "%" FROM pg_stat_progress_create_index;
参数怎么定(官方原文 + 默认值):
| 索引 | 参数 | 默认 | 调大的效果 |
|---|---|---|---|
| HNSW | m | 16 | 每个节点的连接数,越大召回越好、索引越大 |
| HNSW | ef_construction | 64 | "更高的值带来更好的召回,代价是构建时间/插入速度"(官方原文) |
| HNSW | hnsw.ef_search(查询期) | 40 | 候选队列大小,越大召回越好越慢 |
| IVFFlat | lists | — | 官方建议:100 万行以内用 rows / 1000,超过用 sqrt(rows) |
| IVFFlat | ivfflat.probes(查询期) | 1 | 官方建议起点 sqrt(lists);越大召回越好越慢 |
IVFFlat 的三条官方实操建议(逐字,直接可抄):
【官方原文】"1. Create the index after the table has some data 2. Choose an appropriate number of lists - a good place to start is
rows / 1000for up to 1M rows andsqrt(rows)for over 1M rows 3. When querying, specify an appropriate number of probes (higher is better for recall, lower is better for speed) - a good place to start issqrt(lists)"
空表建 IVFFlat 会怎样——pgvector 自己会告警:
【我实测捕获的 pgvector 告警】"
ivfflat index created with little data/This will cause low recall./Drop the index until the table has more data."
【本人实测】
probes与召回的关系(20k 行、lists=100、probes 逐条真设): 1→2/10、5→2/10、10→4/10、50→9/10、100→10/10。 这就是它典型的"召回换延迟"曲线——probes必须调,默认的 1 召回很差。
⚠️ 一条失败的对比,如实写出来:我本想验证"空表建索引召回更低",但实测 probes=10 时是 7/10(空表)vs 4/10(有数据后建), 方向相反。原因是随机向量没有聚类结构、且单条查询信噪比太低。所以这条只能作为"官方会告警"的佐证,不能当结论。
5. ★ 两个最容易踩的坑
5.1 算子必须和索引的 opclass 匹配,否则索引白建
这是 pgvector 实践中最常见、也最隐蔽的性能事故:你建了索引、查询也走了索引语法,但规划器根本不用它。
**【本人实测】**索引建的是
vector_l2_ops,查询却用余弦<=>:Sort (…)-> Seq Scan on items (actual rows=20000 loops=1) ← 走的还是顺序扫描Execution Time: 233.4 ms补一个
vector_cosine_ops索引后,同一查询:Index Scan using items_hnsw_cos on items ← 用上了Execution Time: 3.73 ms
63 倍差距,代码一行没改。 官方也提醒:"Add an index for each distance function you want to use."
自查动作:拿你的真实查询跑一次 EXPLAIN ANALYZE,确认看到 Index Scan using <你的向量索引>,而不是 Seq Scan + Sort。
5.2 ★★ 后过滤:过滤条件一窄,LIMIT 10 会静默拿不满
这是 pgvector 最需要提前设计的一条。官方原文说得很直白:
【官方原文】"With approximate indexes, filtering is applied after the index is scanned. If a condition matches 10% of rows, with HNSW and the default
hnsw.ef_searchof 40, only 4 rows will match on average. For more rows, enable iterative index scans, which will automatically scan more of the index when needed."
原因:索引只负责按距离吐候选(默认 40 个),过滤是在候选出来之后才做的。 条件越窄,40 个候选里能通过的就越少,于是"要 10 条"最后只剩几条——而且不报错。
【本人实测,同一个
LIMIT 10,受控对照】
场景 hnsw.iterative_scan=off(默认)=relaxed_order无过滤 10 — 过滤命中 10% 5 10 过滤命中 0.1% 0 ← 一条都没返回,且无任何报错 10 注:小表上规划器可能直接选顺序扫描(此时结果是精确的,只是慢);上面用
SET enable_seqscan = off强制走索引路径,才暴露出真实的后过滤行为。
三条处置(按推荐顺序):
-- ① 开迭代索引扫描(pgvector 0.8+):按需继续扫描索引,直到够数
SET hnsw.iterative_scan = relaxed_order; -- 或 strict_order(严格按距离序)
-- 单条查询内生效:SET LOCAL hnsw.iterative_scan = relaxed_order;
-- ② 调大候选队列(简单粗暴,代价是延迟)
SET hnsw.ef_search = 200;
-- ③ 过滤值种类少时,官方建议用【部分索引】
CREATE INDEX ON items USING hnsw (embedding vector_l2_ops) WHERE tenant_id = 42;
【本人实测】开
iterative_scan = relaxed_order后,10% 和 0.1% 两种窄过滤都恢复成 10 条。 另外记一个实操细节:hnsw.iterative_scan/hnsw.max_scan_tuples这两个 GUC 是懒注册的—— 在会话里还没用过 hnsw 索引之前,SHOW hnsw.iterative_scan会报unrecognized configuration parameter。 先跑一条向量查询再去SHOW/SET就正常了。 (2026-09-24 补正)准确的说法是:注册之前SET一个合法值,库加载时会被继承生效; 只有越界值和拼错的名字才会在库加载时被丢弃(各打一条 WARNING)。受控实测见实战篇 §8。
6. 混合检索:全文 + 向量 + RRF,一条 SQL
稠密向量擅长语义、全文检索擅长精确关键词;两者融合常用 RRF(倒数排名融合,1/(k+rank),k 通常取 60)。
在 pgvector 里不需要任何外部编排:
ALTER TABLE items ADD COLUMN fts tsvector
GENERATED ALWAYS AS (to_tsvector('simple', content)) STORED; -- 中文建议换 zhparser / pg_jieba
CREATE INDEX items_fts ON items USING gin (fts);
WITH v AS (SELECT id, row_number() over () AS rk FROM (
SELECT id FROM items WHERE category='common'
ORDER BY embedding <-> $1 LIMIT 20) s),
t AS (SELECT id, row_number() over () AS rk FROM (
SELECT id FROM items WHERE fts @@ plainto_tsquery('simple','alpha') LIMIT 20) s)
SELECT COALESCE(v.id, t.id) AS id,
COALESCE(1.0/(60+v.rk), 0) + COALESCE(1.0/(60+t.rk), 0) AS rrf,
(v.rk IS NOT NULL) AS 向量命中, (t.rk IS NOT NULL) AS 全文命中
FROM v FULL OUTER JOIN t ON v.id = t.id
ORDER BY rrf DESC LIMIT 5;
【本人实测】全文命中 2857 条,融合后前 5 名里既有命中全文的、也有命中向量的,两条路都真实参与了排序。 融合公式与取舍细节见 多路检索与 RRF。
7. 量化与维度上限:省内存、换召回
官方给的存储公式(逐字):vector 每维 4 字节 + 8,halfvec 2 字节 + 8,sparsevec 非零元 8 字节 + 16。
【本人实测,各 5000 行 × 768 维】
类型 5000 行占用 相对 fp32 vector(768)fp3220 MB 1× halfvec(768)fp168032 kB ≈ 2.5× 小 bit(768)二值680 kB ≈ 30× 小
二值量化还能直接算汉明距离(<~>),做"先粗排再精排"的第一段:
**【本人实测】**两行的汉明距离 391(768 位里 391 位不同)。
维度上限(官方逐字):vector 索引 2000 维、halfvec 4000 维、bit 64000 维。
超过 2000 维想建索引是常见卡点——官方给的出路是半精度、二值量化、索引子向量(模型支持的话)或降维。
8. 生产清单
□ 建索引前:调大 maintenance_work_mem(否则索引会溢出到磁盘、构建极慢)
□ 建索引时机:IVFFlat 一定要【先有数据再建】(空表建会拿到官方告警)
□ 算子匹配:L2↔vector_l2_ops、余弦↔vector_cosine_ops、内积↔vector_ip_ops
□ 验证:每条向量查询都跑 EXPLAIN ANALYZE,确认走 Index Scan 而不是 Seq Scan
□ 查询期参数:HNSW 调 hnsw.ef_search(默认 40);IVFFlat 调 ivfflat.probes(默认 1,太小)
□ 有过滤条件:先想清楚命中比例;窄过滤必须开 hnsw.iterative_scan 或建部分索引
□ 召回验收:拿精确结果当基准算 recall@k,不要凭感觉
□ 省内存:量大时上 halfvec(2 倍)或二值量化(32 倍),但要知道会掉召回
□ 事务:向量与业务数据写在同一个事务里(这是选它的核心理由,别浪费)
□ 进度:建索引时查 pg_stat_progress_create_index
□ 规模预警:接近千万级/高 QPS 时重新评估,必要时切专用向量库
9. 本文的局限
- 全部是本机容器内实测(单机、无并发、无真实负载),耗时只说明机制,不代表线上表现;
- 数据是均匀随机向量,没有真实 embedding 的聚类结构 —— 这会让召回显得偏低,也让 "空表建索引 vs 有数据后建"的对比信噪比不足(实测 7/10 vs 4/10,方向与预期相反,正文已标注为"不作为结论");
- §5.2 用
SET enable_seqscan = off强制走索引才看到后过滤;小表上规划器本来就爱顺序扫描 (顺序扫描此时反而精确,只是慢)。生产上表更大时规划器才会选索引路径; - 未实测:
halfvec/subvector索引、sparsevec、partial index 的实际效果、pgvectorscale、 流复制下的索引行为、并发 QPS 与 p99; - 官方条款为逐字原文(留档
official-sources.md),与[本人实测]是两类陈述,正文分别标注。
关联
- 选型的大图(谁第一、为什么是第一、其他家的位次):向量数据库全景
- 另一条路(专用向量库的入门与运维):Milvus 入门、Milvus 企业级开发运维方案
- 检索融合的上层:多路检索与 RRF、RAG 的状态与 Pixel RAG
- 数据在 Postgres 里的相邻话题:高性能 SQLite(同样的"写入代价/索引取舍"思路)
- 实测脚本与逐字来源:仓库内
source/pgvector-lab/
自测
- 选 pgvector 的核心理由是什么?为什么它通常不是"因为它快"?
- 独立向量库做不到、而 pgvector 天然能做的那件事是什么?(用一句话说清)
vector的三种算子分别对应哪个 opclass?为什么内积返回负数?- 为什么"加了索引之后查询结果变了"是正常的?官方原文怎么说的?
- 建 HNSW 索引很慢,第一件该查的配置是什么?
- IVFFlat 的
lists和probes官方推荐起点分别是什么?空表建索引会发生什么? - 过滤条件命中 0.1% 时,
LIMIT 10可能返回几条?为什么?怎么修(至少两种办法)? vector索引的维度上限是多少?超了怎么办?
参考
- pgvector 官方 README(默认精确检索、后过滤与迭代扫描、HNSW/IVFFlat 参数与建议、维度上限与存储公式):github.com/pgvector/pgvector
- 逐字引文留档与实测输出:
source/pgvector-lab/official-sources.md、source/pgvector-lab/run.log