MySQL 索引详解:InnoDB B+ 树、聚簇索引与优化实践

MySQL 索引详解:InnoDB B+ 树、聚簇索引与优化实践
Lichermione索引用额外空间与写入维护成本换取检索速度。理解 MySQL 索引不能停留在 CREATE INDEX:还要知道数据页、B+ 树、聚簇索引、回表、覆盖索引、最左前缀和执行计划之间的关系。
1. 没有索引会怎样
1 | SELECT * FROM employee WHERE employee_no = 998877; |
没有可用索引时,优化器可能执行全表扫描。数据量增大后,需要检查的页和记录不断增加。索引把查找范围从“大量数据页”缩小到少量树节点和目标叶子页。
代价包括:
- 索引占用磁盘与缓存;
INSERT、UPDATE、DELETE要维护索引;- 索引过多增加优化器选择和运维成本;
- 不合理索引可能从未被使用。
2. 为什么数据库按页 I/O
磁盘和操作系统按块传输数据,数据库也不会每次只读一条记录。InnoDB 以页为基本管理和 I/O 单位,常见页大小为 16 KiB,并通过 Buffer Pool 缓存页。
读取一个页后,页内多条相邻记录都能被利用,这体现空间局部性。优化目标常常不是减少一次 CPU 比较,而是减少随机页访问与磁盘 I/O。
3. 为什么使用 B+ 树
B+ 树适合页式存储:
- 每个非叶节点能容纳大量键与子页指针,分支因子高;
- 树高低,查找需要的页访问少;
- 数据集中在叶子层;
- 叶子页按键值顺序相连,适合范围查询与排序扫描;
- 插入删除通过页分裂、合并和重平衡保持有序。
与二叉平衡树相比,B+ 树一个节点能使用整页容纳更多分支,显著降低树高。与 B 树相比,非叶节点不存完整行,可容纳更多目录项,叶子链也更利于范围访问。
4. 聚簇索引
InnoDB 每张表都有一个聚簇索引,叶子记录存放完整行:
- 有主键时使用主键;
- 没有主键时,选择第一个所有列均非空的唯一索引;
- 两者都没有时,生成隐藏的聚簇索引。
因此每张 InnoDB 表最好显式定义短小、稳定的主键。
所谓“数据按主键组织”不等于 SELECT * 可以省略 ORDER BY。SQL 结果顺序仍必须由 ORDER BY 保证。
5. 二级索引与回表
InnoDB 二级索引叶子记录通常保存:
1 | 二级索引键 + 主键值 |
查询二级索引未包含的列时:
- 在二级索引找到主键;
- 再用主键查聚簇索引获取完整行。
这称为回表:
1 | CREATE INDEX idx_employee_name ON employee(name); |
若索引改为:
1 | CREATE INDEX idx_employee_name_salary ON employee(name, salary); |
查询所需列都能从二级索引取得,可能形成覆盖索引,减少回表。覆盖索引不是独立索引类型,而是“某条查询被索引覆盖”的状态。
6. 联合索引与最左前缀
1 | CREATE INDEX idx_orders_customer_status_time |
B+ 树首先按 customer_id 排序,再在相同客户内按 status,最后按时间排序。通常可以支持:
1 | WHERE customer_id = ? |
单独按 status 或 created_at 查询通常不能完整利用该索引的有序前缀。
联合索引列顺序应结合:等值条件、范围条件、排序分组、选择性和覆盖需求决定,不是简单把“选择性最高”永远放第一位。
7. 范围条件与排序
1 | WHERE customer_id = 10 |
索引 (customer_id, created_at) 可以先等值定位客户,再按时间做范围扫描并天然有序。
联合索引中遇到范围条件后,后续列通常不能继续用于缩小同一段索引范围,但某些条件仍可能通过索引条件下推过滤。最终以执行计划为准。
8. 索引失效或利用不足的常见情况
- 在索引列上做函数或计算;
- 字符串列与数字参数比较,触发隐式转换;
LIKE '%keyword'使用前导通配符;- 联合索引跳过最左列;
- 返回比例太高,优化器认为全表扫描更便宜;
- 排序方向、列顺序与索引不匹配;
- 统计信息不准确;
- 使用
OR组合缺乏索引的条件。
“没有使用某个索引”不一定是失效,也可能是成本模型判断不用更便宜。
9. 创建和删除索引
1 | CREATE INDEX idx_orders_created_at ON orders(created_at); |
主键:
1 | ALTER TABLE users ADD PRIMARY KEY (id); |
修改主键对 InnoDB 可能意味着重建聚簇组织,属于高风险 DDL。
10. 前缀索引与全文索引
长字符串可以考虑前缀索引:
1 | CREATE INDEX idx_article_title_prefix ON article(title(32)); |
前缀过短选择性差,过长又增加空间。前缀索引不能覆盖完整列值,排序能力也有限。
全文检索:
1 | CREATE FULLTEXT INDEX ft_article_body ON article(title, body); |
中文分词、复杂相关性和大规模搜索通常还需评估专用搜索系统。
11. EXPLAIN
1 | EXPLAIN |
重点字段:
| 字段 | 关注点 |
|---|---|
type |
访问方式,如 const、ref、range、index、ALL |
possible_keys |
理论可选索引 |
key |
实际选择索引 |
key_len |
使用的索引键长度 |
rows |
估计扫描行数 |
filtered |
条件过滤比例估计 |
Extra |
覆盖索引、临时表、文件排序等信息 |
type=index 仍可能是全索引扫描,不等于高效。Using filesort 也不必然写磁盘,只表示使用额外排序算法。
MySQL 8.0 可使用:
1 | EXPLAIN ANALYZE SELECT ...; |
它会实际执行查询并给出估计与真实耗时/行数,不能对危险写语句随意使用。
12. 索引设计流程
- 从真实慢查询与业务访问模式出发;
- 明确过滤、连接、排序和返回列;
- 设计少量高价值联合索引;
- 使用与列类型一致的参数;
- 用
EXPLAIN/EXPLAIN ANALYZE验证; - 在真实数据分布和并发下测试;
- 观察写放大、缓存与磁盘成本;
- 定期识别重复、冗余和长期未使用索引。
13. 常见误区
- 每列都建一个单列索引;
- 认为索引越多越好;
- 把主键值设计得很长,忽略它会进入二级索引;
- 把联合索引理解为多个独立单列索引;
- 只看
key非空,不看扫描行数和回表; - 误以为覆盖索引是一种固定索引类型;
- 认为表有索引就不会锁很多行;锁范围还受实际扫描索引范围影响;
- 没有
ORDER BY却依赖索引返回顺序。
索引优化的本质是减少需要访问的页与行,同时控制维护成本。必须围绕具体查询设计,而不是围绕字段列表设计。