Mem0 于 2026 年 9 月 28 日更新复盘:两个 HNSW 索引在 1153 GB 表上「零命中」,删掉后写入快 99 倍,检索降到约 110 毫秒。 这条经验落在「前置部署」Forward Deployed Engineer(FDE)的诊断段:先看向量索引有没有被「查询规划器」用上,再决定要不要动存储,而不是一上来就迁库。对在客户现场做「企业 RAG enterprise RAG」交付的「前置部署」Forward Deployed Engineer(FDE)来说,向量库选型与「数据库成本」是返工代价最高的一环,这篇复盘给出的是一条先验证、再迁移的顺序。 原文《How We Cut Vector Search Latency by 70x》由 Mem0 的 Aashi Dutt 撰写,2026 年 7 月 30 日发布,2026 年 9 月 28 日更新。Mem0 是给 AI Agent 提供记忆层的基础设施,一条记忆在库里是一行记录,同时存放文本、元数据与嵌入向量。Mem0 是多租户系统,每次检索在进入向量比较之前,先按组织、项目与实体标识过滤,实体可以是用户、Agent 或会话。 改造前的环境是一套标准的高配 Postgres 部署:pgvector 版本 0.8.0,集群是 AWS Aurora PostgreSQL 并且读写分离,实例是内存优化型大规格。主「记忆表」带若干张关联表的外键,表上有两个不同维度的嵌入列。关系列与高维嵌入向量并排放在同一张表里,相似度比较交给 pgvector;而这两类数据的「访问模式」本来是相反的:关系查询按列过滤、只取少数行,向量查询拿一个输入去比对上百万个高维点。 这张表已经长到一个真实的「量级」:表数据本身 115 GB,索引 86 GB,TOAST「外部存储」952 GB,合计 1153 GB。TOAST 是 Postgres 存放放不进行内的超大值的地方,高维向量正属于这一类。接近 1 TB 的这部分,装的就是嵌入向量的「溢出存储」。 按每一个 pgvector 教程都会推荐的做法,Mem0 在两个向量列上建了 HNSW 索引。几个月后去查「索引利用率」,两个向量索引的「扫描次数」都是零,即「零命中」。生产环境里每一次向量检索都在绕开这两个索引,退化成对 1153 GB 表的「顺序扫描」,高负载下耗时 8 至 14 秒。Mem0 把这条症状放在原文开头:一个「空闲索引」被建好之后,没有任何查询在读它。 根因不是 pgvector 的缺陷,而是 Postgres「查询规划器」在正常工作。一条查询同时包含向量相似度与高选择性过滤时,规划器要在两条路径之间选:先走 HNSW 索引再过滤结果,或者先扫出符合「过滤条件」的行再排序。它按「代价估算」来选,而一旦选择性超过某个阈值,成本判定就变成顺序扫描更便宜,此后不再使用索引。 这一类失效模式有长期公开记录:即使查询只取很少的返回条数,即使做过 REINDEX 和 VACUUM ANALYZE,即使试图用 hint 强行指定,规划器仍然退回并行顺序扫描。原文引用的两份公开材料给出了可比较的数字。一份报告显示 pgvector 0.8.0 在约一百万行的表上,KNN 查询会落到 30 至 70 秒。2026 年初的一篇 DBA 文章把机制讲得更细:当 ef_search 调得足够高,Postgres 估算索引扫描的「查询代价」高于读取每一行,同一条查询从约 2.5 毫秒跳到 365 毫秒。 Mem0 自己的触发条件是「多租户过滤」。把组织、项目与实体标识三个条件放在余弦距离计算之前,正好生成规划器处理得最差的查询形状。余弦距离算子本身也有历史包袱:在同样的查询里,它比 Euclidean 算子更容易被规划器跳过索引。 在决定迁移之前,Mem0 先做了一个实验:既然规划器根本不用这两个向量索引,那就直接删掉,看实际损失是什么。结果很直接,表的总大小从 1023 GB 降到 683 GB。 逐项拆开看:表数据从 79 GB 变成 78 GB,索引总量从 359 GB 变成 34 GB,TOAST 从 944 GB 变成 605 GB。两个向量索引独占了约 325 GB,这部分存储一直在被付费存储、备份与清理,而规划器读取它的次数是零,这就是「索引开销」的真实形态。原文给出一条可复用的经验值:HNSW 索引的体积可以做到向量数据本身的 3 至 4 倍。需要说明的是 1023 GB 与前面的 1153 GB 是不同时间点的两次读数,表在持续增长,两组数字不能当成一本连续的账。 删掉索引之后,写路径拿到最大收益,因为每次插入与更新都不再需要维护一个没人读的索引结构。插入平均耗时从 800 毫秒降到 8.12 毫秒,约 99 倍。更新平均耗时从 500 毫秒降到 13.7 毫秒,约 36 倍。查询平均耗时从 50 毫秒降到 13.4 毫秒,约 3.7 倍。 但这一步只解决了写性能与「存储膨胀」,原始问题仍然在。简单查询变快了,把耗时推到 8 至 14 秒的那个工作负载还在:全表「相似度搜索」。而现在连向量索引都没有了。 第二步是把向量与向量检索整体搬到 Turbopuffer,它的存储落在对象存储上,而不是塞进 TOAST。Postgres 保留自己擅长的部分:关系数据、事务与一致性;Turbopuffer 只接管检索里的「向量负载」,也就是相似度这一段。于是检索被重写成「两阶段查询」:第一阶段在 Turbopuffer 做「相似度搜索」,返回命中的记忆 ID;第二阶段用这些 ID 回 Postgres 取完整记录。高频查询前面再加一层缓存。 迁移本身分四步。第一步改 schema,把向量列从记忆表移除,但保留全部关系列与外键,同时加上指向 Turbopuffer 的引用 ID。第二步导出既有向量及其记忆 ID。第三步把向量带元数据批量灌进 Turbopuffer。第四步在灌入过程中做「召回校验」,确认检索质量没有退化。 搬完之后,端到端检索时间平均落在 110 至 120 毫秒,并且随着数据增长保持稳定。原文贴出的一次单品调用追踪是 142 毫秒。拿 8 至 14 秒的单次相似度搜索,对比约 110 毫秒的端到端路径,提升幅度约 70 倍。原文特别提醒口径要看清:改造前是向量相似度搜索单独耗时,改造后是含向量库搜索与 Postgres 回表取记录的端到端耗时。 拆开工作负载不是免费的,Mem0 把代价写得很清楚。第一,从一套系统变成两套系统,要监控的对象更多,故障时要推理的链路更长。第二,新增一种失败模式:一次写入要同时落到 Postgres 与 Turbopuffer,两边可能漂移。Mem0 把 Postgres 当作「事实来源」,在同一个请求路径里写记录与向量,如果向量写入失败,就做「异步对账」把向量补上。第三,两阶段读多了一次网络跳,缓存的另一部分作用就是吸收这次跳转的延迟。 第四,存储回收的容量大致抵消了专用向量库新增的开销,Mem0 说这一步接近「成本中性」。真正的代价是运维面变大,原文把这句当作整个拆分方案的价格。这一点对「前置部署」Forward Deployed Engineer(FDE)尤其重要:客户现场最缺的往往不是性能,而是第二天早上能看懂、能接手、能排障的系统。 原文给出的「迁移门槛」可以直接抄成判断规则。多数 RAG 与记忆系统索引不到 500 万份文档,在这个量级上,Postgres 上的 pgvector 在简单性与成本上很难被击败。如果还在早期,或者向量规模在 1000 万以下并且查询简单,继续留在 pgvector 是合理默认值。但一旦超过 1000 万向量,并且查询带复杂的多租户条件,这笔账就不一样了。 原文的最后一句是可执行动作,也是整篇里最值钱的一句。如果向量查询在变慢,而你信任自己建的索引,那么先去查「索引利用率」、看「扫描次数」,而不是先换索引或先换数据库。如果扫描次数是零,问题大概率不在索引质量,而在于向量根本不该继续留在「关系型数据库」里。 原文没有给出查询索引利用率的具体 SQL,这里补上 Postgres 的标准做法,方便「前置部署」Forward Deployed Engineer(FDE)到现场就能跑。在库上查 pg_stat_user_indexes 视图,按 indexrelname 分组取 idx_scan 字段,与同一张表在 pg_stat_user_tables 里的 seq_scan 字段对照,就能看出索引是不是被绕开了。判断口径是相对的而不是绝对的:idx_scan 长期为零、seq_scan 持续增长,就是本次复盘的同一形状。跑之前要注意两点:统计信息在实例重启后会清零,至少要观察一个完整的业务周期;另外只读副本上的统计与主库不一定一致,诊断要在写入侧确认。这几句是本文补充的通用做法,不属于原文内容。 适用条件需要写明,避免把结论套错场景。这套做法针对的形状是:向量与关系行放在同一张表,并且查询带高选择性的多租户过滤。单租户、查询里只有向量相似度、或者向量规模很小的系统,套用这次迁移会明显过重。Mem0 也明确说,他们没有碰 LLM 底座、没有换嵌入模型,改动全部落在存储与检索调度这一层。 回滚路径来自架构本身。Mem0 保留 Postgres 作为事实来源,向量库只作为可重建的派生索引,因此向量侧出问题时的回退动作,是用 Postgres 里的记录与嵌入重新灌一次向量库,而不是回滚数据。删索引这一步的退出成本也低:重建 HNSW 索引即可。这两条是本文据原文操作反推的退出方式,原文没有给出重建耗时与回滚演练数据。 局限要按原文承认的写。第一,这是一次单案例复盘,原文没有给出迁移前的向量条数、查询 QPS 或分位数基线,已公开的都是单次操作均值,因此 70 倍这个倍数不能直接外推到别的系统。第二,110 至 120 毫秒是端到端均值而不是分位数,原文没有给出 p95 或 p99,长尾表现未知。 第三,成本结论是「接近成本中性」,原文没有给出两边的具体金额。第四,HNSW 索引扫描次数为零的现象与 pgvector 0.8.0 绑定,原文没有验证更高版本是否已经改掉这个规划器行为。第五,迁移四步里的召回校验只说做了校验,没有给出校验集规模与通过阈值。 第六,隔离环境没有覆盖。把向量搬到托管向量库意味着数据要出自己的网络边界,这在需要 FedRAMP、ITAR 或 CMMC 的「前置部署」交付里是硬约束,而 Mem0 的场景不涉及。如果客户环境是物理隔离的,替代路径是在客户网络内自建向量库并复用同一套两阶段读法,但这会同时失去托管服务在容量与运维上的省事,原文没有评估这条路径。 还有几类场景原文没有覆盖。一是 Postgres 原生分区或部分索引能否绕开这次规划器退化,原文只提到 REINDEX、VACUUM ANALYZE 与 hint 无效。二是把向量搬出去之后,多租户权限与行级安全如何在两套系统之间保持原来的隔离语义,原文没有展开。三是嵌入维度的影响,原文只说表上有两个不同维度的嵌入列,没有给出具体维度值。四是没有对比其他向量库方案,原文只解释了为什么选择现在这一条路。 把这条复盘压缩成一句可执行的话:向量查询变慢时,先去查「索引利用率」;扫描次数为零就不要换索引,而要重新评估向量还该不该放在「关系型数据库」里。判断门槛是 1000 万向量加复杂「多租户过滤」,低于这个量级继续用 pgvector 是更省事的默认值。对「前置部署」Forward Deployed Engineer(FDE)来说,这条顺序的价值在于它把一个常见的错误动作——先换数据库——换成了一次十分钟能做完的只读检查。 向量检索迁移自检清单,逐条勾选,每一条都对应上面某一段的结论。 - FDE(「前置部署」Forward Deployed Engineer):先跑 pg_stat_user_indexes,取 idx_scan 并按 indexrelname 分组,确认向量索引是否真的被使用。 - FDE(「前置部署」Forward Deployed Engineer):对照 pg_stat_user_tables 的 seq_scan,确认「顺序扫描」是否在持续增长。 - FDE(「前置部署」Forward Deployed Engineer):记录统计信息的清零时间,至少观察一个完整业务周期再下结论。 - FDE(「前置部署」Forward Deployed Engineer):在只读副本上做诊断时,说明统计口径与主库可能不一致。 - FDE(「前置部署」Forward Deployed Engineer):确认查询形状:组织、项目、实体三个过滤条件是否都排在距离计算之前。 - FDE(「前置部署」Forward Deployed Engineer):确认距离算子口径,比较余弦与 Euclidean 两种写法在规划器上的差异。 - FDE(「前置部署」Forward Deployed Engineer):在改索引或改数据库之前,先只删掉确认零命中的向量索引,观察写路径变化。 - FDE(「前置部署」Forward Deployed Engineer):删索引前先记录表数据、索引总量与 TOAST 三项的大小,作为对照基线。 - FDE(「前置部署」Forward Deployed Engineer):用插入、更新、查询三类操作的平均耗时做前后对比,而不是只看其中一类。 - FDE(「前置部署」Forward Deployed Engineer):把存储回收量算成可报销的数字:本次是约 325 GB。 - FDE(「前置部署」Forward Deployed Engineer):确认 HNSW 索引体积与向量数据的比值,量级参考 3 至 4 倍。 - FDE(「前置部署」Forward Deployed Engineer):区分不同时间点的读数,不要把两次快照当成一本连续的账。 - FDE(「前置部署」Forward Deployed Engineer):迁移前先决定事实来源是哪一套,本次选择 Postgres。 - FDE(「前置部署」Forward Deployed Engineer):把向量写入与记录写入放在同一个请求路径里,失败时走异步对账。 - FDE(「前置部署」Forward Deployed Engineer):为两套系统之间的漂移写一条对账任务与告警阈值。 - FDE(「前置部署」Forward Deployed Engineer):为两阶段读多出来的一次网络跳准备缓存层。 - FDE(「前置部署」Forward Deployed Engineer):改造前先测量端到端检索基线,避免只测相似度搜索这一段。 - FDE(「前置部署」Forward Deployed Engineer):对外汇报倍数时写明口径:本次是单次相似度搜索对比端到端路径。 - FDE(「前置部署」Forward Deployed Engineer):评估迁移门槛:向量数是否超过 1000 万。 - FDE(「前置部署」Forward Deployed Engineer):评估查询复杂度:是否存在复杂多租户过滤,低于 1000 万且查询简单就不要迁。 - FDE(「前置部署」Forward Deployed Engineer):确认没有改动 LLM 底座与嵌入模型,把变更范围锁在存储与检索调度。 - FDE(「前置部署」Forward Deployed Engineer):为向量库准备可重建方案,保证数据能回灌。 - FDE(「前置部署」Forward Deployed Engineer):为物理隔离环境准备替代路径,托管向量库在合规场景不可用。 - FDE(「前置部署」Forward Deployed Engineer):把 p95 与 p99 补进监控,均值不能代表长尾。 - FDE(「前置部署」Forward Deployed Engineer):把校验集规模与通过阈值写进迁移验收标准。 - FDE(「前置部署」Forward Deployed Engineer):确认迁移后的权限隔离语义与迁移前一致,必要时补行级安全测试。 - FDE(「前置部署」Forward Deployed Engineer):记录嵌入维度值,方便后续更换模型时做对照。 - FDE(「前置部署」Forward Deployed Engineer):对比至少一个替代向量库方案,避免单一路径锁定。 - FDE(「前置部署」Forward Deployed Engineer):把 70 倍、99 倍、36 倍标记为单案例数字,不外推到别的系统。 - FDE(「前置部署」Forward Deployed Engineer):把这次诊断顺序写进交付手册:先查索引利用率,再考虑迁移。 原始来源: - https://mem0.ai/blog/how-we-cut-vector-search-latency-by-70x - https://github.com/pgvector/pgvector/issues - https://www.postgresql.org/docs/current/runtime-config-query.html