03-MySQL索引类型及优化方案

9 阅读10分钟

本文从三个角度梳理 MySQL 索引:底层实现(B+ 树、Hash 等)、物理存储(聚簇 / 二级)、逻辑用途(主键、联合、覆盖等),并单列 常见失效场景 便于对照 SQL;最后给出设计与排查上的实践要点。默认以 InnoDB 为主(MySQL 8.x 常见默认存储引擎)。


1. 底层实现:索引「长什么样」

1.1 常见实现一览

类型典型引擎 / 场景适用查询备注
B+TreeInnoDB 默认、多数 OLTP等值、范围、排序、最左前缀本文重点
HashMEMORY;InnoDB 有自适应哈希(AHI,不可手动建)等值(=IN不支持范围扫描
全文索引InnoDB 5.6+、MyISAMMATCH ... AGAINST替代低效 LIKE '%x%'
空间索引(R-Tree)支持 GIS 的类型与函数地理范围、MBR 等POINTLINESTRING

1.2 B+Tree(InnoDB 默认)

特点简述:

  • 非叶子节点只存键,不存完整行数据,扇出大,树高通常 2~4 层即可支撑大量数据。
  • 叶子节点存数据(聚簇索引存整行;二级索引存主键),叶子间双向链表,便于范围查询与顺序扫描。
  • 从根到叶路径长度一致,单次查找的 IO 次数约等于树高,延迟相对稳定。

为什么不用二叉树 / 红黑树?
数据量大时树高过高,磁盘以页为单位随机读,树越高 IO 越多。B+ 树是多路平衡树,单层能挂更多子节点,从而降低树高。

为什么 InnoDB 常用 B+ 树而不是 B 树?
B 树非叶子也会带数据记录,单页能存的键变少,扇出下降、树变高;范围扫描时 B 树需要在不同层间跳转,而 B+ 树只需沿叶子链表扫描,更友好。


1.3 Hash 索引

原理: 对索引列求哈希,映射到桶;桶内再精确比较。

特点:

  • 只擅长 等值=IN),不支持范围>BETWEENORDER BY 利用索引排序等场景受限)。
  • 无法按前缀匹配联合索引中「部分列」的典型用法(与 B+ 树最左前缀不同维度的限制)。
  • 哈希冲突多时,桶内扫描变多,性能会下降。

适用: 以键值精确查找为主的场景(如 MEMORY 引擎缓存表);InnoDB 的 AHI 由引擎根据热点自动维护,不能当普通 Hash 索引来设计。


1.4 全文索引

用于文本关键词检索,底层多为倒排索引结构。

  • InnoDBMySQL 5.6 起支持全文索引,语法为 MATCH(col1,...) AGAINST('keyword' IN NATURAL LANGUAGE MODE) 等。
  • 相比 LIKE '%keyword%'(无法走普通 B+ 树前缀),全文索引在大量文本检索上通常更高效;支持自然语言、布尔模式等。

1.5 空间索引(R-Tree)

用于 GEOMETRY 等空间类型,结构多为 R 树,配合 MBRContains 等空间函数做范围查询。


2. 物理存储:数据与索引怎么放

2.1 聚簇索引(Clustered Index)

InnoDB 中,表数据按主键顺序存放在 B+ 树的叶子节点,这张「以主键为键的 B+ 树」就是聚簇索引

聚簇索引是谁?(优先级)

  1. 有显式主键 → 主键即聚簇索引。
  2. 无主键 → 选第一个非空唯一约束对应的列。
  3. 以上都没有 → InnoDB 内部生成 隐藏主键 row_id 作为聚簇索引。

意义: 按主键访问、主键范围扫描时,少一次「指针跳转」,与数据存储天然一致。


2.2 二级索引(Secondary Index)

定义: 除聚簇索引以外的索引(唯一索引、普通索引、联合索引等)。

结构: 叶子节点存 主键值,不存完整行(与 MyISAM 不同)。

回表(Bookmark Lookup):
先在二级索引上找到 主键,再到聚簇索引 B+ 树上按主键取整行。

示例:t 主键 id,二级索引 name

SELECT * FROM t WHERE name = 'Tom';

执行顺序可理解为:在 name 索引上找到 id = 100 → 再到主键索引读取 id = 100 的完整行。

若查询列全部在二级索引内(含主键若二级索引叶子已含),则可能不回表,见下文「覆盖索引」。


2.3 InnoDB 与 MyISAM 对比(理解「回表」)

项目InnoDBMyISAM
数据与索引聚簇:数据在主键索引叶子数据在 .MYD,索引在 .MYI
主键索引叶子完整行行在数据文件中的物理地址
二级索引叶子主键值 → 需回表到聚簇索引物理地址 → 通常不回表(直接读堆文件)
事务 / 行锁支持不支持(表锁为主)

生产环境新建表一般优先 InnoDB;了解 MyISAM 有助于读老文档或对比「为什么 InnoDB 总说回表」。


3. 逻辑分类:业务上怎么建索引

3.1 主键索引(PRIMARY KEY)

  • 唯一非空,一张表只有一个。
  • 在 InnoDB 中即 聚簇索引
  • 语法:PRIMARY KEY (col)

3.2 唯一索引(UNIQUE)

  • 列值必须唯一;NULL 是否允许多个 NULL 与版本、引擎有关,设计时建议查官方文档并显式 NOT NULL 若业务不允许空。
  • 语法:UNIQUE INDEX idx_name (col)

3.3 普通索引(INDEX)

  • 无唯一约束,仅加速查询。
  • 语法:INDEX idx_name (col)

3.4 联合索引(复合索引)

多列组成一个 B+ 树键,语法示例:

INDEX idx_multi (a, b, c)

最左前缀原则: 条件从最左列连续匹配时,才能尽量用上该索引;跳过左侧列则右侧列通常无法单独走该索引。

示例条件能否较好利用 (a,b,c)
WHERE a = 1能(用到 a
WHERE a = 1 AND b = 2能(a,b
WHERE a = 1 AND b = 2 AND c = 3能(a,b,c
WHERE a = 1 AND c = 3通常只用 ac 无法跳过 b 使用该复合索引
WHERE b = 2WHERE b = 2 AND c = 3不符合最左前缀,一般不走该联合索引

范围条件后的列: 例如 WHERE a = 1 AND b > 2 AND c = 3,常见情况下 **ab 可走索引,b 为范围后 c 往往无法再用于索引内过滤**(以 EXPLAIN 为准)。

排序: ORDER BY a, b 常与 (a,b,...) 方向一致时可减少 filesort;ORDER BY a, c(跳过 b)则往往难以完全利用索引有序性。

设计提示: 区分度高的列、过滤性强的条件尽量靠前;在列顺序允许的前提下,让高频查询形成覆盖索引可减少回表。


3.5 前缀索引

只对字符串前 n 个字符建索引,节省空间:

INDEX idx_name (col(10))

缺点: 不利于 ORDER BY / GROUP BY 完全依赖该索引;覆盖能力也受限。
长度 n: 可用选择性估算,例如 COUNT(DISTINCT LEFT(col, n)) / COUNT(*),越接近全列选择性越好,再结合长度与存储折中。


3.6 覆盖索引(Covering,概念而非独立索引类型)

SELECT 的列与 WHERE 用到的列都包含在某个索引中,优化器可能只读索引树不回表EXPLAINExtra 常出现 **Using index**。

示例: 联合索引 (name, age)

SELECT name, age FROM t WHERE name = 'Tom';

nameage 均在索引中即可覆盖;若 SELECT * 则多半要回表取其余列。


4. 常见失效场景

下列情况容易导致无法按预期走索引、或只用到索引的一部分;是否全表扫描以 **EXPLAIN / EXPLAIN ANALYZE** 为准,优化器版本与成本估算不同,结论可能变化。

4.1 对索引列做「计算」或函数包装

索引按列的原始值组织;对列套函数、运算后,条件往往无法与 B+ 树键直接匹配。

典型写法说明与规避
WHERE YEAR(create_time) = 2024无法走 create_time 上普通索引的常见形式;改为范围create_time >= '2024-01-01' AND create_time < '2025-01-01'
WHERE id + 1 = 100列参与运算;改为 WHERE id = 99
WHERE UPPER(name) = 'TOM'若必须大小写不敏感,可考虑统一存放大写函数索引(MySQL 8.0+)或生成列 + 索引

4.2 隐式类型转换

比较两侧类型不一致时,MySQL 可能把列转成另一侧的类型,导致无法使用该列上的索引(常见于字符串列与数字比较)。

典型情况说明与规避
phoneVARCHAR,写 WHERE phone = 13800138000可能触发对 phone 的转换;改为字符串字面量WHERE phone = '13800138000'
连接条件两侧类型、**排序规则(collation)**不一致可能无法利用索引;建表与 JOIN 条件保持同类型、同 collation

4.3 LIKE 与前缀模糊

写法说明
LIKE 'abc%'通常可走前缀扫描(列上有合适 B+ 树索引时)。
LIKE '%abc'LIKE '%abc%'无法用普通 B+ 树做「整列有序」定位,易退化为大范围扫描;大文本检索考虑全文索引搜索引擎

4.4 OR<>NOT IN 等条件

  • **OR 两侧若不能同时用索引(不同列、或一侧无索引),优化器常选合并多次范围直接全表**;可改为 **UNION ALL 拆两条各自走索引,或在业务允许下改写为 **IN(同列多值)
  • **!=<>NOT IN** 往往对应大范围区间扫描或全表;有时可用 **IN 正向枚举**、标记位、或反范式冗余减轻(需权衡一致性)。
  • 部分场景下优化器会使用 **index_merge**(多索引合并),不一定更优,仍需看执行计划与耗时。

4.5 联合索引与最左前缀

WHERE b = 2 而索引为 (a,b,c)跳过最左列的情况,通常不能利用该联合索引;前文 3.4 节已展开,设计时优先把高选择性、等值条件放在左侧。


4.6 优化器「认为」全表更便宜

即使 SQL 形式「可索引」,仍可能不走索引,例如:

  • 表很小:全表读几页即可完成,索引回表反而更贵。
  • 范围条件过宽:如时间跨度极大,type=range 扫描行数接近全表时,可能选 ALL
  • 统计信息不准ANALYZE TABLE、检查直方图(高版本)、大变更后更新统计

4.7 其他易忽略点

场景说明
索引列隐式参与字符集/排序规则转换连接或 WHERE 中与字面量/另一列 collation 不一致时,可能无法走索引。
**ORDER BY 与索引顺序不一致**如降序/多列顺序与索引定义不一致,可能额外排序filesort),不等同于「WHERE 不走索引」,但影响性能。
非最左前缀、前缀索引前缀索引无法用于部分 GROUP BY / ORDER BY;见 3.5 节

排查时建议:固定一条慢 SQL → 看 EXPLAINtype、key、rows、Extra → 对照上表逐项排除,再决定改 SQL、改索引或改统计信息。


5. 最佳实践与排查

方向建议
主键尽量短、有序(如整型自增或有序 UUID),减少二级索引叶子主键占用与页分裂随机性。
建索引高频、高选择性条件建索引;避免低区分度列单独建索引。
联合索引合并单列索引为少数联合索引,遵守最左前缀,避免冗余索引。
覆盖与回表在业务允许下,用覆盖索引减少回表;避免 SELECT * 成习惯。
表达式避免对索引列套函数、隐式类型转换、运算(如 WHERE YEAR(col)=2024),易导致索引失效。
数量与维护索引会拖慢写入、占空间;单表索引数不宜盲目堆叠(常见经验值 5 个左右量级需结合表与写入压测)。
验证变更后用 **EXPLAIN / EXPLAIN ANALYZE(视版本)看是否走预期索引;结合慢查询日志**持续治理。