当你需要同时找“红苹果”和“北纬51度”:数据库索引的多维宇宙生存指南

40 阅读6分钟

——那些B树解决不了的问题,后来都怎么样了


你有没有这种感觉:每次看数据库索引教程,都是从“电话簿按姓氏排序”开始讲起?

  • B树(B-Tree)
  • LSM树(Log-Structured Merge-Tree)
  • SSTable(Sorted String Table)

漂亮,优雅,时间复杂度 O(log n)。你点点头,觉得自己懂了。然后你的产品经理走过来,笑眯眯地说:

“用户想在地图上找现在开着门的、评分4.5以上、卖红苹果的店。哦对了,还要按距离排序。”

你低头看了看你刚建好的B树索引。它正无辜地看着你,像个只会按字母顺序排电话簿的实习生。

欢迎来到多维索引的世界。这里的规则不一样了,我的朋友。


一、串联索引:那个“只会一个条件”的老实人

我们先说一个悲伤的故事。

你有一个餐厅表,有纬度经度两列。你建了个索引:

CREATE INDEX idx_lat_lon ON restaurants (latitude, longitude);

这是串联索引(Concatenated Index)。它像电话簿:先按姓排,再按名排。好用,如果你知道姓。

但你的查询是:

SELECT * FROM restaurants 
WHERE latitude BETWEEN 51.49 AND 51.51
AND longitude BETWEEN -0.12 AND -0.10;

这个索引能帮忙吗?能,但只帮一半。

它可以快速定位所有纬度在51.49–51.51的行——然后呢?它得在这些行里一个个检查经度。这就像你先找出所有姓“王”的人,再挨个问他们是不是叫“王小明”。

如果经度条件是“不在北极圈内”,那效率?自己品。

结论:串联索引是一维思维的囚徒。它不知道“附近”是什么意思。


二、空间索引:给地球画框和串珠子的人

要同时处理两个维度的范围条件,你需要多维索引(Multidimensional Index)

最经典的代表是R树(R-Tree)。这玩意儿不讲线性顺序了,它讲边界框(Bounding Box)

想象你有一堆大小不一的矩形,每个矩形框住一群数据点。R树把这些矩形组织成树形结构——大框套中框,中框套小框。想查一个点?先看它在哪个大框里,然后一层层缩小范围。PostgreSQL的PostGIS模块就是这么干的,地图App搜“附近的餐厅”背后常有它。

另一种思路更像变魔术:空间填充曲线(Space-Filling Curve)

你有一张网格地图。现在拿一支笔,从左上角开始,不抬笔地走遍所有格子,一笔画出一条连续线。每经过一个格子,就给格子编个号:1、2、3、4……这条线就是空间填充曲线。

原本你要用两个数字(x, y)定位一个格子;现在你只要知道它在这条线上的编号——一个数字。二维问题,就这么被你硬生生变成了一维问题。

然后呢?你可以把这个编号塞进普通B树里,范围查询、排序,B树全包了。Google的S2库、Uber的H3网格,都是这套思路。

不同曲线走法不同。Z阶曲线(Z-order curve) 像写字母Z,跳来跳去;希尔伯特曲线(Hilbert curve) 像贪吃蛇绕圈,优点是空间上相邻的格子,编号也挨得近,查询效率更高。

“如果你不能解决多维问题,就把它变成一维问题。”
—— 某个不想重写存储引擎的人


三、全文搜索:当每一个词都是一个维度

现在我们换个人设。

你不找餐厅了,你找文档。用户输入:

“苹果 手机 2024”

你想要所有同时包含“苹果”、“手机”、“2024”的文章。

这是几个维度?词典有多大,维度就有多大。100万个词,就是100万维。

B树:???

这时候出场的叫倒排索引(Inverted Index)

它的结构极其简单:词项 → 文档列表

举个具体例子。假设有三个文档:

  • 文档1:“I love apples”
  • 文档2:“I love bananas”
  • 文档3:“Apples are tasty”

倒排索引会把这几个文档“拆词”,然后记录每个词出现在哪些文档里:

词项(Term)倒排列表(Postings List)
"i"[1, 2]
"love"[1, 2]
"apples"[1, 3]
"bananas"[2]
"are"[3]
"tasty"[3]

现在用户搜索 “apples” —— 直接拿出倒排列表 [1, 3]
搜索 “love bananas” —— 拿出 "love"[1,2]"bananas"[2],求交集,得到 [2]

如果文档ID是连续的,倒排列表还能压缩成位图(Bitmap),要查同时包含A和B的文档,加载两个位图,按位与(AND),CPU三秒钟完事。Lucene、Elasticsearch、Solr全这么干。

你以为这就完了?天真。

全文搜索引擎还要处理:

  • 拼写错误:编辑距离(Edit Distance),Levenshtein自动机
  • 词形变化:跑、跑着、跑了 → 词干提取(Stemming)
  • 子串匹配:n-gram(三元组)索引

你输入“漫威”,它给你找“Marvel”。这背后是有限状态转换器(FST),一种藏在Lucene内核里的自动机魔法。


四、向量嵌入:当计算机开始“感觉”语义

现在进入赛博朋克章节

用户不搜“苹果手机”了。他搜:

“适合拍照的轻便设备”

你的文档里根本没有“轻便”这个词。怎么办?

欢迎来到语义搜索(Semantic Search)时代。主角是向量嵌入(Vector Embedding)

你有一个模型(比如BERT、Word2Vec、GPT),它能把任何文本变成一个几百上千维的浮点数列表。这个列表不是乱写的——它代表了这段文本在“语义空间”里的位置。

“狗”和“猫”的向量很近。“狗”和“操作系统”很远。

于是查询变成了:给我找向量最接近我问题的那些文档

这不是等于号,这是相似度。用的工具是余弦相似度(Cosine Similarity)欧氏距离(Euclidean Distance)

问题是:几千维的空间里,怎么快速找“附近”的点?

  • 扁平索引(Flat Index):暴力全扫描。准确,慢。适合数据集小或钱多。
  • 倒排文件索引(IVF, Inverted File Index):先把空间聚类成质心(Centroid),只查最近几个簇。快,但可能漏。
  • 分层可导航小世界(HNSW, Hierarchical Navigable Small World):建一个多层图,上层稀疏,下层稠密。从上往下跳,像在社交网络里找大V。目前最流行。

五、所以,我们学到什么了?

B树是优秀的——在它擅长的领域。

但它不知道经纬度。不懂法语动词变位。不理解“轻便”和“便携”是近义词。

而现代应用的要求早就不是“给我键等于42的那行”了。

是“给我语义上像这个、空间上在这个框里、时间上在这个范围、而且用户大概率会点的那一堆”。

我们用来应对这些需求的工具,名字越来越长,原理越来越怪:

  • R树(R-Tree):用框套框
  • 空间填充曲线(Space-Filling Curve):用一笔画把地图串成糖葫芦
  • 倒排索引(Inverted Index):用词找文档
  • n-gram索引:用碎片拼回整体
  • HNSW(Hierarchical Navigable Small World):用图跳过空间
  • IVF(Inverted File Index):用聚类缩小范围

它们都不是B树。它们都不试图把宇宙压进一条直线。

它们承认这个世界是复杂的、多维的、充满模糊的——然后说:“没关系,我也有办法。”

这才是存储引擎的浪漫。