------那些B树解决不了的问题,后来都怎么样了
你有没有这种感觉:每次看数据库索引教程,都是从"电话簿按姓氏排序"开始讲起?
- B树(B-Tree)
- LSM树(Log-Structured Merge-Tree)
- SSTable(Sorted String Table)
漂亮,优雅,时间复杂度 O(log n)。你点点头,觉得自己懂了。然后你的产品经理走过来,笑眯眯地说:
"用户想在地图上找现在开着门的、评分4.5以上、卖红苹果 的店。哦对了,还要按距离排序。"
你低头看了看你刚建好的B树索引。它正无辜地看着你,像个只会按字母顺序排电话簿的实习生。
欢迎来到多维索引的世界。这里的规则不一样了,我的朋友。
一、串联索引:那个"只会一个条件"的老实人
我们先说一个悲伤的故事。
你有一个餐厅表,有纬度和经度两列。你建了个索引:
sql
CREATE INDEX idx_lat_lon ON restaurants (latitude, longitude);
这是串联索引(Concatenated Index)。它像电话簿:先按姓排,再按名排。好用,如果你知道姓。
但你的查询是:
sql
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树。它们都不试图把宇宙压进一条直线。
它们承认这个世界是复杂的、多维的、充满模糊的------然后说:"没关系,我也有办法。"
这才是存储引擎的浪漫。