个人主页: > for_ever_love__ <(欢迎各位大佬莅临😊)
其他栏目: > 大模型开发从0到1 <
其他栏目: > iOS项目总结大全 <
其他栏目: > 我想学python了 <
其他栏目: > iOS UI <
文章目录
- [MySQL 索引类型全解:B+Tree / Hash / R-Tree / 全文 的底层差异,以及一次性建对的选型表](#MySQL 索引类型全解:B+Tree / Hash / R-Tree / 全文 的底层差异,以及一次性建对的选型表)
-
- 一、先把三件事分开:类型、方法、存储形态
- [二、按数据结构分:MySQL 到底支持几种树](#二、按数据结构分:MySQL 到底支持几种树)
-
- [2.1 B+Tree:InnoDB 的主力,也是绝大多数场景的答案](#2.1 B+Tree:InnoDB 的主力,也是绝大多数场景的答案)
- [2.2 InnoDB 与 MyISAM 的叶子节点完全不同](#2.2 InnoDB 与 MyISAM 的叶子节点完全不同)
- [2.3 Hash 索引:MySQL 里你基本创建不了](#2.3 Hash 索引:MySQL 里你基本创建不了)
- [2.4 R-Tree:只服务于空间数据](#2.4 R-Tree:只服务于空间数据)
- [2.5 FULLTEXT:其实是倒排索引](#2.5 FULLTEXT:其实是倒排索引)
- 三、按逻辑角色分:一张表看懂全部
-
- [3.1 主键索引:InnoDB 表的"底座"](#3.1 主键索引:InnoDB 表的"底座")
- [3.2 唯一索引:既是加速,也是约束](#3.2 唯一索引:既是加速,也是约束)
- [3.3 组合索引:最左前缀是唯一铁律](#3.3 组合索引:最左前缀是唯一铁律)
- [3.4 前缀索引:省钱但不是免费的](#3.4 前缀索引:省钱但不是免费的)
- [3.5 函数索引:8.0 给的好东西](#3.5 函数索引:8.0 给的好东西)
- [3.6 不可见索引:灰度删索引的正确姿势](#3.6 不可见索引:灰度删索引的正确姿势)
- [四、怎么读 `SHOW INDEX` 的输出](#四、怎么读
SHOW INDEX的输出) - 五、选型决策表
- 六、三个常见误区
- 七、小结
MySQL 索引类型全解:B+Tree / Hash / R-Tree / 全文 的底层差异,以及一次性建对的选型表
"给 status 加个索引吧"------然后有人加了唯一的,有人加了普通的,有人加了前缀索引,
还有人问能不能加哈希索引。
MySQL 里"索引类型"这个词其实混着三层意思:数据结构、逻辑角色、物理形态 。
混着讲必然讲不清,本文先把这三层拆开,再逐个说明它们各自的边界、成本和坑,
最后给一张可以直接对着用的选型表。
一、先把三件事分开:类型、方法、存储形态
绝大多数讲索引的文章含糊,是因为把下面三个维度压成了一张表。它们其实互不隶属:
| 维度 | 回答什么问题 | 取值 |
|---|---|---|
| 数据结构(索引方法) | 按什么结构组织键值 | B+Tree、Hash、R-Tree、倒排(FULLTEXT) |
| 逻辑角色 | 这个索引承担什么业务职责 | 主键、唯一、普通、组合、前缀、函数、空间、全文、多值 |
| 物理形态 | 叶子节点里存的是什么 | 聚簇索引(整行)、二级索引(主键值 / 行指针) |
同一个索引,三个维度各取一个值。比如:
sql
INDEX idx_shop_status (shop_id, status) -- B+Tree / 组合索引 / 二级索引
理解这一点之后,再看 SHOW INDEX 的字段就不会懵了。
二、按数据结构分:MySQL 到底支持几种树
2.1 B+Tree:InnoDB 的主力,也是绝大多数场景的答案
InnoDB 的索引全部是 B+Tree(全文索引除外,它用倒排索引;空间索引用 R-Tree)。
为什么是 B+Tree 而不是二叉查找树、B-Tree 或红黑树?一句话:为了让一次磁盘 IO 拿到尽可能多的键,从而把树高压到极小。
InnoDB 一页 16KB,一个 B+Tree 节点就是一页。假设主键是 BIGINT(8 字节)+ 指针(6 字节),
一页能放下约 1170 个键值:
| 树高 | 非叶子节点扇出 | 可容纳记录数(估算) |
|---|---|---|
| 2 层 | 1170 | 约 1170 行 |
| 3 层 | 1170 | 约 136 万行 |
| 4 层 | 1170 | 约 16 亿行 |
也就是说 2000 万行的表,索引树通常只有 3 层 ,定位一行最多 3 次 IO,
而且非叶子节点几乎恒定在缓冲池里,实际往往只需 0~1 次磁盘 IO。这就是 B+Tree 的全部理由。
B+Tree 相比 B-Tree 的关键差异:所有数据在叶子节点,叶子之间用双向链表相连 。
于是范围扫描变成了"定位起点 + 沿链表扫",不需要回溯上层节点------这对 BETWEEN、>、分页查询是决定性的。
2.2 InnoDB 与 MyISAM 的叶子节点完全不同
这是面试和排障都爱问的点:
| 引擎 | 主键索引叶子存什么 | 二级索引叶子存什么 |
|---|---|---|
| InnoDB | 整行数据(聚簇索引) | 主键值 |
| MyISAM | 数据文件的行指针(偏移地址) | 同样的行指针 |
MyISAM 里主键索引和二级索引没有本质区别,都不聚簇。
而 InnoDB 里二级索引查到 PK 值后,还要回主键树再查一次------这就是"回表"(下一篇展开)。
2.3 Hash 索引:MySQL 里你基本创建不了
容易混淆的事实,一次性说清:
| 说法 | 真假 | 说明 |
|---|---|---|
| "给 InnoDB 建个 HASH 索引" | ❌ | InnoDB 不支持显式创建 Hash 索引,写了也会被忽略或报错 |
| "MEMORY 引擎可以用 Hash" | ✅ | CREATE INDEX ... USING HASH,仅 MEMORY / NDB |
| "InnoDB 内部有 Hash" | ✅ | 自适应哈希索引(AHI),自动维护,不可手动创建 |
sql
-- 只有 MEMORY 表才能显式指定 HASH
CREATE TABLE t_hash (id INT, name VARCHAR(20)) ENGINE=MEMORY;
CREATE INDEX idx_name USING HASH ON t_hash (name);
自适应哈希索引(Adaptive Hash Index) 是 InnoDB 自己干的:
当它发现某些索引前缀被频繁以等值方式访问时,会在内存中为这些页建一个哈希索引,
把 B+Tree 的 O(log n) 变成 O(1)。相关参数:
sql
SHOW VARIABLES LIKE 'innodb_adaptive_hash_index'; -- 开关
SHOW VARIABLES LIKE 'innodb_adaptive_hash_index_parts'; -- 分区数,高并发下可调大减小锁竞争
Hash 的天然局限(这也是它做不了通用索引的原因):
| 局限 | 后果 |
|---|---|
只支持等值 =、IN、<=> |
不支持范围查询、不支持 ORDER BY |
| 哈希值大小与键值大小无关 | 无法用于排序 |
| 组合索引是整体算哈希 | 不满足最左前缀,部分列查询用不上 |
| 存在哈希冲突 | 命中后仍需回表比对,不能避免表访问 |
⚠️ 顺带纠正一个高频误解:MySQL 8.0.18 引入的 Hash Join 是一种 JOIN 执行算法 ,
跟"Hash 索引"没有任何关系。它是优化器在两张表都没合适索引时用来加速连接的手段。
2.4 R-Tree:只服务于空间数据
空间索引必须用 R-Tree,列必须是几何类型(GEOMETRY/POINT/LINESTRING/POLYGON 等)并且 NOT NULL。
sql
CREATE TABLE shop (
id BIGINT PRIMARY KEY,
loc POINT NOT NULL /*!80003 SRID 4326 */,
SPATIAL KEY idx_loc (loc)
) ENGINE=InnoDB;
SELECT id FROM shop
WHERE ST_Within(loc, ST_GeomFromText('POLYGON((...))', 4326));
MySQL 5.7 起 InnoDB 支持空间索引(早年只有 MyISAM)。
如果你只是"按距离排序找附近的店",朴素的 ST_Distance 是不走索引的,
常规解法是先用矩形范围粗筛再精算,或者干脆上 Redis Geo / ES。
2.5 FULLTEXT:其实是倒排索引
全文索引不是 B+Tree,它在底层用倒排索引(auxiliary table)实现,SHOW INDEX 里 Index_type 会显示 FULLTEXT。
sql
ALTER TABLE article ADD FULLTEXT ft_title_body (title, body) WITH PARSER ngram;
SELECT * FROM article WHERE MATCH(title, body) AGAINST('索引优化' IN BOOLEAN MODE);
约束:只支持 CHAR / VARCHAR / TEXT 列 ;InnoDB 从 5.6 起支持。
中文分词要用 ngram(内置)或 MeCab(日文,也可配中文)。全文索引细节另有专篇,这里不展开。
三、按逻辑角色分:一张表看懂全部
| 角色 | 关键字 | 约束 | 能否多个 NULL | 典型用途 |
|---|---|---|---|---|
| 主键索引 | PRIMARY KEY |
唯一 + 非空 | 不可 | 行的唯一标识,InnoDB 下同时是聚簇索引 |
| 唯一索引 | UNIQUE |
唯一 | 可以 | 业务唯一性约束(手机号、订单号) |
| 普通索引 | INDEX / KEY |
无 | 可以 | 纯加速查询 |
| 组合索引 | INDEX (a,b,c) |
无 | 可以 | 多条件联合查询 + 覆盖扫描 |
| 前缀索引 | INDEX (col(n)) |
无 | 可以 | 长字符串列省空间 |
| 函数索引 | INDEX ((func(col))) |
无 | 可以 | 8.0.13+,表达式作为键的一部分 |
| 空间索引 | SPATIAL |
列 NOT NULL | 不可 | 地理位置查询 |
| 全文索引 | FULLTEXT |
仅字符类型 | 不可 | 文本检索 |
| 多值索引 | INDEX ((CAST(j->'$.k' AS ... ARRAY))) |
仅 JSON 数组 | --- | 8.0.17+ ,MEMBER OF / JSON_OVERLAPS |
| 降序索引 | INDEX (a DESC, b ASC) |
无 | 可以 | 8.0+,5.7 会解析但忽略 DESC |
| 不可见索引 | INVISIBLE |
无 | 可以 | 8.0+,让优化器假装它不存在 |
3.1 主键索引:InnoDB 表的"底座"
sql
CREATE TABLE t (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
PRIMARY KEY (id)
) ENGINE=InnoDB;
ALTER TABLE t ADD PRIMARY KEY (id); -- 后加,注意会重建表
几个必须知道的事实:
- 没有显式主键时 ,InnoDB 会选第一个非空的唯一索引做聚簇索引;再没有就生成一个 6 字节的隐藏
ROWID - 二级索引叶子存的是主键值 ,所以主键越大,所有二级索引都越大。这就是为什么主键建议用
BIGINT UNSIGNED而不是 UUID 字符串(主键选型另有专篇) - 主键只能在表上有一个,因为它定义了数据的物理组织顺序
3.2 唯一索引:既是加速,也是约束
sql
ALTER TABLE member ADD UNIQUE KEY uk_phone (phone);
CREATE UNIQUE INDEX uk_order_no ON orders (order_no);
唯一索引用错的两个典型:
- 以为它能防重复 NULL:不能,多个 NULL 完全共存(详见 NULL 那篇 2.5 节)
- 拿它当防重工具却忽略了冲突成本:插入时的唯一性校验是必须做的读,高并发下这就是热点。幂等场景很多团队最终改成了"业务幂等表 + 普通索引"
另外,外键依赖的列必须有索引,InnoDB 会优先复用唯一索引满足这个要求。
3.3 组合索引:最左前缀是唯一铁律
sql
ALTER TABLE orders ADD INDEX idx_shop_status_time (shop_id, status, created_at);
能命中 shop_id、(shop_id, status)、(shop_id, status, created_at) 三种前缀;
单独查 status 或 created_at 走不了这个索引的索引查找 (可能退化成 Using skip scan,但那是优化器的兜底策略,性能远不如真正的索引查找)。
列顺序的排布原则:
| 考量 | 规则 |
|---|---|
| 等值列 vs 范围列 | 等值列放前面,范围列放最后(范围列后面的索引列无法继续用于过滤) |
| 区分度 | 区分度高的优先靠前(注意这是经验值,最终还是要靠 EXPLAIN 验证) |
| 排序需求 | 若查询有固定 ORDER BY a, b,直接建成 (a, b) 省掉排序 |
| 复用性 | (a,b,c) 可以服务 a 和 (a,b),不必再单独建 a |
3.4 前缀索引:省钱但不是免费的
sql
ALTER TABLE user ADD INDEX idx_email (email(16));
好处:索引体积骤降。代价有三条:
- 不能用于覆盖扫描------截断后的前缀无法还原原值,所以必然回表
- 无法用于 ORDER BY / GROUP BY------这两个操作需要完整列值保证顺序正确
- 区分度会下降,要挑合适的长度
选长度的标准做法:
sql
SELECT
COUNT(DISTINCT email) AS full_cnt,
COUNT(DISTINCT LEFT(email, 12)) AS p12,
COUNT(DISTINCT LEFT(email, 16)) AS p16,
COUNT(DISTINCT LEFT(email, 20)) AS p20,
COUNT(*) AS total
FROM user;
-- 挑和 full_cnt 接近、再往上加长度收益很小的那个
关于前缀长度的上限:MySQL 8.0 默认 DYNAMIC 行格式下单列前缀最多 3072 字节 (utf8mb4 下约 768 个字符),
老版本 COMPACT/REDUNDANT 是 767 字节。TEXT / BLOB 建索引必须指定前缀长度,否则报错。
3.5 函数索引:8.0 给的好东西
MySQL 5.7 时代"对列做函数就走不了索引"只能靠冗余列或者虚拟列解决。8.0.13 起可以直接建:
sql
-- 8.0.13+:把表达式写进索引(注意双重括号)
CREATE INDEX idx_month ON orders ((MONTH(created_at)));
CREATE INDEX idx_lower_name ON member ((LOWER(nickname)));
SELECT * FROM orders WHERE MONTH(created_at) = 6; -- 可以走 idx_month
仍然要记:写对了 functional index 也要 SQL 里原样写表达式 ,优化器不会替你做代数等价变换。
比如 WHERE MONTH(created_at) = 6 和 WHERE DATE_FORMAT(created_at,'%m') = '06' 不等价识别。
3.6 不可见索引:灰度删索引的正确姿势
sql
ALTER TABLE t ALTER INDEX idx_old INVISIBLE; -- 优化器看不见它,但它仍在被维护
-- 观察一周慢查询没变化,再真的删掉
ALTER TABLE t DROP INDEX idx_old;
这解决了运维上一个真实痛点:删索引很容易,加回来很贵(大表重建要几小时) 。
不可见索引给了你一条零成本的回滚路径。MySQL 5.7 及更早没有这个能力。
四、怎么读 SHOW INDEX 的输出
sql
SHOW INDEX FROM orders;
| 字段 | 含义 | 看这个字段要判断什么 |
|---|---|---|
Non_unique |
0 唯一 / 1 不唯一 | 是不是唯一约束 |
Key_name |
索引名(PRIMARY 即主键) |
索引叫啥 |
Seq_in_index |
列在索引中的序号,从 1 开始 | 组合索引的列顺序,别猜,看这里 |
Column_name |
列名 | --- |
Collation |
A 升序 / D 降序 / NULL 无 |
8.0 起 D 才有效(降序索引) |
Cardinality |
估算的基数 | 区分度,越低越可能被优化器放弃(详见统计信息那篇) |
Sub_part |
前缀长度字节数(整列则为 NULL) | 是不是前缀索引,长度多少 |
Null |
是否含空值 | 允许 NULL 跟很多查询坑相关 |
Index_type |
BTREE / FULLTEXT / HASH / RTREE |
数据结构 |
Visible |
8.0+,YES / NO | 是否被优化器忽略 |
一句话技巧:判断组合索引用没用满,看 EXPLAIN 的 key_len ,而不是只看 key。
key_len 显示了实际用掉了多少字节,能精确到第几列。
五、选型决策表
| 你的需求 | 该选什么 |
|---|---|
| 行的唯一标识 | PRIMARY KEY,BIGINT UNSIGNED AUTO_INCREMENT |
| 业务唯一性(手机号/工号/订单号) | UNIQUE(注意多个 NULL 不冲突) |
| 高频等值单列查询 | 普通 B+Tree 索引 |
| 多条件 AND 组合查询 | 组合索引,等值列在前、范围列在后 |
| 查询只要这几个列 | 组合索引做到覆盖索引,避免回表 |
| 长 VARCHAR 只用于等值匹配 | 前缀索引,但要确认区分度、放弃覆盖扫描 |
WHERE func(col) = ? 且 8.0.13+ |
函数索引(5.7 用虚拟生成列 + 索引) |
ORDER BY a DESC, b ASC 固定 |
降序索引(8.0+) |
| JSON 数组里的成员判断 | 多值索引(8.0.17+) |
| 文本关键词搜索 | FULLTEXT + ngram(别用 LIKE '%x%') |
| 经纬度范围查询 | SPATIAL 空间索引,或外接 Redis Geo / ES |
| 不确定某个索引还有没有用 | 先 INVISIBLE 观察,删之前留退路 |
| 低区分度列(性别、是否删除) | 单独建索引通常没用,放进组合索引末尾,或者干脆不建 |
六、三个常见误区
误区 1:低区分度列不能建索引
------不绝对。单列索引确实没用(命中行数超过约 20%~30% 时优化器宁愿全表扫),
但作为组合索引的最后一列配合覆盖扫描 非常有用。典型例子:INDEX(status, is_deleted) 配合 COUNT(*)。
误区 2:索引越多越好
------每条二级索引都是一棵独立的 B+Tree,代价是三重的:磁盘空间、写放大(每次 DML 要同步维护所有索引)、优化器选择成本 。
一张 20 个索引的表,插入一条记录要写 21 棵树。线上见到"为了保险多建几个"的表,删掉冗余索引往往比优化 SQL 收益更大。
误区 3:Using index、Using filesort 跟索引类型有关
------无关。它们分别反映的是"是否走了覆盖扫描"和"是否需要额外排序",
属于执行计划层面的信息。真正决定它们的是索引的列组合是否覆盖了查询所需,而不是索引是哪种类型。
七、小结
- 索引有三层维度:数据结构 (B+Tree / Hash / R-Tree / 倒排)、逻辑角色 (主键 / 唯一 / 组合 / 前缀 / 函数 / 全文 / 空间 / 多值)、物理形态(聚簇 / 二级)
- B+Tree 是 InnoDB 的默认答案:16KB 一页、扇出约 1170,2000 万行树高仅 3 层;叶子双向链表成就了高效范围扫描
- InnoDB 二级索引叶子存主键值,MyISAM 存行指针------这是"回表"的根源,也是主键要尽量小的原因
- Hash 索引你基本用不上:只有 MEMORY 能显式建;InnoDB 的 AHI 自动维护、不可手动干预;8.0 的 Hash Join 与它无关
- R-Tree 只服务几何类型 且列必须 NOT NULL;FULLTEXT 是倒排索引,仅支持字符类型,中文要用 ngram
- MySQL 8.0 相比 5.7 新增的关键能力:降序索引(5.7 会忽略 DESC)、函数索引(8.0.13)、多值索引(8.0.17)、不可见索引
- 前缀索引 省空间但牺牲覆盖扫描和排序能力,长度用
COUNT(DISTINCT LEFT(...))实测来定 - 判断组合索引用了几个列,看
key_len而不是key;删索引前先INVISIBLE留退路 - 索引不是越多越好:每多一条索引,就多一份写放大和优化器选择成本