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

混合查询为什么不能简单叠加
一个常见的做法是先使用向量索引取得距离最近的若干行,再在应用层或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