索引

返回 数据库基础

索引是数据库在存储数据之外额外维护的数据结构,让查询从全表扫描 O(n) 降低到 O(log n),以写入/存储开销换取查询速度。


为什么用 B+ 树

数据结构查找范围查询磁盘 IO问题
哈希表O(1)❌ 不支持差(无序)无法做 >, <, BETWEEN, ORDER BY
二叉搜索树O(log n)差(树高随数据量增长,最坏退化成链表)
B 树O(log n)一般中(内节点存数据,每个节点 IO 只能查一条)
B+ 树O(log n)✅ 极佳最优(内节点只存 key,叶节点存数据+双向链表)InnoDB 的选择

B+ 树关键特性:

                   [30 | 60]                    ← 内节点,只存 key,不存数据行
                  /    |    \
          [10|20]  [40|50]  [70|80]             ← 内节点
          /                          \
[10→20→30→40→50→60→70→80→...]         ← 叶节点,存完整行/主键,双向链表连接
  • 所有数据行(或主键)在叶节点,查找路径固定为 O(log n)
  • 叶节点双向链表,支持范围扫描不需要回溯
  • 内节点只存 key,单个页能放更多 key,树高更低(通常 3~4 层可覆盖千万行)

聚簇索引 vs 非聚簇索引

聚簇索引(Clustered Index)

B+ 树叶节点直接存完整的数据行,表只有一个聚簇索引:

InnoDB:主键就是聚簇索引(无主键时用唯一非空列,再无则自动生成隐藏 row_id)
叶节点:[主键值 | 完整行数据]

主键选用自增整数的原因:新行总是追加到最右叶节点,不会引起页分裂;UUID 等随机值会导致频繁页分裂,写入性能差。

非聚簇索引(Secondary Index / 二级索引)

叶节点存的是索引列值 + 主键值,不含完整行:

idx_email 叶节点:[email 值 | user_id(主键)]

回表:通过二级索引找到主键后,再用主键去聚簇索引查完整行,需要两次 B+ 树查找。


覆盖索引

查询所需的列全部包含在索引中,无需回表:

-- 索引:idx_name_age(name, age)
-- 以下查询不需要回表(name 和 age 都在索引里)
SELECT name, age FROM users WHERE name = 'Alice';
 
-- EXPLAIN 中 Extra 列显示:Using index  ← 覆盖索引

设计高频查询时,有意识地让查询列被索引覆盖,可大幅减少 IO。


联合索引与最左前缀原则

联合索引 (a, b, c) 在 B+ 树中按 a → b → c 顺序排列:

先按 a 排序 → a 相同时按 b 排序 → 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 b = 2❌ 跳过了 a
WHERE a = 1 AND c = 3⚠️ 只用到 a(跳过 b,c 失效)
WHERE a > 1 AND b = 2⚠️ a 范围查询后,b 失效

索引下推(ICP,Index Condition Pushdown):MySQL 5.6+,对于联合索引中 b 的过滤条件,在存储引擎层过滤(而不是回表后再过滤),减少回表次数。


索引失效场景

场景示例原因
对索引列使用函数WHERE YEAR(created_at) = 2024破坏了 B+ 树的有序性,需改为 BETWEEN 范围
隐式类型转换WHERE phone = 13800138000(phone 是 VARCHAR)MySQL 对字符串做了数值转换,等同于加函数
LIKE 前缀通配符WHERE name LIKE '%Alice'无法利用 B+ 树前缀有序性
OR 连接非索引列WHERE id = 1 OR name = 'x'(name 无索引)任一条件无法用索引就全表扫描
!= / NOT IN / NOT EXISTS取反操作通常导致全表扫描(数据量小时优化器可能仍走索引)
联合索引不满足最左前缀见上表
索引列参与运算WHERE age + 1 = 18改为 WHERE age = 17

EXPLAIN 执行计划解读

EXPLAIN SELECT * FROM orders WHERE user_id = 1 AND status = 'paid';
字段重点值含义
typeALLindexrangerefeq_refconst访问方式,越靠右越好;ALL = 全表扫描
key索引名 / NULL实际使用的索引
key_len数值使用索引的字节长度(可判断联合索引用了几列)
rows数值预估扫描行数,越小越好
Extra见下额外信息,关键信号

Extra 常见值:

Extra 值含义
Using index覆盖索引,无需回表 ✅
Using where存储引擎返回后,Server 层再过滤
Using filesort需要额外排序(无法利用索引顺序),应考虑索引优化 ⚠️
Using temporary使用临时表(常见于 GROUP BYDISTINCT),需优化 ⚠️
Using index condition索引下推(ICP)
NULL(无 Extra)正常回表查询

索引设计原则

  1. 高选择性列优先建索引status(3 个值)选择性低,user_id(唯一)选择性高
  2. 频繁出现在 WHERE / JOIN ON / ORDER BY 的列考虑建索引
  3. 联合索引列顺序:等值条件列放前,范围条件列放后((a, b) where a=x AND b>y
  4. 覆盖索引优化高频查询:把 SELECT 中的列加入索引(宽索引)
  5. 索引不是越多越好:每个索引占空间,写操作需维护所有索引
  6. 避免冗余索引(a) 已存在则 (a, b) 已包含它,单独的 (a) 索引可删除
  7. 大文本列用前缀索引INDEX idx_bio(bio(20)) 只索引前 20 字符

常见索引类型(MySQL)

类型语法说明
普通索引INDEX idx_name(col)无约束,仅加速查询
唯一索引UNIQUE INDEX idx_email(email)保证唯一,允许 NULL
主键索引PRIMARY KEY (id)非空 + 唯一,聚簇索引
联合索引INDEX idx_ab(a, b)多列组合
全文索引FULLTEXT INDEX idx_ft(content)文本搜索,MATCH...AGAINST
前缀索引INDEX idx_pre(email(10))减小索引体积

相关