MySQL 索引类型全解:B+Tree / Hash / R-Tree / 全文 的底层差异,以及一次性建对的选型表

个人主页: > 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);   -- 后加,注意会重建表

几个必须知道的事实:

  1. 没有显式主键时 ,InnoDB 会选第一个非空的唯一索引做聚簇索引;再没有就生成一个 6 字节的隐藏 ROWID
  2. 二级索引叶子存的是主键值 ,所以主键越大,所有二级索引都越大。这就是为什么主键建议用 BIGINT UNSIGNED 而不是 UUID 字符串(主键选型另有专篇)
  3. 主键只能在表上有一个,因为它定义了数据的物理组织顺序

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));

好处:索引体积骤降。代价有三条:

  1. 不能用于覆盖扫描------截断后的前缀无法还原原值,所以必然回表
  2. 无法用于 ORDER BY / GROUP BY------这两个操作需要完整列值保证顺序正确
  3. 区分度会下降,要挑合适的长度

选长度的标准做法:

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 留退路
  • 索引不是越多越好:每多一条索引,就多一份写放大和优化器选择成本
相关推荐
仍然.9 小时前
Redis---缓存
数据库·redis·缓存
Aloudata9 小时前
LookML 语义模型 vs 企业级独立语义层:BI 建模语言能否承担企业语义底座?
数据库·人工智能·数据分析·数据资产·dataagent
细嗅蔷薇@10 小时前
排序和分⻚(order by 、limit)
数据库·oracle
wdfk_prog10 小时前
LWIP教程 09:从 `tcp_slowtmr()` 到 Fast Retransmit——TCP 超时重传与重复 ACK
服务器·网络·数据库·网络协议·tcp/ip
逸模10 小时前
BIM在连锁餐饮装修中的应用:不只是画三维图
大数据·数据库·人工智能·物联网·建模
梦想平凡10 小时前
棋牌游戏技术实战(五):从对局事件重新执行规则,核对棋盘和最终状态
数据库·游戏·oracle
白露与泡影10 小时前
Oracle AI Database 26ai RAC 部署步骤、关键命令与一键脚本
数据库·人工智能·oracle
天天喝旺仔11 小时前
MySQL 索引原理与慢查询优化实战:从 B+ 树到执行计划调优
数据库·sql·mysql·性能优化
liulilittle11 小时前
Linux 下 select 测试函数
linux·服务器·网络·数据库·c++·select·c