MySQL 索引的结构是什么?为什么不用哈希表?
一、MySQL(InnoDB)索引的核心结构:B+Tree
在 InnoDB 存储引擎中,绝大多数索引(主键索引、唯一索引、普通索引、联合索引)的底层结构都是 B+Tree。
1. B+Tree 是什么?
B+Tree 是一种“多叉平衡树”,特点:
- 每个节点可以存放大量 key + 子指针;
- 树高度一般很低(2~4 层),查找复杂度为 O(logN);
- 所有数据都在叶子节点,中间节点只存 key;
- 叶子节点之间通过双向链表相连,天然有序。
简单理解一次查询过程:
- 从根节点开始,根据 key 范围选择合适的子指针;
- 层层向下,直到叶子节点;
- 在叶子节点中找到满足条件的记录(或范围起点);
- 如需范围查询,就沿着叶子链表向后扫描。
2. 聚簇索引与二级索引
InnoDB 有两个关键概念:
2.1 聚簇索引(Clustered Index)
- 主键索引就是聚簇索引;
- B+Tree 的叶子节点存的不是“指针 + 行地址”,而是整行数据;
- 表数据按照主键顺序存放,逻辑上相当于一棵“以主键为 key 的 B+Tree”。
2.2 二级索引(Secondary Index)
- 普通索引、唯一索引等都是二级索引;
- 叶子节点存的是:索引列值 + 主键值,而不是整行;
- 查询流程:
- 在二级索引上根据索引列查到主键;
- 再回到聚簇索引上按主键查整行数据(“回表”)。
B+Tree 的这一设计决定了:
- 索引天然是“有序”的;
- 查询、排序、范围扫描可以复用同一个结构。
二、为什么不用哈希表做主力索引?
很多人会想:哈希表查找是 O(1),看起来比 O(logN) 更快,那为什么不用哈希表?
关键在于:数据库的查询需求远不止“根据主键等值查询”这么简单。
下面按几个维度拆开说。
1. 范围查询是刚需,哈希天生不擅长
日常非常常见的 SQL:
SELECT * FROM user
WHERE age BETWEEN 20 AND 30;
SELECT * FROM order
WHERE create_time >= '2025-01-01'
AND create_time < '2025-02-01';
SELECT * FROM product
WHERE price < 100;
-
B+Tree 的做法:
- 找到
age >= 20的最左记录; - 沿着叶子节点的链表顺序扫,直到
age > 30停止; - 中间是“局部顺序 I/O”,非常友好。
- 找到
-
哈希表的问题:
- 哈希之后 key 被打散到不同的桶中;
- “20~30 这一段”完全找不到连续区域;
- 若想找范围内的所有 key,等价于遍历哈希表中的大量桶 —— 性能接近全表扫描。
结论:对于范围查询这种数据库高频场景,哈希表几乎帮不上忙。
2. 排序 / 分组 / 前缀匹配都需要“有序性”
例如:
-- 排序
SELECT *
FROM order
WHERE user_id = 100
ORDER BY create_time DESC
LIMIT 20;
-- 分组
SELECT user_id, COUNT(*)
FROM order
GROUP BY user_id;
-- 前缀匹配
SELECT *
FROM user
WHERE name LIKE 'zhang%';
-
B+Tree 的优势:
- key 在树中是有序的;
ORDER BY可以直接利用索引的顺序,避免额外排序(filesort);GROUP BY可以在有序数据上顺序扫,同一 group 的 key 会聚集在一起;LIKE 'abc%'本质是一个“以'abc'为起点的范围查询”,也能用 B+Tree 做到。
-
哈希表的劣势:
- key 被 hash 成随机分布,完全没有顺序可言;
- 想排序必须把结果拎出来再排序;
- 想分组也必须扫一遍再分桶;
LIKE 'abc%'无法利用 hash 值进行范围定位,只能遍历匹配。
数据库是一个需要大量复杂查询(排序、分组、范围、前缀)的系统,
哈希表只能在“单 key 等值查询”这一个点上亮眼,而 B+Tree 则是综合能力强。
3. 联合索引与最左前缀需求,哈希搞不定
对于联合索引:
CREATE INDEX idx_user_status_time
ON t_order (user_id, status, create_time);
本质顺序是:
- 先按
user_id排序; - 同一个
user_id内再按status排序; - 再按
create_time排序。
所以以下条件都能很好利用这个索引:
WHERE user_id = ?;
WHERE user_id = ? AND status = ?;
WHERE user_id = ?
AND status = ?
AND create_time > ?;
这就是最左前缀原则。
如果用哈希表实现联合索引,一般会把多个列拼起来算一个哈希:
hash(user_id, status, create_time)
问题就来了:
- 查询
WHERE user_id = ?时:没有办法只用user_id这个前缀来利用哈希; - 查询
WHERE user_id = ? AND status = ?时也类似; - 联合 key 一旦被 hash 成一个值,前缀信息就被“揉碎”了。
而在实际业务里,“以某个字段为前缀进行查询”的需求太普遍了,
B+Tree 能天然支持,哈希表则不行。
4. 磁盘 I/O 模型下,B+Tree 更适合大数据量
数据库面对的不是仅内存环境,而是磁盘 + 大数据量。
-
B+Tree 的磁盘表现:
- 每个节点对应一个(或少数几个)磁盘页,例如 16KB;
- 一个节点可以容纳很多 key,分叉多,树高度低;
- 查找一条记录只需要访问极少的页(几次随机 I/O);
- 范围扫描是沿着叶子节点链表顺序读,一般是较好的顺序 I/O。
-
哈希表的磁盘表现:
- 桶可能分布在大量离散的磁盘页上;
- 范围查询/遍历桶会产生大量随机 I/O;
- 哈希扩容、rehash 在磁盘环境下代价极大。
在磁盘 I/O 是主要瓶颈的大数据场景中,B+Tree 的结构明显更适合做通用索引。
5. 哈希本身也有问题:冲突和扩容
-
哈希冲突:
- 不同的 key 可能落到同一个桶;
- 需要通过链表、开放寻址等方式解决;
- 冲突增多时,性能会退化,最坏几乎退化为 O(N)。
-
扩容问题:
- 当哈希表装满到一定比例,就需要扩容;
- 扩容意味着整表 rehash,把所有 key 重新分布到新的桶中;
- 对于大规模持久化数据来说,这个成本非常高。
相比之下,B+Tree 的页分裂、页合并是局部操作,开销相对温和、渐进,整体行为更稳定。
三、MySQL 其实也在用哈希,只是用得很克制
并不是 MySQL 完全不用哈希,而是各做各的事。
1. MEMORY 引擎的 HASH 索引
CREATE TABLE demo (
id INT,
name VARCHAR(20),
PRIMARY KEY (id)
) ENGINE = MEMORY;
- MEMORY 引擎默认就支持 HASH 索引;
- 表数据放在内存中,适合做临时表或缓存;
- 主打一个“等值查询快”,并不适合复杂查询。
2. InnoDB 的自适应哈希索引(Adaptive Hash Index)
InnoDB 内部会:
- 监控哪些 B+Tree 页面被频繁访问;
- 对这些“热点范围”再建立一层哈希索引;
- 让部分等值查询可以直接通过哈希命中,而不用每次都爬 B+Tree。
注意:
- 这只是对 B+Tree 的一个加速层;
- 主体结构仍然是 B+Tree;
- 是否开启/如何实现由引擎自己管理,我们不需要手动构建。
四、小结
可以用几句话直接概括:
- MySQL(InnoDB)的索引核心是 B+Tree,不是哈希表。
- 使用 B+Tree 的关键原因:
- 支持 范围查询;
- 支持 排序 / 分组 / 前缀匹配 等有序需求;
- 支持 联合索引 + 最左前缀 等复杂索引设计;
- 更适合 磁盘 I/O 模型,树高低、范围扫描友好;
- 行为稳定,不存在大规模 rehash 的问题。
- 哈希表只在 MySQL 里扮演“配角”:
- MEMORY 引擎的 hash 索引适合等值查询;
- InnoDB 的自适应哈希索引用来加速热点查询。
一句话总结:
数据库不是简单的“key → value”存取系统,而是需要大量范围、排序、分组等操作的复杂系统。
B+Tree 提供的是通用、稳定、综合能力很强的索引能力,哈希则更适合作为局部加速手段,而不是整个系统的基础索引结构。

被折叠的 条评论
为什么被折叠?



