B+树与HNSW——数据库索引与向量检索的两座丰碑
一、两类检索,两套解法
做后端的同学应该都有体会:检索这件事,本质上分两种。
第一种是精确检索——"查 id = 10086 的记录"、"查工资在 1万~2万之间的员工"。结果必须一个不多、一个不少,错了就是事故。
第二种是近似检索——"找和这段文字意思最像的文档"、"找长得最像的这张脸"。结果只要**"差不多"就行**,漏掉一两个冷门候选完全可以接受,但速度必须快到毫秒级。
这两种需求,各自催生了各自的事实标准:
- 精确检索 → B+ 树(MySQL InnoDB、PostgreSQL、SQLite 的默认索引结构)
- 近似检索 → HNSW(Milvus、Weaviate、Qdrant、FAISS 的主流索引结构)
它们名字里都带个"树"或"图",但设计哲学天差地别。这篇把它们放一起掰开揉碎地对比——搞清楚它们各自解决什么问题、为什么长成这样、什么时候该用哪个。
二、B+ 树:为磁盘而生的"多路平衡树"
2.1 它长什么样
B+ 树是 B 树(Bayer & McCreight, 1972)的改良版,1973 年由 Knuth 正式命名。它的结构一句话概括:
内部节点只存"导航"信息(键 + 指针),所有数据都放在叶子节点,叶子节点之间用指针连成有序链表。
1 | ┌─────────┐ |
两个关键设计:
1. 高扇出(fan-out)压树高。 每个节点能装很多键(InnoDB 默认 16KB 一页,能装上千个键),所以树极矮——三层就能装数亿行数据。查找复杂度 O(log N),而且 log 的底数很大,实际就是"走 3~4 步"。
2. 叶子链表支撑范围查询。 要查"工资 1万~2万",找到起点后顺着链表往后扫就行,不用回溯树——这是 B+ 树相对 B 树最大的改进。
2.2 为什么它是数据库的事实标准?
| 特性 | 对数据库的意义 |
|---|---|
| 磁盘友好 | 节点大小对齐磁盘页(4KB/16KB),一次 I/O 读一整页 |
| 矮树 = 少 I/O | 3~4 次 I/O 找到任意数据,磁盘随机访问的昂贵被压到最低 |
| 有序性 | 天然支持范围查询、排序、去重、聚合(GROUP BY) |
| 平衡性 | 所有叶子同深度,最坏情况也是 O(log N),性能可预期 |
| 局部更新 | 插入/删除只需分裂/合并少量节点,写放大可控 |
B+ 树的一切设计,都是围绕一个物理现实展开的:磁盘随机读太贵了,必须让"找到数据"这件事只花几次 I/O。
三、HNSW:为内存而生的"分层小世界图"
3.1 它长什么样
HNSW(Hierarchical Navigable Small World,分层可导航小世界图)由 Malkov & Yashunin 在 2018 年提出(论文《Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs》)。它解决的问题是:在几十亿个高维向量里,找到与查询向量"最接近"的 K 个。
它的结构一句话概括:
把数据组织成多层图:底层包含所有节点,越往上节点越稀疏(顶层只有少数"枢纽"),查询从顶层开始,贪心地往"更近的邻居"跳,逐层下探。
1 | 第2层 (最稀疏) [A]──────────────────────[C] |
查询流程(比如找 q 的最近邻):
1 | 1. 从顶层入口点 A 开始,贪心移动到最近的邻居(在顶层:A → C) |
"小世界"来自社会网络研究的"六度分隔"现象:真实世界的图里,任意两点之间存在很短的路径,所以贪心跳跃能快速逼近目标,而不是遍历全图。
3.2 为什么它是向量检索的事实标准?
| 特性 | 对向量检索的意义 |
|---|---|
| 对数复杂度 | 查询路径长度 ∝ log N,十亿级数据也能毫秒级响应 |
| 内存友好 | 数据常驻内存,靠指针跳转,无磁盘 I/O |
| 召回率可控 | efSearch 参数在"速度"与"精度"之间连续调节 |
| 动态更新 | 支持增量插入,无需重建索引 |
| 近似可接受 | 检索"足够好"即可——语义相似本就没有唯一正确答案 |
HNSW 的一切设计,都是围绕另一个物理现实展开的:内存里的指针跳转几乎免费,但"高维空间的相似度"没有天然顺序,必须靠图来导航。
四、核心对比:B+ 树 vs HNSW
| 维度 | B+ 树 | HNSW |
|---|---|---|
| 本质 | 多路平衡搜索树 | 分层小世界图 |
| 查询类型 | 精确匹配、范围查询、排序 | 近似最近邻(top-K 相似) |
| 结果精度 | 精确,一个不多一个不少 | 近似,召回率 95%+(可调) |
| 复杂度 | O(log N),稳定可预期 | O(log N) 期望,但近似 |
| 存储介质 | 磁盘优先(页对齐) | 内存优先(指针跳转) |
| 范围查询 | ✅ 天生支持(叶子链表) | ❌ 不适用 |
| 排序/去重 | ✅ 天然有序 | ❌ 只返回"最像的" |
| 动态增删 | ✅ 局部分裂/合并 | ✅ 增量插入,删除较麻烦 |
| 显存/内存占用 | 低(仅键+指针) | 较高(每节点多条边,还有多层) |
| 构建成本 | 批量导入快 | 构建慢于 B+ 树,需调参 |
| 典型场景 | 数据库主键/二级索引 | 向量数据库、RAG 检索、推荐 |
| 底层数据 | 结构化标量(id、数值、字符串) | 高维向量(embedding) |
| 代表系统 | MySQL、PostgreSQL、SQLite | FAISS、Milvus、Qdrant、Weaviate |
4.1 一个最容易混淆的点
很多人以为 HNSW 能"替代"B+ 树——不能。它俩解决的问题根本不同:
1 | "工资在 1万~2万之间的员工有多少?" → B+ 树(范围查询,必须精确) |
B+ 树回答**"哪些满足条件";HNSW 回答"哪些最像"**。前者没有"像不像"的概念,后者没有"等于不等于"的概念。
4.2 但边界正在模糊
现代数据库已经在融合两者:
- PostgreSQL 的 pgvector:用 IVFFlat / HNSW 索引做向量相似度检索,跟普通 B+ 树索引共存
- MySQL 的 HeatWave / 向量扩展:在关系表上直接做 ANN 查询
- 混合检索(Hybrid Search):BM25(词法)+ 向量(语义)+ HNSW 索引,用 RRF 融合——这是 RAG 系统的标配
一个真实场景:OA 系统搜文档,"标题精确匹配"走 B+ 树索引,"内容语义相似"走 HNSW 向量索引,两路结果融合后重排。两种索引不是替代关系,是搭档关系。
五、选型决策树
判断该用哪个,三步走:
1 | 你的查询需要精确结果吗? |
5.1 工程建议
- 别用 B+ 树做语义检索——它没有"相似度"概念,只能做精确/范围匹配
- 别用 HNSW 做精确查询——它是近似算法,无法回答"等于多少"或"区间是多少"
- 长文本检索用混合:B+ 树管结构化条件过滤(部门、时间、状态),HNSW 管语义召回,最后重排
- HNSW 调参记住三个数:M(每个节点边数,默认 16~32,越大召回越高越占内存)、efConstruction(构建质量)、efSearch(查询精度旋钮)
- 数据量小到几千条就别上 HNSW——暴力扫描就够快,构建索引的开销反而不值
六、总结:一个关于"导航"的故事
把两种索引放到一起看,会发现它们其实是同一道题的两种解法:
如何在浩如烟海的数据里,快速到达"我要的那一片"?
B+ 树的答案:用有序性导航——数据本身有顺序(数字大小、字典序),把顺序组织成矮树,每次排除一半,3~4 步到达。
HNSW 的答案:用相似性导航——数据本身没有顺序(高维向量无从比较大小),但存在"远近",把远近组织成多层图,从粗到细贪心下探,log N 步到达。
一个靠序,一个靠近。一个是磁盘时代的纪念碑,一个是 AI 时代的里程碑。理解它们的区别,你就理解了后端检索的半壁江山。
工程师视角的一句话总结:B+ 树问"它在哪一排",HNSW 问"它离谁最近"——前者管精确,后者管相似;现代系统从不二选一,而是让它们各司其职,把答案拼出来。

