从数据结构理解 MySQL 联合索引
前言
索引的本质是一种通过特定数据结构来优化数据检索速度的机制。是我们开发岗接触 MySQL 最重要的概念之一,与我们的应用开发息息相关。
结合应用思考

1)在语料平台中的 Item 表中,假设我们的目标是快速搜索 key,只考虑完全匹配的情况下,如何建立索引?
- 索引的字段(业务驱动、是否要覆盖)
- 索引顺序(最左前缀匹配、高选择性或离散性)
2)考虑大批量写的情况下,怎样的索引可以减少性能损耗?
3)高选择性的字段是不是一定排最前?
1、MySQL 索引的数据结构种类
| 索引类型 | 存储引擎 | 底层数据结构 | 特点与用途 |
|---|---|---|---|
| 普通索引 / 主键 / 唯一索引 | InnoDB | B+Tree | 默认且绝对主流。聚簇索引叶子节点存数据,二级索引叶子节点存主键。 |
| 普通索引 / 主键 / 唯一索引 | MyISAM | B+Tree | 非聚簇索引,叶子节点存数据行的物理地址。 |
| 哈希索引 | Memory | Hash Table | 默认类型,等值查询极快,用于临时表和缓存。 |
| 自适应哈希索引 | InnoDB | Hash Table | 内部自动功能,自动缓存热点数据,加速等值查询。 |
| 全文索引 (FULLTEXT) | InnoDB, MyISAM | 倒排索引 | 解决 LIKE ‘%…%’ 性能问题,用于全文检索。 |
| 空间索引 (SPATIAL) | InnoDB, MyISAM | R-Tree | 专用于地理空间数据类型查询。 |
其他索引数据结构:Skip List跳表、红黑树、BitMap 位图、Trie 前缀树
2、B+树
B+Tree 是多路平衡搜索树,一种专为磁盘等外部存储设备设计的、多路的、平衡的搜索树。它的主要目标是减少磁盘 I/O 次数,从而高效地支持大规模数据的增删改查,尤其是范围查询。
https://www.cs.usfca.edu/~galles/visualization/BPlusTree.html
3、从 B+树看联合索引设计原则

最左前缀匹配原则

高选择性在前
-
快速过滤:在B+树的早期层次就能大幅缩小搜索范围
-
减少I/O:避免扫描大量无关数据页
-
提高缓存效率:集中访问少量相关页面

无序字段放后
-
空间局部性:相关数据分散存储,缓存效率低
-
时间局部性:随机访问模式,无法利用预读机制
-
结构局部性:频繁页分裂破坏树的平衡性

等值查询&范围查询顺序
- 等值条件优先:把
=、IN这类高过滤条件放在联合索引前部,尽量先缩小搜索范围。 - 范围条件靠后:
>、<、BETWEEN、LIKE 'x%'一旦命中范围,后续列通常难以继续高效利用索引。 - 排序字段紧随其后:如果需要
ORDER BY,尽量让排序字段仍处于可用前缀中,减少 filesort。
覆盖索引原则
- 尽量让查询所需字段都在二级索引中,避免回表。
- 对高频读接口,覆盖索引通常是最稳定的收益项。
- 覆盖并不等于“越多列越好”,要结合写放大和索引体积做权衡。
宽度权衡原则
- 联合索引列越多、列越宽,索引页能容纳的记录越少,树高更容易上升。
- 写入时需要维护更多索引页,页分裂与随机 I/O 成本更高。
- 建议优先放入高价值查询列,低频列通过回表获取,整体通常更划算。
4、应用
回到应用场景,语料平台上的 corpusId、lang、customize、key 这四个字段的索引顺序应该如何排?
选择性又高到低排序:key > corpusId > lang > customize(读性能最佳)
随机性字段:key > lang > customize > corpusId (写性能最差)
key 作为随机、高选择性字段,如何权衡?

可以按“读写目标优先级”做拆分:
- 读优先:
(key, corpusId, lang, customize),等值查 key 路径最短。 - 写优先:
(corpusId, lang, customize, key),把随机性更强的 key 放后,降低页分裂概率。 - 折中策略:保留一个主业务联合索引,再根据核心慢查询补一个窄索引,避免一次上太多大索引。
5、实验
1) 索引顺序:key,lang,customize,corpusId
a. 删除一个项目环境的所有语料

b. 导入 8w 条语料

2) 索引顺序:corpusId,lang,customize,key
a. 删除一个项目环境的所有语料

b. 导入 8w 条语料

实验结论
- 两组索引顺序在导入场景下表现存在差异,核心原因是随机字段位置不同。
- 当 key 前置时,写入更容易触发随机写和页分裂,批量导入成本更高。
- 当 corpusId 前置、key 后置时,写入局部性更好,更符合批量写场景。
- 这也说明“高选择性字段不一定必须放最前”,要结合查询模式与写放大综合决策。
6、BTW,简单说下语料平台写场景下的并发控制机制
问题
如何控制并发,防止修改覆盖,防止死锁?
批量新增或更新
1)方式一:ON DUPLICATE KEY UPDATE
- 适合幂等写入,语义简单,吞吐稳定。
- 需要关注唯一索引冲突路径的锁竞争,批量并发时可能出现死锁。
2)方式二:应用层 SELECT + INSERT/UPDATE
- 业务可控性更强,可插入更多校验逻辑。
- 网络往返和代码复杂度更高,并发窗口更难完全收敛。
锁控制
并发读写有 MySQL 的 MVCC 机制,写冲突需要我们处理:
1、乐观锁 单个写:版本乐观锁(CAS CompareAndSwap,无锁并发)
2、悲观锁 批量写:粒度尽量小,产品-> 项目 ->环境
死锁问题在批量并发写时无法完全避免,具体原因看这篇 MySQL 死锁问题的系统化排查与并发优化。对 corpusId 加锁可以避免修改覆盖,但不能彻底消除死锁风险。可以捕获死锁异常后快速失败并重试,重试次数设置上限。