☰
pgvector实战:把向量检索塞进PostgreSQL,一条SQL搞定联合查询
2026/9/28 17:50:20 网站建设 项目流程

把 pgvector 和向量检索“塞”进关系数据库,然后一条 SQL 搞定联合查询,这个思路我一开始是持怀疑态度的。毕竟过去做 AI 应用,向量库和业务库基本是两个系统:一边用专门的向量数据库存 embedding,另一边用 MySQL/PgSQL 存业务数据,检索时先查向量库拿 ID 列表,再回业务库查详情,中间还有数据同步、一致性、重复开发一堆破事。pgvector 出现以后,等于直接在 PostgreSQL 里多了一种 vector 类型,支持用 SQL 做相似度排序。你可以在一个库里同时维护文本、业务字段和向量,写一条带 WHERE 条件 + ORDER BY 距离的 SQL,把“找相似”和“按条件过滤”“关联其他表”一起搞定。

这篇文章不是简单介绍 pgvector 有什么,而是把我从安装、建表、写 SQL 到调索引、排查慢查询的完整过程记录下来。适合正在做知识库、推荐系统、语义搜索的后端开发,也适合想评估“能不能把向量检索并入现有业务库”的技术负责人。你可以直接照着操作,也可以只读里面的设计思路和避坑点。

1. 为什么非要把向量检索放进关系数据库

1.1 传统两套架构的痛点

我做第一个知识库问答项目的时候,用的是“向量库 + MySQL”的经典组合。流程看起来清晰:离线把文档切片、生成 embedding、写入向量库;在线查询时把用户 query 做 embedding,然后去向量库做 topK 检索,拿到 doc_id 列表,再拿着这一堆 ID 去 MySQL 查标题、分类、权限、状态。

这个方案最大的问题在“一致性”和“复杂度”。每次文档改了 embedding,需要同时更新两个系统,一边是向量库的向量数据,一边是 MySQL 的业务数据,事务根本没法跨库保证。线上偶尔会出现“向量库里能搜到,但业务库里查不到详情”的脏数据,排查起来特别痛苦。另外为了减少跨库查询,我得缓存 ID 列表,或者把部分业务字段冗余到向量库,导致向量库越来越像业务库,维护成本直接翻倍。

还有权限过滤的问题。很多场景要求“只搜索当前用户可见的数据”,比如公司内部知识库按部门隔离。两套架构下,我得先向量检索出一大批结果,再在业务层用代码逐条过滤,或者批量查 MySQL 做二次筛选。topK 明明只要 10 条,向量库却得拉回 100 条甚至更多来兜底,不然过滤完就剩不下几条了。这种额外开销在小 demo 里没什么,真到了几百万条数据和复杂权限规则的时候,体验和性能都会崩。

1.2 pgvector 的关键价值:类型、索引、操作符

pgvector 不是一个独立的数据库,而是一个 PostgreSQL 扩展。它做的事情无非三件:提供 vector 数据类型,提供几种距离计算操作符,提供专门的向量索引方法。正是因为这三件事都发生在数据库内部,向量字段和普通字段才能放进同一张表,共用一套事务、备份、权限体系。

这个思路很像“把内存缓存塞进数据库的 JSON 字段”——听上去不如专门的系统强大,但绝大多数业务场景根本不需要那种极端性能,反而更需要简单、可靠、可维护。比如你已经有 PostgreSQL 在承载业务数据,那么加一个扩展就能获得向量检索能力,不需要引入新的中间件,不需要学习新的 API,不需要维护两套数据。对于中小团队和技术验证阶段,这个“少一个系统”的价值可能比性能数字更重要。

1.3 什么场景适合,什么场景不适合

我在实践中对 pgvector 的定位是“够用就好”。适合的场景包括:数据量在百万级到千万级、对召回率要求不是极度苛刻、业务逻辑复杂需要频繁和结构化条件联合过滤、希望降低系统架构复杂度。比如个人知识库、商品语义搜索、企业内部文档检索、基于用户行为向量的粗排召回,这些我都觉得没问题。

不适合的场景也要说清楚。如果你有上亿甚至十亿级向量、对 p95 延迟要求个位数毫秒、召回率要求在 95% 以上,那 pgvector 大概率不是最优选择。专用向量数据库在某些高并发场景下确实有性能优势,pgvector 能帮你快速上线,但不保证能陪你走到互联网级规模。我做项目时有个原则:先用 pgvector 把业务跑通,如果哪天量级上来了、指标出现明显瓶颈,再考虑迁移到专用系统,反正 embedding 数据和业务数据都在库里,导出也方便。

2. 从零开始:安装 pgvector 和准备基础环境

2.1 扩展安装的几种方式

我第一次是在 Windows 上安装 pgvector,这里确实有点坑。pgvector 的源码是一个 C 扩展,需要匹配 PostgreSQL 的版本和架构,不是随便装个 pip 包就能用。官方提供了预编译的 Windows 安装包,你可以在 GitHub Releases 页面找到类似pgvector-0.7.4-pg16-windows-x64.zip这样的文件,注意看清楚 pg 后面的版本号,必须和你的 PostgreSQL 一致。

下载后解压,把里面的vector.control、vector--*.sql文件放到 PostgreSQL 安装目录的share/extension下,把vector.dll放到lib下。然后重启 PostgreSQL 服务,在数据库里执行CREATE EXTENSION vector;,看到CREATE EXTENSION输出就说明成功了。如果报找不到文件或者权限错误,八成是版本不匹配或者放错了目录。

Linux 下要省心很多,Ubuntu/Debian 用apt-get install postgresql-16-pgvector,CentOS/RHEL 用dnf install pgvector_16,不过这些包通常在官方源里没有,需要先配置 PGDG 源。另外如果你用 Docker,可以之间拉pgvector/pgvector:pg16镜像,里面已经装好了扩展,适合拿来快速验证。

2.2 验证安装和创建扩展

安装完成后,先接入你的数据库执行:

CREATE EXTENSION IF NOT EXISTS vector;

然后可以做一个最简单的冒烟测试:

SELECT '[1,2,3]'::vector;

如果返回[1,2,3],说明 vector 类型已经生效。接着建一张测试表:

CREATE TABLE items ( id bigserial PRIMARY KEY, title text, category text, price numeric, embedding vector(3) );

这里的vector(3)表示每个向量是三维的,实际使用中通常用 384 维或 1536 维,取决于你用的 embedding 模型。要注意维度一旦定下来,表里每条数据的向量维度就必须一致,否则插入会报错。我一开始没注意这点,换了个模型重新生成向量后忘了改表结构,折腾了半天才发现是维度不一致。

2.3 向量距离计算:三种操作符怎么选

pgvector 提供了三种距离操作符,这是写 SQL 时最基础的知识:

操作符含义排序方式典型场景
<->L2 欧氏距离值越小越相似图片特征、通用 embedding
<#>负内积值越小越相似内积相似度,适合未归一化向量
<=>余弦距离值越小越相似文本语义搜索最常用

一开始我有个误区:以为余弦相似度越大越相似,然后写ORDER BY embedding <=> query DESC。实际上这三个操作符算的都是“距离”,距离越小越相似,全部要用ASC排序。如果你用的是 OpenAI 的 text-embedding-ada-002 这类模型,默认输出向量的模长已经被归一化,此时余弦距离可以稳定反映语义相似度。

插入一条数据时,向量字面量直接用数组形式:

INSERT INTO items (title, category, price, embedding) VALUES ('pgvector实战教程', '技术', 39.9, '[0.1, 0.2, 0.3]');

如果向量是程序里生成的,比如 Python 那边通过openai接口拿到的 embedding,用参数化方式传进去,不要把数组手拼成字符串,避免 SQL 注入和转义问题。

3. 一条 SQL 搞定向量检索和业务条件联合查询

3.1 先说一个真实的业务场景

假设我在做一个在线课程平台的“找相似课程”功能。课程表里有标题、简介、分类、价格、上架状态这些普通字段,同时每门课有一个 embedding 向量,表示课程内容的语义。用户看中一门课,想找内容相近的课,但必须满足:分类相同、价格不超过 100 元、处于上架状态。传统做法是先查向量库拿相似课程 ID,再去业务库过滤,最后还要处理分页和排序。用 pgvector 就简单了——课程本身和向量就在同一张表里,一条 SQL 同时完成语义排序和结构化过滤:

SELECT id, title, category, price, embedding <=> '[0.12, 0.34, ...]'::vector AS distance FROM courses WHERE category = '数据库' AND price <= 100 AND status = 'on_shelf' ORDER BY distance ASC LIMIT 10;

这条 SQL 我看了不下十遍,每次看都觉得舒坦——没有跨系统调用,没有代码里二次过滤,数据库原生把“向量最近”和“业务条件”一起算完了。执行计划里,优化器会先判断业务过滤条件的选择性,再决定是先走向量索引还是先做条件过滤,这一点后面调优时再细说。

3.2 用 JOIN 同时检索多个业务表

上面还只是单表操作。实际业务中向量检索经常要和关联表配合,比如用户收藏表。需求是:推荐 10 门和当前用户收藏的课程整体语义最接近的课程,且这些课程没有被用户收藏过。这个需求在传统“向量库 + 业务库”架构下要写好几轮循环,pgvector 里一条 SQL 就出来了:

SELECT c.id, c.title, c.category, c.embedding <=> '[0.2, 0.1, ...]'::vector AS distance FROM courses c LEFT JOIN user_favorites uf ON uf.course_id = c.id AND uf.user_id = 123 WHERE uf.id IS NULL AND c.status = 'on_shelf' ORDER BY distance ASC LIMIT 10;

这里核心思路就是:把 pgvector 的距离计算当作一个普普通通的表达式,放在 SELECT 和 ORDER BY 里面,其他任何 PostgreSQL 能力都可以照常使用。关联子查询、CTE、窗口函数这些高级特性,全都能和向量检索放一起。这是 pgvector 作为“扩展”而非“独立系统”的巨大优势。

3.3 每个分类各取 Top-N:窗口函数也不含糊

还有一种常见需求:不看全局 topK,而是希望结果均匀分布在多个分类里,每个分类取最相似的若干条。SQL 窗口函数正好派上用场。给每个分类按距离排序,再用ROW_NUMBER()取分组内前 N:

WITH ranked AS ( SELECT id, title, category, embedding <=> '[0.1, 0.3, ...]'::vector AS distance, ROW_NUMBER() OVER ( PARTITION BY category ORDER BY embedding <=> '[0.1, 0.3, ...]'::vector ASC ) AS rn FROM courses WHERE status = 'on_shelf' ) SELECT id, title, category, distance FROM ranked WHERE rn <= 5 ORDER BY category, distance ASC;

这种写法等于把 pgvector 的向量距离计算能力嵌入到窗口函数的排序表达式里,从根本上避免了“查完一堆向量再分段挑”的笨办法。我实测这个 SQL 在几十万行、维度 384 的情况下,加了 HNSW 索引后跑得挺快,分组聚合造成的开销主要来自排序本身,和是不是向量关系不大。

3.4 和全文检索混搭出“混合检索”

知识库应用里还有一类玩法叫混合检索:既要关键词命中,又要语义相近。PostgreSQL 内建全文检索配合 pgvector,可以实现一个最简单的 RRF(Reciprocal Rank Fusion)雏形。不用搞复杂的数学,直接让两种检索结果用 UNION 合并再按最终分数排,或者用ts_rank和向量距离做加权。

简单做法是先把全文检索的结果和向量检索的结果各自限制一个较大的候选集,然后用UNION把结果合并,加上一个bonus分数,比如全文命中的记录在距离基础上减去一个固定值:

WITH semantic AS ( SELECT id, title, embedding <=> '[0.2, 0.3, ...]'::vector AS distance, 0 AS fts_bonus FROM courses ORDER BY distance ASC LIMIT 200 ), fts AS ( SELECT id, title, 0 AS distance, 0.15 AS fts_bonus FROM courses WHERE to_tsvector('chinese', title || ' ' || description) @@ to_tsquery('pgvector & 实战') LIMIT 200 ), combined AS ( SELECT * FROM semantic UNION ALL SELECT * FROM fts ) SELECT id, title, MIN(distance - fts_bonus) AS final_score FROM combined GROUP BY id, title ORDER BY final_score ASC LIMIT 20;

注意我这里用distance - bonus这种简单方式示意,实际调优需要根据业务调整权重。这个 SQL 已经不是“一条 SQL 搞定联合查询”的程度了,它是把一个完整检索策略压缩在了一条查询里。虽然不算标准 RRF,但胜在简单、可解释。

4. 索引选型与性能调优,这步直接决定体验

4.1 没有索引时只有几十万行就会卡顿

如果你的表只有几千行,直接全表扫描算距离也能接受。但到了十万、百万级别,每个查询都要拿全部 embedding 逐一和查询向量做距离计算,那响应时间会随时间线性增长。我第一次在几十万行课程数据上跑ORDER BY embedding <=> query LIMIT 10,不加索引时响应时间直接飙到 800 毫秒以上,加了 HNSW 索引后掉到 30 毫秒左右,这个差距是肉眼可见的。

pgvector 提供两种索引方法,我做成表格给你对比:

索引类型建索引方式适合数据量优点注意点
IVFFlatUSING ivfflat (embedding vector_cosine_ops)百万级以上索引体积小、构建快建索引前表里最好已有数据,lists参数要提前设好;查询时要用ivfflat.probes
HNSWUSING hnsw (embedding vector_cosine_ops)十万到百万级查询精度高、无需预训练、支持增量插入内存占用大、构建耗时较长,m和ef_construction参数影响质量

4.2 我的建索引标准流程

以余弦距离为例,执行:

CREATE INDEX ON courses USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64);

m表示每个节点的最大连接数,ef_construction表示构建时动态列表大小。值越大索引质量越高,但占用内存也越多。我建议别一开始就上超大参数,先用默认值验证效果,不够再调。数据量只有几十万行时,m = 16, ef_construction = 64已经能有不错的表现;真正到千万级再来考虑m = 32, ef_construction = 128。

如果是 IVFFlat,建索引的时机很重要,因为它基于 k-means 聚类,空表或数据量太少时聚类效果差。正确姿势是先把数据写入表,再建索引:

CREATE INDEX ON courses USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);

lists通常设置为行数的开方量级左右,比如 100 万行设 1000 个 lists 比较合理。设置太小每个桶数据太多,检索精度下降;设置太大建索引慢且占用空间涨得快。

4.3 查询期参数:ef_search 和 probes

建完索引后,查询时还需要设置一个额外的参数,这个参数不写在 SQL 里,而是通过SET语句在会话级别设置:

SET hnsw.ef_search = 100;

ef_search越大,搜索时检查的候选节点越多,召回率越高,但查询变慢。ef_search建议设置在 40 到 100 之间。IVFFlat 对应的参数是ivfflat.probes,表示查询时检查多少个聚类桶,比如:

SET ivfflat.probes = 10;

这两类参数千万不要写在 SQL 语句里,我第一次用 pgvector 时以为像LIMIT一样直接放在查询里,结果报语法错误。正确做法是应用层在建立连接池之后初始化一下,或者在每次执行前SET LOCAL,避免影响其他会话。

4.4 用 EXPLAIN ANALYZE 判断索引有没有生效

判断索引是否生效,最直接的方式是执行计划。加上EXPLAIN ANALYZE:

EXPLAIN ANALYZE SELECT id, title FROM courses ORDER BY embedding <=> '[0.1, ...]'::vector ASC LIMIT 10;

如果执行计划里出现"Index Scan using courses_embedding_idx",说明向量索引被实际使用;如果出现的是"Sort"和"Seq Scan",说明优化器选择了全表扫描。遇到后者时我先检查 SQL 里是不是漏了LIMIT——pgvector 的近似索引扫描需要 LIMIT 来触顶,全表排序通常发生在没有 LIMIT 的情况下。

另外要注意的是,不是所有情况下走索引都更好。如果业务条件WHERE category = '数据库'本身能过滤掉 99% 的数据,优化器可能选择先按 category 过滤再算距离,反而更快。这时不要强行用索引提示,让数据库自己做决策更稳妥。

4.5 慢查询优化顺序

我一般按下面这个顺序排查慢查询:

  1. 先看执行计划是Seq Scan还是Index Scan,确认索引有没有被使用。
  2. 确认LIMIT是否存在,没有 LIMIT 时向量索引经常不走。
  3. 确认距离操作符和索引ops是否匹配。索引用vector_cosine_ops,查询用<->(L2)的话索引无效,因为操作符类不同。
  4. 确认ef_search或probes参数是否设置,太低会导致检索精度差但不会变慢,太高显著影响延迟。
  5. 最后才考虑升级机器内存、调整 PostgreSQLshared_buffers。

5. 常见问题与排查技巧实录

5.1 安装后 CREATE EXTENSION 报错:文件版本不匹配

我最常遇到的是could not open extension control file。Windows 上手动安装时,控制文件和 SQL 脚本放错目录,或者 PostgreSQL 版本和 pgvector 预编译包版本不一致。这里没有捷径,先查SELECT version();看 PostgreSQL 具体小版本,再去下载对应 pg 大版本的包。PostgreSQL 16 和 PostgreSQL 15 的 pgvector 包不能互相替代。另外记得以管理员权限运行命令提示符,否则写不进安装目录。

5.2 执行查询时报错:different vector dimensions

ERROR: different vector dimensions

这个错误意思是查询向量维度和表中存储的维度不一致。比如建表时定义了vector(384),但程序传入了 1536 维向量。常见原因是切换了 embedding 模型,或者开发环境用了假的测试向量。解决方法是统一模型后重建表或者ALTER TABLE ... ALTER COLUMN ... TYPE vector(1536),不过我会直接重建列并重新计算全部 embedding,避免脏数据残留。

5.3 查出来结果不准确,像是乱找

如果索引和数据量没问题,结果不准确大概率是相似度口径问题。文本向量用余弦距离没有错,但有些 embedding 模型生成向量时没有归一化,此时余弦距离和欧氏距离的排序结果可能有差异。你可以先用一个小样本数据集,把三种距离操作符的结果打印出来对比,看哪个最符合业务直觉。还有一种情况是ef_search设得太低导致召回率不够,特别是有强过滤条件时,建议先调大参数观察一下。

5.4 距离值是负数或大于 1,到底怎么回事

余弦距离<=>的正常范围在 0 到 2 之间,0表示完全相似,2表示方向完全相反。如果你看到负数或者大于 2,先确认是不是用了错误的操作符。有些模型返回的向量没有归一化,此时内积可能任意大,余弦距离也可能超出常规范围。排序本身没问题,但如果你希望对外展示一个 0-100 的相似度分,可以先在程序里做归一化,或者把1 - 余弦距离转成相似度再映射。千万别直接在 SQL 里假设余弦距离一定在 [0, 1]。

5.5 带上业务过滤条件后反而更慢

我遇到过不少次:单跑向量排序很快,加上WHERE status = 'on_shelf' AND category = '数据库'后变慢。这时我第一反应不是怀疑索引失效,而是先看过滤条件对数据的选择性。如果条件能过滤掉 90% 数据,数据库会优先全表过滤再对剩余行算距离,此时向量索引没被使用是正常的。这种场景下的优化思路是:要么给category、status建普通 B-tree 索引,让过滤更快;要么让查询分成两步,先粗召回一批候选,再在应用层过滤。后者需要写点代码,但有时候效果更可控。

5.6 SQL 注入风险依然要重视

最后提一句和 SQL 本身相关的事。pgvector 只是新增了类型和操作符,它并没有改变 SQL 注入的风险模型。应用里如果要根据用户输入动态拼 SQL,无论是拼普通条件还是拼向量字面量,都应该使用参数化查询。比如 Python 的psycopg2里:

cursor.execute( "SELECT id FROM courses ORDER BY embedding <=> %s::vector LIMIT 10", (query_vector,) )

而不是用 f-string 把query_vector直接拼进 SQL。向量数组看起来像一串数字,但它本质上还是用户可控的字符串,千万别图省事。这一点和以前写普通 SQL 的注意事项完全一致,不会因为用了向量检索就自动免疫。

5.7 问题速查表

现象可能原因处理方式
CREATE EXTENSION报找不到文件pgvector 版本和 PG 版本不匹配下载对应 PG 大版本的包,按目录放置
查询报different vector dimensions向量维度不一致统一 embedding 模型,重建向量列
索引没生效没有 LIMIT、操作符和 ops 不匹配加上 LIMIT,检查<=>/<->是否匹配vector_cosine_ops/vector_l2_ops
结果不准确ef_search 太低或没用余弦距离调大hnsw.ef_search,换用<=>
带业务条件变慢过滤选择性太强,优化器走别的路径给普通字段建索引,或拆两步查询
延迟高但召回率正常HNSW 内存不够调小m、ef_construction,或换 IVFFlat

从我自己实际操作下来的感受说,pgvector 不是那种“看起来很厉害但用起来束手束脚”的技术。它最打动我的地方在于把向量检索拉回了 SQL 的舒适区,业务团队写查询条件不需要学习一套新的检索语法,DBA 也能用熟悉的执行计划来分析性能。如果你手里已经有一套 PostgreSQL 基础设施,又是从知识库、语义匹配这类场景切入 AI 应用,完全值得从今天开始拿一张表试试看。先把单表检索跑通,再加上业务过滤、联合查询、窗口函数,你会发现自己正在用一条条 SQL 解决过去需要两三个系统才能搞定的事。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询