尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

pgvector实战:一条SQL搞定向量相似度与业务字段联合查询

发布时间:2026/9/28 17:50:39

资讯中心
01
ARTICLE

pgvector实战:一条SQL搞定向量相似度与业务字段联合查询

pgvector实战:一条SQL搞定向量相似度与业务字段联合查询
做知识库问答或者 RAG 应用的朋友大概率都经历过这么一段纠结业务数据放在 PostgreSQL 或者 MySQL 里向量数据单独丢给 Milvus、Faiss 或者 Elasticsearch然后中间写一堆胶水代码把两边的结果在应用层拼起来。我一开始也是这么干的直到被“两张表同步”和“混合查询”折磨得够呛才彻底转向 pgvector。标题里用了“塞”这个字确实有点戏谑但把向量检索直接装进关系数据库之后你会发现原来需要两套系统配合才能做的事现在真的就是一条 SQL 的事。这篇文章就围绕 pgvector 的实战过程展开重点聊聊怎么用一条 SQL 完成“向量相似度排序 业务字段过滤”的联合查询以及这中间有哪些值得注意的坑。这篇内容比较适合正在做向量检索选型评估的后端工程师、准备搭知识库的开发者还有被“双库架构”的同步问题搞得焦头烂烂的人。我会把从安装、建表、查询到索引调优的完整链路写清楚也会把实际项目中踩过的坑一起交代。1. 为什么说“双库架构”是知识库项目最大的隐性成本1.1 一个典型需求背后的复杂实现先还原一个真实的业务场景。假设你在做一个企业内部的知识库系统文档表在 PostgreSQL 里字段大概有标题、正文、分类 ID、上传人、审核状态这些。同时你把每篇文档的正文做了 embedding向量存在独立的向量数据库里。某天产品经理提了一个需求“用户输入一段问题返回语义最相近的 10 篇已审核文档而且只能返回当前用户所属部门能看的那部分。”这个需求听起来平平无奇但在双库架构下实现起来非常别扭。你只有两条路可以走要么把分类、部门、审核状态这些业务字段全部冗余同步到向量库里向量库在查询时先用业务条件过滤再做向量检索要么先从向量库捞回相似度最高的 100 篇文档拿着这 100 个文档 ID 回业务库里做权限过滤最后在应用层排序截断。方案一的问题在于同步。业务字段一变你得想办法通知向量库更新元数据这个链路越长出问题的地方就越多。我有一次就是文档改了审核状态但向量库那边没同步结果一排检索结果全是未审核的草稿被测试直接提了 bug。方案二的问题大家也能想到如果这 100 篇里大部分都不满足权限条件过滤之后可能连 10 篇都凑不齐而且为了补齐结果你还得多查几轮召回质量很不稳定。1.2 pgvector 的定位不是替代品而是缝合剂pgvector 本质上是 PostgreSQL 的一个扩展它把向量类型和几个距离运算操作符加进了数据库内核。装上它之后向量列就是一个普通的列可以跟其他字段一起参与 WHERE、ORDER BY、JOIN、GROUP BY。这带来的最大好处是你不必再维护两套存储之间的数据一致性了。事务、外键、约束、权限这些关系数据库本来就有的能力直接覆盖到向量数据上。有些人可能会说独立向量数据库在超大规模比如十亿级以上或者超高并发每天千万次查询场景下性能更强。这话没错但在绝大多数中小型知识库、企业内部问答系统、个人项目里数据量也就是十万到百万的级别PostgreSQL 加 pgvector 完全能扛住。更别提多一套系统就多一份运维成本Docker、监控、备份、扩容全都得跟着配。我的观点是能少维护一个组件就少维护一个让业务先跑起来等到量级真的大到 PostgreSQL 搞不定的时候再考虑专业向量库也不迟。1.3 一条 SQL 到底是怎么“联合”的这里说的联合查询和传统意义上的多表 JOIN 不完全一样。pgvector 场景下更常见的形态是向量相似度负责“语义召回”普通 WHERE 条件负责“业务过滤”两者在同一个查询计划里完成。SELECT id, title, category_id FROM documents WHERE category_id 7 AND status 1 ORDER BY embedding [0.12, 0.35, ...] LIMIT 10;这条 SQL 的语义是先筛出分类为 7、状态为已发布的文档再按 embedding 与目标向量的余弦距离排序取前 10 条。数据库会把业务过滤和距离计算放在同一个执行流程里应用层不再需要手动拼装结果。这也是标题里“一条 SQL 搞定联合查询”的真正含义。2. pgvector 环境搭建Windows、Docker、源码编译三条路2.1 Windows 用户最容易踩的第一个坑pgvector 官方文档里写了源码编译的方式但很多团队用的是 Windows 开发机装起来就有点讲究。网上不少人卡在“装了扩展但没有 .dll 文件”“psql 里 CREATE EXTENSION 报错找不到控制文件”这类问题上。如果你用的是 Windows最简单的方案是直接从 pgvector 的 GitHub Releases 页面下载对应 PostgreSQL 大版本的安装包或 zip 包。注意两个细节第一版本号必须和你的 PostgreSQL 匹配比如 PostgreSQL 16 配 pgvector 0.7.x 的 Windows 包第二zip 包解压后要把其中的 .dll 文件放到 PostgreSQL 安装目录的 lib 文件夹把 .control 和 .sql 文件放到 share/extension 文件夹之后才能在 psql 里执行 CREATE EXTENSION。另外如果你的机器上有 Docker Desktop直接用官方镜像是最省心的路子。pgvector/pgvector:pg16这个镜像预先装好了扩展你只需要在初始化脚本里执行CREATE EXTENSION vector就行。我实测下来Docker 方案从拉镜像到能跑通查询十分钟内绝对搞定新手强烈建议先走这条路。2.2 装完之后必须做的两件事装完扩展之后第一件事是在目标数据库里执行CREATE EXTENSION IF NOT EXISTS vector;注意是在业务数据库里执行不是默认的 postgres 库。第二件事是确认版本SELECT extversion FROM pg_extension WHERE extname vector;我见过有人代码写得没问题但CREATE EXTENSION一直报错最后发现是用 psql 连错了库。这个检查虽然简单但能帮你省下不少排查时间。2.3 建表与向量字段设计维度一开始就要想清楚向量列的定义要指定维度这个维度取决于你用的 embedding 模型。比如 OpenAI 的text-embedding-3-small是 1536 维旧的ada-002也是 1536 维国产的 BGE 系列常用 768 维或 1024 维。建表语句大概长这样CREATE TABLE documents ( id BIGSERIAL PRIMARY KEY, title TEXT NOT NULL, content TEXT NOT NULL, category_id INT NOT NULL, status SMALLINT NOT NULL DEFAULT 1, embedding VECTOR(1536) );这里要特别强调一句不要拍脑袋选模型。维度一旦定死后续想改是要付出代价的。如果你先用了 768 维的模型之后团队统一换成了 1536 维的新模型那么老数据要全部重新生成 embedding 并且重训索引这个迁移工作量可不小。3. 核心实战一条 SQL 同时做向量相似度 业务字段过滤3.1 最朴素的排序查询先跑通再说我们先从最简单的查询开始不加任何过滤条件只按向量距离排序。假设查询向量已经通过 embedding 接口生成好了SQL 是这样SELECT id, title, 1 - (embedding [0.12, 0.35, ...]) AS similarity FROM documents ORDER BY embedding [0.12, 0.35, ...] LIMIT 10;这里是余弦距离操作符返回的是距离值范围在 0 到 2 之间越小表示越相似。如果想展示成“相似度”就用1 - distance转一下。排序这块用的是距离的升序排列即距离最小的排最前。很多第一次接触 pgvector 的人会有一个疑问这个距离是在全表范围内都算一遍吗答案是在没有索引的情况下是的PostgreSQL 会对每一行计算距离然后排序取 Top N。小数据量没问题数据量大了就必须上索引这部分放到下一章详细讲。3.2 业务过滤 相似度排序的联合查询现在加上业务条件。还是那个知识库的场景查分类为 7、状态为已发布的文档中与目标文本语义最相近的前 10 条。SELECT id, title, category_id, 1 - (embedding [0.12, 0.35, ...]) AS similarity FROM documents WHERE category_id 7 AND status 1 ORDER BY embedding [0.12, 0.35, ...] LIMIT 10;这条 SQL 的执行逻辑大体上是先通过 WHERE 条件把候选集缩小到“分类 7 且已发布”的文档再在较小的集合里计算向量距离并排序。过滤条件越多、过滤性越强需要计算距离的行就越少查询自然就越快。这也是 pgvector 和独立向量库相比的一个天然优势关系数据库的索引和统计信息可以用来先做业务裁剪。实际开发中查询向量不可能是手写的常量一般是通过参数传入。用 Python 的 psycopg 或者其他语言的驱动时要把向量参数显式转成 vector 类型否则数据库会把它当成字符串报类型错误。正确写法是在占位符后加::vectorSELECT id, title, 1 - (embedding %s::vector) AS similarity FROM documents WHERE category_id %s AND status %s ORDER BY embedding %s::vector LIMIT 10;这个细节看着不大但不注意的话经常会在联调阶段被 TypeMismatch 之类的问题卡一下。3.3 三种距离算子怎么选L2、余弦、内积pgvector 提供三个距离操作符对应三种不同的相似度度量方式操作符含义适用场景返回范围-L2 欧氏距离图像特征、二值向量、聚类场景0 到正无穷余弦距离文本 embedding、语义搜索0 到 2#负内积内积相似度、点积匹配负无穷到正无穷文本 embedding 场景我的首选几乎永远是余弦距离。因为文本向量更关注方向的一致性而不是绝对大小。L2 距离对向量模长比较敏感在某些模型下会把“长度不同但方向一致”的文本判为不相似这跟语义检索的目标不太吻合。内积值得一提pgvector 的#返回的是负内积而不是直接返回内积值。这么设计是为了统一“升序排列即相似度最高”的语义。你要用内积相似度排序时直接ORDER BY embedding # %s::vector就行不用再手动加负号。3.4 在 ORM 里怎么调用Prisma 的实践既然热搜词里不少人关心 ORM 怎么调 SQL我也把 Prisma 的例子写出来。Prisma 目前没有把 pgvector 的操作符封装进 query engine所以最直接的方式是用$queryRaw执行原生 SQL。比如在 NestJS 项目里const results await prisma.$queryRaw SELECT id, title, 1 - (embedding ${queryEmbedding}::vector) AS similarity FROM documents WHERE category_id ${categoryId} AND status 1 ORDER BY embedding ${queryEmbedding}::vector LIMIT 10; ;注意 Prisma 的参数占位符是$1、$2这种但在$queryRaw的模板字符串里可以直接用 JavaScript 变量注入。真正容易踩坑的还是::vector这个类型转换Prisma 传参数时如果没转类型数据库会拿 text 和 vector 做比较直接报错。对于其他 ORM 其实也是同样的思路不能直接用模型 API 的findMany来写向量排序必须退回原生 SQL。这个不是 ORM 的缺陷而是向量操作符本来就是数据库方言的一部分。4. 索引选型与调优HNSW 和 IVFFlat 不能瞎选4.1 先搞清楚这两个索引的本质差别pgvector 支持两种索引IVFFlat 和 HNSW。两者的实现思路完全不一样选错了查询性能和构建成本会差很远。IVFFlat 是“先把向量空间划分为多个聚类中心查询时只搜索最近的几个聚类”有点像按行政区划找人。它有一个硬性要求建索引时数据量要足够大否则聚类中心质量很差。官方建议每个 list 至少对应 1000 条数据几千行的时候建 IVFFlat 往往效果不好。HNSW 是基于图的近似最近邻算法它构建的是一张多层导航图。查询时从顶层进入逐层下探到目标区域有点像在高德地图上先看到全国路网再放大到城市街道。HNSW 不依赖数据量来“训练”中心点可以边插数据边建索引更适合持续增长的数据集。我把两者的选择建议整理成一个表对比项IVFFlatHNSW构建速度快慢一些查询精度略低高是否需要充足数据预训练需要不需要增量更新支持但效果一般支持良好内存占用低高适合场景已积累大量静态数据持续增长、需要高召回一句话总结我的实践结论新项目直接用 HNSW除非你对内存占用极其敏感否则没必要冒着聚类质量差的风险去调 IVFFlat。4.2 索引构建语句与操作符匹配是最大的暗坑HNSW 索引的构建语句CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops) WITH (m 16, ef_construction 64);IVFFlat 索引的构建语句CREATE INDEX ON documents USING ivfflat (embedding vector_cosine_ops) WITH (lists 100);注意vector_cosine_ops这个部分它决定了索引支持哪种距离操作符。如果建索引时用的是vector_cosine_ops那查询里只能用来匹配这个索引如果你拿-去查询索引直接失效数据库会老实巴交地全表扫描。这是我认为最容易让人困惑的一个点明明建了索引EXPLAIN 一看还是 Seq Scan。三个操作符类与距离操作符的对应关系如下vector_l2_ops对应-vector_cosine_ops对应vector_ip_ops对应#4.3 查询阶段的参数调优HNSW 和 IVFFlat 都有查询时才能调的参数这个参数决定了“搜索要探多深”。HNSW 是hnsw.ef_search默认值是 40。调大这个值会提高召回率但查询变慢。我的经验是知识库场景下设置 100 左右比较均衡追求极致召回可以到 200。SET hnsw.ef_search 100;IVFFlat 是ivfflat.probes默认值是 1也就是只搜最靠近查询向量的那个聚类。这个默认值实在太保守了数据分布稍有偏差召回率就会很难看建议至少调到 5 到 10。SET ivfflat.probes 10;注意这两个参数是会话级的用SET设置后只对当前连接生效。如果在连接池场景下使用记得每次查询前都设置一次。4.4 过滤条件下规划器的行为索引不一定是最优解这是 pgvector 实战里最有意思的一个现象。很多人的直觉是“我加了 HNSW 索引任何查询都应该走索引”。但实际上当 WHERE 条件过滤性很强时PostgreSQL 的规划器可能主动放弃向量索引选择全表扫描。举个例子文档表里有 10 万行但category_id 7 AND status 1这个条件只筛出 1000 行。此时就算没有向量索引对这 1000 行逐行算距离也就是一眨眼的事。如果走 HNSW 索引反而要先去索引里找出候选向量再回表做业务字段过滤一来一回未必更快。规划器不傻它在代价估算时会把选择率算进去。所以当你发现某个查询没走向量索引第一反应不应该是“数据库坏了”而是先EXPLAIN ANALYZE看看当前计划是不是真的慢。如果过滤后的行数很少全表扫描的效果可能反而更好。真正确认需要走索引的场景是过滤条件比较宽、候选集很大、又要求毫秒级响应的时候。5. 实测中的坑与排查思路从维度报错到查询退化5.1 维度不一致的报错怎么定位最常见的报错长这样ERROR: different vector dimensions 1536 and 768意思很直白表里的向量是 1536 维但查询传入的向量是 768 维。出现这个问题的原因通常不是代码写错了而是 embedding 模型变了。比如你建表时用的是 768 维的 BGE 模型后来代码里换了 OpenAI 的接口查询向量成了 1536 维两边一碰撞就报错。排查方法也很简单先查表定义确认列维度再打印查询向量的长度两边对不上就是模型换掉了。解决办法不是改查询而是统一模型后重新生成全量 embedding。如果老数据没法立刻全部重算也可以考虑在表里加一列新维度的向量字段新旧两列共存一段时间等迁移完成再删掉旧列。5.2 查询没走索引时我们做了什么我之前在十万级数据量的表上遇到过一个问题HNSW 索引建好了但某个查询始终走全表扫描单次查询要 400 多毫秒。当时第一反应是操作符写错了反复确认了和vector_cosine_ops对得上问题依然存在。后来用EXPLAIN ANALYZE仔细看发现 WHERE 条件的过滤性太强了规划器认为走索引的代价更高。我做了个测试注释掉一个过滤条件再查索引正常生效查询降到 20 毫秒以内。这说明数据库的代价估算模型已经充分考虑了业务过滤的影响它选全表扫描其实是有道理的。如果你的场景确实需要“业务过滤后候选集很大 向量快速召回”同时成立可以考虑另一个思路把业务过滤条件和向量距离放在子查询里利用递归 CTE 或者其他方式让规划器按你期望的顺序执行。不过这是比较进阶的优化手段了普通业务场景先确认过滤条件是否合理更重要。5.3 更新频繁的场景要记得维护索引HNSW 索引支持增量更新但频繁的 UPDATE 和 DELETE 会产生死元组。PostgreSQL 的 MVCC 机制决定了旧版本数据并不会被立即物理清理索引里可能积累很多无效条目查询性能会慢慢退化。这个退化不是一天两天突然发生的而是几周后你发现查询从 20 毫秒变成了 200 毫秒才知道内存里的索引已经脏得不行。解决办法是定期维护VACUUM ANALYZE documents;如果数据变动很大可以考虑重建索引REINDEX INDEX documents_embedding_idx;这个我建议放到定时任务里比如每周跑一次。特别是知识库这种持续写入的场景维护频率不能太低。5.4 用 EXPLAIN ANALYZE 做验证的习惯整个实战过程中让我收获最大的一个习惯就是对每条核心 SQL 都做EXPLAIN ANALYZE。不管是确认索引是否生效还是排查查询变慢的原因先看执行计划永远比瞎猜快得多。EXPLAIN ANALYZE SELECT id, title FROM documents WHERE category_id 7 AND status 1 ORDER BY embedding [0.12, 0.35, ...] LIMIT 10;从执行计划里你能直观看到这些信息候选集有多少行、排序用了多久、是否走了索引、实际返回多少行。特别是Execution Time和Rows Removed by Filter这两个指标配合起来看几乎能定位所有基础性能问题。做知识库项目这一年多来我最大的体会是pgvector 不是银弹独立向量库也不是。选型的关键在于你愿意维护几套系统。如果业务数据本来就在 PostgreSQL 里业务过滤条件又很重那 pgvector 的“一条 SQL 联合查询”就是最舒服的方案。最后再分享一个小技巧每次上线前把自己最核心的几条 SQL 全部跑一遍EXPLAIN ANALYZE把执行计划截图留档下次改代码之后做对比性能有没有劣化一眼就能看出来。这个习惯在多人和长期迭代的项目里特别管用。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。