导读:本期聚焦于王柏年创作的《PostgreSQL向量与标量混合查询如何兼顾召回率与性能?》,敬请观看详情。在商品推荐或语义检索系统里,向量相似度排序与价格、库存、类目等标量过滤往往同时出现。如果先执行标量过滤再对候选集做向量距离计算,可能扫全表;若完全依赖向量索引先取Top N,过滤条件会直接丢弃大量相似结果,导致召回不足。PostgreSQL借助pgvector扩展可以构建混合查询,但优化器如何处理过滤条件与距离排序、HNSW索引和部分索引如何配合、多列过滤下执行计划如何变化,是决定查询延迟的关键。本文通过可执行的建表、索引与查询示例,拆解混合查询的典型实现路径,分析不同索引组合的适用边界,并结合执行计划给出参数调优建议,帮助你在亿级向量数据上稳定获得低延迟结果。

PostgreSQL在引入pgvector扩展后,已经具备了对向量数据进行存储与近邻检索的能力。现实业务中很少只按向量相似度返回结果,价格区间、库存状态、类目归属、发布时间等标量条件会与向量距离排序同时出现。这类混合查询的难点在于,向量索引擅长快速定位近似最近邻,但无法直接理解标量过滤条件;B-tree等标量索引擅长过滤,却无法按向量距离排序。理解优化器如何在二者之间选择执行路径,是优化查询性能的前提。

PostgreSQL向量与标量混合查询如何兼顾召回率与性能?

混合查询为什么不能简单叠加

一个常见的做法是先使用向量索引取得距离最近的若干行,再在应用层或SQL外层对标量条件进行过滤。这种方式的问题非常明显:向量索引返回的Top N并不保证满足标量条件的行一定在其中。例如查询要求返回价格低于100元且与查询向量最接近的10件商品,如果向量索引先返回全局最相似的100件商品,而其中只有3件价格低于100元,最终结果只剩3条,召回数量不足。为了提高召回,只能把Top N放大到500、1000甚至更大,但这会增加大量无效计算和回表开销。

反过来,如果先执行标量过滤再计算向量距离,虽然召回率有保障,但可能退化为全表扫描。当价格低于100元的商品有几百万行时,对这几百万行逐一计算向量距离的代价非常高。PostgreSQL的优化器在某些条件下会选择对过滤结果进行排序,但向量距离运算符通常无法利用普通索引有序扫描,只能显式排序或使用向量索引辅助。因此,混合查询需要同时考虑过滤选择率、向量索引的召回边界以及回表成本。

pgvector提供的HNSW和IVFFlat索引都属于近似最近邻索引。它们通过牺牲一定精度换取查询速度,并且可以直接在索引扫描中应用距离排序,但这只对无过滤条件的纯向量查询最有效。一旦加入标量谓词,执行器可能无法在索引扫描的同时安全应用过滤,因为索引返回的顺序是按照向量距离排列,过滤掉某些行之后,后续更远的行仍然可能满足条件,但HNSW索引无法保证继续遍历会得到足够数量的过滤结果。因此理解执行计划中的Filter节点与索引扫描关系至关重要。

基于pgvector的混合查询基础实现

先创建一个示例表,包含商品ID、类目、价格、库存以及向量字段。向量维度这里使用384维,便于演示。安装pgvector后,可以直接使用vector数据类型。

CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE products (
    id bigserial PRIMARY KEY,
    category text NOT NULL,
    price numeric(10,2) NOT NULL,
    stock int NOT NULL,
    published_at date NOT NULL,
    embedding vector(384) NOT NULL
);

CREATE INDEX products_category_price_idx ON products (category, price);
CREATE INDEX products_stock_idx ON products (stock);
CREATE INDEX products_embedding_hnsw_idx ON products USING hnsw (embedding vector_cosine_ops);

上面的结构里,标量字段有独立的复合B-tree索引和单列索引,向量字段有HNSW索引。一个典型的混合查询可以写成如下形式:先按类目和价格过滤,再按余弦距离排序并限制返回条数。

SELECT id, category, price, stock,
       1 - (embedding <=> $1) AS similarity
FROM products
WHERE category = 'electronics'
  AND price < 100
  AND stock > 0
ORDER BY embedding <=> $1
LIMIT 10;

这里 embedding <=> $1 表示查询向量与存储向量之间的余弦距离,距离越小越相似。为了显示相似度,用1减去距离得到余弦相似度。实际执行时,PostgreSQL会根据统计信息估算过滤条件的选择率。如果electronics类目且价格低于100元的行数很少,优化器可能直接使用B-tree复合索引取得候选行,再对这些行计算向量距离并排序,这种路径回表少、延迟低。如果过滤条件宽松,候选集很大,优化器可能尝试使用HNSW索引按向量距离顺序扫描,并在扫描过程中应用Filter,直到凑满10条满足条件的行为止。

然而,第二种路径存在风险。HNSW索引可以按距离顺序返回行,但如果过滤条件与向量距离不相关,满足条件的行在距离序列中可能非常稀疏。例如查询要求类目为books,但向量索引返回的最近邻主要来自electronics,扫描了大量不相关行后才凑够10条books。执行器可能因此消耗大量堆扫描,查询延迟显著升高。此时单纯依赖一个HNSW索引并不理想。

索引策略与过滤条件协同

为了提升混合查询性能,最直接的方法是让向量索引尽量贴近过滤后的数据分布。PostgreSQL支持部分索引,可以在创建向量索引时加入与查询过滤条件一致的WHERE子句。例如业务中大量查询只关心库存大于0的商品,那么可以创建只包含有库存商品的HNSW索引。

CREATE INDEX products_embedding_in_stock_hnsw_idx
    ON products USING hnsw (embedding vector_cosine_ops)
    WHERE stock > 0;

当查询同时包含 stock > 0 条件时,优化器可以匹配这个部分索引。它的好处有两层:一是索引更小,只覆盖有库存商品;二是索引扫描返回的行天然满足过滤条件,避免在HNSW遍历过程中因库存为0而丢弃大量结果。部分索引尤其适合过滤条件固定且选择性较高的场景。

如果过滤条件不是固定的,而是价格区间、类目等多种组合,可以通过多列过滤与向量距离相结合,利用B-tree索引先缩小候选集范围。例如类目加价格范围的复合索引可以在扫描时直接定位到符合条件的行,然后再对候选行计算向量距离。这种方法在过滤选择率低于一定阈值时非常有效。可以用如下查询观察执行计划。

EXPLAIN (ANALYZE, BUFFERS, VERBOSE)
SELECT id, category, price
FROM products
WHERE category = 'electronics'
  AND price BETWEEN 50 AND 150
ORDER BY embedding <=> $1
LIMIT 10;

执行计划中如果出现Index Scan using products_category_price_idx,并且随后有Sort节点按照向量距离排序,说明优化器选择了先过滤再排序的路径。这种路径在候选集较小时非常快,甚至不需要使用HNSW索引。但如果候选集很大,排序会消耗大量内存和CPU,可能触发外部排序。此时需要结合向量索引或调整查询条件来降低候选集规模。

另一种思路是将标量条件编码进向量或分区中。例如按类目分区存储商品,每个分区内使用HNSW索引。查询时先定位到相关分区,再在分区内做向量近邻检索。这种方法适合类目明确且数据量极大的场景,但需要对表结构进行额外设计。

执行计划分析与参数调优

混合查询的性能不能只看索引是否存在,还要看优化器是否选择了合理路径。可以用EXPLAIN ANALYZE观察实际执行计划中的关键指标:索引扫描返回的行数、Filter移除的行数、排序耗时以及堆扫描次数。一个典型的不理想计划是HNSW索引扫描返回大量行,但Filter节点移除率很高,说明向量索引返回的顺序与过滤条件不匹配。

EXPLAIN (ANALYZE, BUFFERS)
SELECT id, category, stock
FROM products
WHERE stock > 0
ORDER BY embedding <=> $1
LIMIT 10;

如果计划中显示HNSW索引扫描了数十万行才凑够10条结果,说明该索引不适合这个过滤条件。可以通过创建部分索引、调整过滤条件顺序或引导优化器使用B-tree先过滤来解决。PostgreSQL允许通过设置enable_indexscan、enable_seqscan等参数影响计划,但不建议全局修改,最好在事务级别验证。

BEGIN;
SET LOCAL enable_indexscan = off;
SET LOCAL enable_seqscan = off;
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, category, stock
FROM products
WHERE stock > 0
ORDER BY embedding <=> $1
LIMIT 10;
ROLLBACK;

另一个调优方向是HNSW索引的参数。pgvector的HNSW索引支持m和ef_construction两个构建参数。m控制每层节点的最大连接数,默认16;ef_construction控制构建时动态候选列表大小,默认64。增大这两个参数能提高召回率,但会增加索引构建时间和内存占用。查询时可以通过设置 hnsw.ef_search 参数调整搜索候选列表大小,默认40。混合查询中如果过滤条件导致满足条件的行稀疏,可以适当增大该参数,让HNSW在更宽的范围内搜索,减少漏召回。

SET hnsw.ef_search = 100;

SELECT id, category, stock
FROM products
WHERE stock > 0
ORDER BY embedding <=> $1
LIMIT 10;

需要明确的是,hnsw.ef_search 增大后会增加每次查询的扫描开销,延迟与召回率之间存在权衡。建议在真实数据分布下用基准查询进行对比,观察返回结果与精确暴力搜索的召回重叠率,同时记录P95延迟。对于IVFFlat索引,查询时可以调整 ivfflat.probes 参数,它控制探测的聚簇数量。过滤条件可能导致某些簇内没有满足条件的行,因此混合查询中适当增大probes能改善召回,但同样会增加扫描量。

最后,如果标量过滤条件非常复杂,例如多个范围条件、JSON字段过滤、地理位置过滤等,单纯依赖现有索引可能无法获得稳定性能。此时可以考虑在应用层拆分查询:先用标量条件异步获取主键候选集,再对这些主键对应的向量做精确距离计算;或者使用PostgreSQL的物化视图维护预过滤数据子集。无论采用哪种方案,都应先通过执行计划量化各阶段成本,再决定索引设计和参数调整方向。混合查询的本质是在过滤选择率与向量召回之间找到平衡点,而不是追求单一索引覆盖所有场景。

PostgreSQL向量检索标量过滤修改时间:2026-08-30 20:32:16

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。