pgvector实战:把向量检索塞进PostgreSQL,一条SQL搞定联合查询
发布时间:2026/9/28 17:50:23来源:尧图网络
把 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-pgvectorCentOS/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()取分组内前 NWITH 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可以实现一个最简单的 RRFReciprocal 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.probesHNSWUSING 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 ANALYZEEXPLAIN 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 慢查询优化顺序我一般按下面这个顺序排查慢查询先看执行计划是Seq Scan还是Index Scan确认索引有没有被使用。确认LIMIT是否存在没有 LIMIT 时向量索引经常不走。确认距离操作符和索引ops是否匹配。索引用vector_cosine_ops查询用-L2的话索引无效因为操作符类不同。确认ef_search或probes参数是否设置太低会导致检索精度差但不会变慢太高显著影响延迟。最后才考虑升级机器内存、调整 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 dimensionsERROR: 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 解决过去需要两三个系统才能搞定的事。
网站建设高端定制企业官网