很多人刚开始接触数据库索引时,都会卡在同一个问题上:为什么所有主流关系型数据库的索引结构都选了B树和B+树这一脉?哈希表查找不是O(1)吗?二叉搜索树不是又简单又直观吗?数据库索引用B+树到底赢在哪?这个问题如果只停留在“B+树矮、B+树支持范围查询”这种背诵层面,过两天肯定忘。
我当年也是先把结论背下来应付面试,直到后来真正处理千万级表的慢查询、一条SQL从几百毫秒优化到几毫秒之后,才意识到B+树的每一个设计细节背后,都写着一笔关于磁盘IO、页存储和访问模式的现实账。这篇文章就把这笔账摊开算一遍,看完你不仅能回答面试,还能在日常建索引时多一些自己的判断。
1. 灵魂拷问:为什么不是哈希表,也不是二叉搜索树
1.1 哈希索引第一个出局
哈希表在等值查询上确实强得离谱,一个哈希函数算完,直接定位数据。数据结构课上我们都说哈希查找平均O(1),那数据库为什么不用它当默认索引?
核心原因是:数据库的查询模式根本不是只有等值查询。
SELECT * FROM users WHERE age > 25 这种范围查询是家常便饭,但哈希函数的输出是散列的,它把原本有序的key值打散成了毫无规律的桶位置。请你告诉我,age大于25的所有记录,分布在哪些哈希桶里?答案是根本不知道,只能把所有桶全扫一遍。
再往下说,ORDER BY排序也用不上哈希索引,因为索引本身不按值有序。GROUP BY分组也一样,哈希索引没法提供有序扫描。甚至对于磁盘IO来说,哈希桶和链表里的数据物理位置是随机分布的,操作系统想帮你做预读都无从下手。
MySQL的InnoDB引擎里有个“自适应哈希索引”,它会在内存里自动为高频等值查询建一层哈希加速,但这只是B+树索引之上的锦上添花,它解决的问题是“缓存里的页查找速度”,而非磁盘上的完整数据索引。所以哈希索引能做辅助,成不了主索引。
1.2 二叉搜索树:内存里的优等生,磁盘上的差生
再看二叉搜索树。如果没有磁盘这堵墙,在内存里维护一棵红黑树或者AVL树,查找、插入、删除都是O(log n),Java的TreeMap、C++的map都是这么干的。但数据库的数据量一上来,问题就绷不住了。
假设一张表有一亿条记录。一棵普通二叉平衡树的高度大约是log2(1亿),算下来在26层左右。数据库每次向下访问一个节点,理论上就要把这个节点所在的页从磁盘读进内存,那就是大约26次磁盘随机读。机械硬盘一次随机读要10毫秒上下,26次就是260毫秒起步。这个延迟放在OLTP系统里,是不可接受的。
有朋友会说:那把整棵树都缓存进内存不就行了?但一亿条记录对应的索引结构本身就很大,内存根本扛不住全部缓存。就算热数据都在内存,冷数据查询依然会被打回原形。
所以二叉搜索树不是不好,它错在高度太高,而高度就是数据库的磁盘IO次数。数据结构教科书在内存模型下追求的比较次数优化,到了磁盘模型里全都得换算成页访问次数。
