Chapter 06
自测辨析
前五章建好了"一条向量在 pgvector 里的一生"。这一章不再讲新东西——它用三层梯度的题把那条主线从你脑子里逼出来:记得住(概念)、想得通(原理)、选得对(跨章判别)。
用法:每道题先合上前面的章节,把答案写在纸上或编辑器里,再翻到文末 答案 对照。直接看答案等于把这一章当成第六遍泛读——那不会留下任何东西。判别层尤其要写完整理由,不只是选一个选项。
▸概念层(对应 01 章)
- 说出
vector、halfvec、bit三种类型的可建索引维度上限,并解释vector那个上限为什么是 2,000。 <=>、<->、<#>各算什么距离?哪个返回的是负值,为什么?- opclass 在建索引时起什么作用?查询运算符和它对不上号会怎样——报错还是变慢?
- 一句话说清
amcanorderbyop排序契约:向量索引只服务什么形状的查询? - 为 3072 维 embedding 建 HNSW 索引,写出能成功的那行 DDL。
- 向量已归一化时,
<=>和<#>的排序结果一样吗?哪个更省算力?
▸原理层(对应 02 · 03 章)
- HNSW 和 IVFFlat,哪个能在空表上建、哪个必须先灌数据?为什么 IVFFlat 不行?
- "构建悬崖"是什么?跨过
maintenance_work_mem那条线后,建索引为什么会慢 10–50 倍? - HNSW 图的搜索为什么是随机 I/O 密集?这和"图放不放得进 RAM"有什么关系?
- 默认
ef_search = 40给的召回偏低且不报警。怎样取一个精确的基准来量召回? - UPDATE / DELETE 一行后,HNSW 图里那个旧节点立刻消失了吗?查询时它怎么被处理?
- vacuum 修 HNSW 图为什么"写得重"?它修复连通性,但不做的那件事是什么?
- 高频写入的 embedding 表召回逐周下降。什么时候该
REINDEX而不是等 vacuum?
▸应用判别层(跨 01–05 章,重点)
每道题给一个真实场景。先判断根因在哪一章,再在多个方案里选一个并说清为什么其它不合适。
- 欠返 vs 腐烂:多租户 RAG,某小语种租户搜出来只有 3 条。另有一张高频 re-embedding 的表,召回逐周下降。这两个症状看起来都是"结果变少",但根因分属不同章、解法完全不同。分别指出根因与解法。
- 选哪条过滤计划:
WHERE tenant_id = 99在全表只命中 0.3%。iterative scan、partial index、B-tree 精确排序——选哪个?为什么另外两个在这个选择性下不划算? - EXPLAIN 显示 Seq Scan:列出至少三个会让 planner 放弃 HNSW 索引的原因,并各给一句诊断/修复。其中哪个根本不是 bug?
- 内存放不下图:5 千万条 1536 维向量,HNSW 图远超 RAM,p99 延迟爆炸。在"加大机器内存 / 换 halfvec / 换 pgvectorscale 的 StreamingDiskANN"里,分别解决了问题的哪一面?哪个最治本?
- 该不该离开 pgvector:团队数据已在 Postgres,2 千万向量、查询几乎都带租户过滤、写入中等。另一个团队 8 亿向量、纯向量热路径、几乎不过滤。两个团队该继续用 pgvector 还是换专用库 / pgvectorscale?把判断依据落到具体维度上。
亲手画一张图
合上整本教程,在纸上画出"一条向量在 pgvector 里的一生"——只画 6 个节点:向量列 → 排序契约 → 索引实现 → MVCC → planner 过滤 → 选型。在每条边上写一句它发生了什么。
画完回到 起点页的概念地图 对照:你画的图里,"排序契约"那个节点有没有标出"不是过滤索引"?这条边是后面三章所有事故的总根源——漏了它,说明主线还没真正立住。
答案(三层都做完再展开)
概念层
vector≤ 2,000、halfvec≤ 4,000、bit≤ 64,000。2,000 来自 8KB page:一个索引项要把向量原值连同图指针塞进单页,4 × 2000 + 开销已逼近 8KB。<=>cosine 距离、<->L2、<#>负内积。<#>返回负值:内积越大越相似要降序,但索引只能升序吐,取负让"最相似"变成"最小"。- opclass 决定索引按哪种距离建图,并让 planner 把查询运算符和索引对上号。对不上不报错,只是索引用不上、退化全表扫描(30s vs 5ms)。
- 只服务
ORDER BY <距离运算符> ... LIMIT k:按距离排序、取前 k。没有 ORDER BY、没有 LIMIT、或带 DESC,都用不上。 CREATE INDEX ON doc_chunks USING hnsw ((embedding::halfvec(3072)) halfvec_cosine_ops);- 排序结果完全一样(都是
dot的单调变换)。<#>更省:cosine 还要多算模长和除法,单位向量下是浪费。
原理层
- HNSW 能在空表上建(增量插入建图);IVFFlat 必须先有数据,因为
lists个聚类中心要靠 k-means 在已有行上算出来。 - 构建在内存里进行,直到
maintenance_work_mem填满,之后退化成逐元组的磁盘路径(日志打印 "hnsw graph no longer fits...")。同样数据、只跨了一条线,建索引时间就 10–50 倍地涨。 - 图节点的邻居指针散落在任意 8KB 页上,每跳一步就要解引用到不同页。图放得进 RAM 时这些是内存访问;放不下时每跳都可能触发 page fault,延迟塌方。
BEGIN; SET LOCAL enable_indexscan = off;跑同一条ORDER BY ... LIMIT k得到精确暴力结果作为 ground truth,再和走索引的结果算 overlap@k。- 不会立刻消失。MVCC 下它仍指向已死的堆行;查询时 pgvector 走完图、拿到候选后,逐个重检堆可见性,把已死的丢掉。图本身要等 vacuum 才清。
- 因为 vacuum 的第二趟要为每个"邻居被删了"的节点重新计算替补边(
HnswFindElementNeighbors)并改写邻居元组——这是大量随机写。它修复连通性,但不重平衡图,所以质量仍会随删除累积而漂移。 - 当死元组比例到 ~10–15%:此时 vacuum 只能修不能重建,召回已腐烂,
REINDEX INDEX CONCURRENTLY重建一张干净的图更划算。
应用判别层
- 症状一(小语种只剩 3 条)= post-filter 欠返,根因第 1 章排序契约 + 第 4 章:索引先取固定
ef_search候选、WHERE后筛,命中率低就筛没了。解法:开hnsw.iterative_scan = relaxed_order(或对该租户建 partial index)。症状二(逐周下降)= 召回腐烂,根因第 3 章:churn 在图里堆死节点。解法:REINDEX CONCURRENTLY+ 调激进 autovacuum。两者同形(结果变少)但一个在查询期、一个在索引寿命。 - B-tree 精确排序。命中仅 0.3%(极少行),精确算距离又快又准、无近似误差,planner 走 B-tree 是对的。iterative scan 会一路扫到
max_scan_tuples仍可能补不够;partial index 对 0.3% 散落的行维护不起、且若 tenant 值很多则不可扩展。 - ① 查询带
DESC(索引只升序);② 没有LIMIT(要求全量有序,扫表更便宜);③ opclass 与查询运算符不匹配;④ 0.8.0 前的成本误估在复杂查询里退回 Seq Scan;⑤ 表太小。其中⑤(小表)根本不是 bug——几百行暴力算比走图快,planner 选 Seq Scan 正确。诊断统一用EXPLAIN。 - 加内存:治标,把图勉强塞回 RAM,但 5 千万还会继续涨、成本高。换 halfvec:把图体积砍半(2 字节/分量),多数情况召回损失 <1%,能把"放得下 RAM"的天花板推高一倍。StreamingDiskANN:最治本——它把图放 SSD、只在内存留压缩副本做搜索,本就不受"图必须进 RAM"约束。规模继续涨时只有它不撞墙。
- 团队一继续 pgvector:数据已在 PG、2 千万在舒适区、查询都带关系过滤(pgvector + 关系过滤正是它的主场)、写入中等。团队二该换(pgvectorscale 或专用库):8 亿向量远超 pgvector 单机 HNSW 的舒适区、纯向量热路径让向量延迟成为瓶颈、又不靠关系过滤——这正是 StreamingDiskANN / 专用库的主场。判断维度:数据量级、是否带关系过滤、向量延迟是否是热路径、churn 率。