当你在 PGVector 中同时执行向量相似度检索和标量过滤时,延迟突然从几十毫秒飙升到几百毫秒甚至更高——这并非偶然的工程失误,而是 PostgreSQL 查询优化器在“混合搜索”这个场景下被推到了它的能力边界。
索引结构决定了搜索路径不是一条直线
PGVector 的 IVFFlat 索引会把高维向量划分成多个聚类(lists)。查询时,根据向量与聚类中心的距离选定若干个候选 list,再在这些 list 内部遍历找出近似最近邻。这个机制在纯向量搜索时高效且可控,但一旦附加 WHERE 条件,整个执行路径就变了。
PostgreSQL 优化器面对一个同时包含 ORDER BY embedding <=> query_vector LIMIT N 和 WHERE metadata->>'tag' = 'urgent' 的查询时,它必须做出的选择是:要么先利用向量索引取出“距离近”的候选行再过滤标量条件,要么先按标量条件过滤再计算距离排序。前者的问题是,如果标量过滤器的选择性很高(比如只保留 1% 的行),你从向量索引里捞出来的大量候选行在回表检查后会被丢弃,导致对索引和堆的大量无用扫描。后者的问题更致命——一旦先做标量过滤,向量索引就无法被驱动,因为索引键是向量,它根本无法在扫描层面植入一个对其他列的过滤条件。于是退化成了对过滤结果集的完整距离计算加排序,而且过滤本身可能还得走 Seq Scan 或者 Bitmap Scan。
更隐蔽的杀伤力来自搜索剪枝的失效
IVFFlat 索引中设置的 lists 参数本身就意味着一种剪枝:只探索部分聚类。但标量过滤会让这种剪枝的性质发生变化。PostgreSQL 使用向量索引时,通常会用“Index Scan”节点去读取向量列索引,拿到元组 ID 再回表。然而 PostgreSQL 的 Index Scan 支持所谓的 “Filter” 条件:可以在索引扫描之后、回表之前或在回表时对行做额外的条件检查。如果标量过滤条件被当作 Filter 附加在向量索引扫描上,索引本身仍然会被全量驱动——它必须先按照向量距离的顺序生产出所有可能的行,再用 Filter 排除掉不满足条件的,直到凑够 LIMIT 条。
这就意味着,哪怕你只想要 10 条满足某个条件的最近邻,数据库依然可能被迫遍历大量与过滤条件无关的向量聚类,只为从中筛出那稀少的几条。当数据量跨过 500 万级别,这种“生产后丢弃”的开销会迅速吞噬掉向量索引带来的增益,甚至在很多人的测试中表现出比纯全表扫描还要糟糕的延迟。
隐式类型转换与统计信息的真空
还有个经常被忽略的细节:metadata 列通常以 JSONB 存储,查询时容易引入隐式类型转换。例如 metadata->>'year' 和整数的比较,如果 GIN 索引没建对,优化器拿不到准确的直方图,就会用默认选择率去估算。错误的基数估计会让它觉得 “过滤后没剩多少行”,于是选择先做过滤——却又因为 JSONB 操作代价高、或者没有合适的索引,最终走上慢速的全量扫描。此时向量索引完全被旁路,性能断崖式下跌也就不奇怪了。
这些根源性问题决定了,想在 PGVector 内部通过调参就让混合搜索稳定在低延迟,往往徒劳。解剖到这层,你应该能看出,问题的本质不是 PGVector 写得烂,而是多条件检索与高维索引的逻辑在数据库内核里本就存在着结构性冲突——这恰好是许多人在选型时最容易低估的物理定律。

说得对,我也遇到过这情况。
之前调IVFFlat的lists参数折腾好久,感觉原因就在这里。
JSONB那个隐式转换怎么避免啊?
但我觉得先过滤再排序也不一定慢吧,看数据分布。
之前项目用PGVector做混合查询,到了500万直接慢成狗,换了别的方案才解决,这篇文章终于让我明白为啥了。