170530-MySQL索引总结

MySQL 索引完全指南

目录


一、索引概述

1.1 什么是索引

索引是帮助 MySQL 高效获取数据的数据结构。可以把索引理解为书的目录,通过目录可以快速定位到需要的章节,而无需翻阅整本书。

核心要点

  • 索引是一种数据结构(通常是 B+Tree)
  • 索引存储在磁盘上
  • 索引以空间换时间,提高查询效率

1.2 索引的作用

主要优势

  1. 大幅提升查询速度:将全表扫描 O(n) 优化为树查找 O(log n)
  2. 降低 IO 成本:减少磁盘读取次数
  3. 排序优化:利用索引的有序性避免 filesort
  4. 分组优化:加速 GROUP BY 操作

性能对比示例

1
2
3
4
100 万条数据查询:
- 无索引:全表扫描,约需 1-2 秒
- 有索引:B+Tree 查找,约需 0.001 秒
- 性能提升:1000 倍以上

1.3 索引的优缺点

优点

  • 大幅提高查询效率
  • 加速排序和分组操作
  • 保证数据唯一性(唯一索引)

缺点

  • 占用额外存储空间
  • 降低写操作性能(INSERT/UPDATE/DELETE 需维护索引)
  • 索引过多会影响优化器选择

权衡原则

  • 读多写少的表适合建索引
  • 频繁更新的字段谨慎建索引
  • 区分度低的字段(如性别)不适合建索引

二、索引分类体系

索引可以从多个维度进行分类,理解这些分类有助于我们更好地设计和使用索引。

2.1 按数据结构分类

索引类型 说明 适用场景 MySQL 支持
B+Tree 平衡多路搜索树 范围查询、排序、分组 ✅ InnoDB/MyISAM 默认
Hash 哈希表 等值查询 ✅ Memory 引擎
Full-Text 全文索引 文本搜索 ✅ MyISAM/InnoDB 5.6+
R-Tree 空间索引 地理空间数据 ✅ MyISAM

重点说明

  • MySQL InnoDB 和 MyISAM 引擎默认使用 B+Tree 索引
  • Hash 索引仅支持精确匹配,不支持范围查询
  • 全文索引用于大文本字段的关键词搜索

2.2 按物理存储分类

聚簇索引(Clustered Index)

定义:数据行存储在索引的叶子节点中,索引和数据在一起。

特点

  • ✅ InnoDB 的主键索引就是聚簇索引
  • ✅ 每个表只能有一个聚簇索引
  • ✅ 查询效率高(一次 IO 即可获取数据)
  • ❌ 插入非连续数据会导致页分裂

示意图

1
B+Tree 叶子节点 → 直接存储数据行

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

定义:叶子节点存储的是主键值,需要通过主键再次查找数据(回表)。

特点

  • ✅ 一个表可以有多个二级索引
  • ✅ 适合辅助查询条件
  • ❌ 需要”回表”操作(二次查询)

示意图

1
B+Tree 叶子节点 → 存储主键值 → 通过主键查找聚簇索引 → 获取数据行

2.3 按逻辑用途分类

1. 普通索引(Normal Index)

最基本的索引类型,没有任何约束。

1
2
3
4
5
6
7
8
9
-- 创建普通索引
CREATE INDEX idx_name ON users(name);

-- 或在建表时指定
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(50),
INDEX idx_name (name)
);

特点

  • 可以包含重复值
  • 可以包含 NULL 值
  • 一个表可以有多个普通索引

2. 唯一索引(Unique Index)

索引列的值必须唯一,但允许有空值。

1
2
3
4
5
6
7
8
9
-- 创建唯一索引
CREATE UNIQUE INDEX idx_email ON users(email);

-- 或在建表时指定
CREATE TABLE users (
id INT PRIMARY KEY,
email VARCHAR(100) UNIQUE,
UNIQUE INDEX idx_email (email)
);

特点

  • ✅ 保证数据唯一性
  • ✅ 允许 NULL 值(多个 NULL 不违反唯一性)
  • ⚠️ 插入重复值会报错

3. 主键索引(Primary Key)

特殊的唯一索引,不允许有空值。

1
2
3
4
CREATE TABLE users (
id INT PRIMARY KEY, -- 主键索引
name VARCHAR(50)
);

特点

  • ✅ 唯一且非空
  • ✅ 每个表只能有一个主键
  • ✅ InnoDB 中主键就是聚簇索引

4. 复合索引(Composite Index / 联合索引)

多个列组合成一个索引。

1
2
-- 创建复合索引
CREATE INDEX idx_name_age ON users(name, age, city);

特点

  • ✅ 遵循最左前缀原则
  • ✅ 覆盖多个查询条件时效率高
  • ⚠️ 索引列顺序很重要

最左前缀原则示例

1
2
3
4
5
6
7
8
9
10
11
12
13
-- 索引:idx_name_age (name, age, city)

-- ✅ 可以使用索引
SELECT * FROM users WHERE name = 'Tom';
SELECT * FROM users WHERE name = 'Tom' AND age = 20;
SELECT * FROM users WHERE name = 'Tom' AND age = 20 AND city = 'Beijing';

-- ❌ 不能使用索引(跳过 name)
SELECT * FROM users WHERE age = 20;
SELECT * FROM users WHERE city = 'Beijing';

-- ⚠️ 部分使用索引(只用 name)
SELECT * FROM users WHERE name = 'Tom' AND city = 'Beijing';

三、索引底层实现

3.1 Hash 索引

基于哈希表实现,通过哈希函数计算键值的哈希码。

工作原理

1
Key → Hash Function → Hash Code → Bucket → Data Pointer

优点

  • ✅ 等值查询极快:O(1) 时间复杂度
  • ✅ 实现简单

缺点

  • ❌ 不支持范围查询(>, <, BETWEEN
  • ❌ 不支持排序(ORDER BY)
  • ❌ 不支持模糊查询(LIKE)
  • ❌ 哈希冲突影响性能

适用场景

  • Memory 存储引擎
  • 只需要等值查询的场景

3.2 B-Tree 索引

B-Tree(Balance Tree)是一种自平衡的多路搜索树。

特点

  • 所有节点(包括内部节点和叶子节点)都存储数据
  • 每个节点可以有多个子节点
  • 树保持平衡,所有叶子节点在同一层

缺点

  • ❌ 非叶子节点也存储数据,导致每个节点能存储的键值较少
  • ❌ 树的高度相对较高,IO 次数较多
  • ❌ 范围查询需要遍历多个分支

3.3 B+Tree 索引(MySQL 默认)

B+Tree 是 B-Tree 的改进版本,也是 MySQL InnoDB 和 MyISAM 引擎默认使用的索引结构。

核心改进

  1. 数据只在叶子节点

    • 非叶子节点只存储索引键值和指针
    • 可以在同样大小的页中存储更多键值
    • 降低树的高度,减少 IO 次数
  2. 叶子节点形成链表

    • 所有叶子节点通过指针连接成双向链表
    • 范围查询只需找到起始节点,然后顺序遍历
    • 非常适合范围查询和排序
  3. 更高的扇出(Fan-out)

    • 假设页大小为 16KB,每个键值 8 字节,指针 6 字节
    • B+Tree 非叶子节点可存储约 1170 个键值
    • 3 层 B+Tree 可存储约 1170³ ≈ 16 亿条记录

结构示意图

1
2
3
4
5
        [Root Node]
/ | \
[Internal] [Internal] [Internal]
/ \ | / \
[Leaf] [Leaf] [Leaf] [Leaf] [Leaf] ← 双向链表连接

优势总结

  • ✅ 树的高度更低(通常 2-3 层),IO 次数少
  • ✅ 范围查询效率高(叶子节点链表)
  • ✅ 适合磁盘存储(顺序访问友好)
  • ✅ 支持排序和分组操作

3.4 为什么选择 B+Tree

对比项 B+Tree B-Tree Hash 二叉树 红黑树
范围查询 ✅ 优秀 ⚠️ 一般 ❌ 不支持 ⚠️ 一般 ⚠️ 一般
等值查询 ✅ O(log n) ✅ O(log n) ✅ O(1) ⚠️ O(n)~O(log n) ✅ O(log n)
排序支持 ✅ 优秀 ⚠️ 一般 ❌ 不支持 ❌ 不支持 ❌ 不支持
树高度 ✅ 低(2-3层) ❌ 较高 - ❌ 可能很高 ❌ 较高
IO 效率 ✅ 高 ⚠️ 一般 ⚠️ 一般 ❌ 低 ❌ 低
磁盘友好 ✅ 优秀 ⚠️ 一般 ⚠️ 一般 ❌ 差 ❌ 差

选择 B+Tree 的核心原因

  1. 降低 IO 次数:树高度低,每次查询只需 2-3 次磁盘 IO
  2. 范围查询优化:叶子节点链表使范围查询只需一次定位 + 顺序扫描
  3. 磁盘预读友好:顺序访问利用操作系统预读机制
  4. 稳定性好:自平衡特性保证性能稳定

四、InnoDB 索引实现

4.1 聚簇索引

InnoDB 的数据文件本身就是索引文件,采用聚簇索引组织数据。

特点

  • 表数据按照主键顺序存储
  • 主键索引的叶子节点存储完整的数据行
  • 如果没有显式定义主键,InnoDB 会选择第一个非空唯一索引
  • 如果都没有,InnoDB 会生成一个隐藏的 row_id 作为聚簇索引

存储结构

1
2
聚簇索引(主键索引):
B+Tree 叶子节点 → 完整数据行 (id, name, age, ...)

优势

  • ✅ 主键查询极快(一次 IO)
  • ✅ 范围查询高效(物理存储有序)
  • ✅ 排序操作无需额外排序

劣势

  • ❌ 插入非连续主键会导致页分裂
  • ❌ 更新主键代价高(需要移动数据)

4.2 二级索引

除了聚簇索引外,其他索引都是二级索引。

特点

  • 叶子节点存储的是主键值,而非数据行
  • 查询时需要”回表”:先查二级索引得到主键,再查聚簇索引得到数据

存储结构

1
2
二级索引:
B+Tree 叶子节点 → 主键值 → 回表查询聚簇索引 → 获取数据行

查询流程示例

1
2
3
4
5
6
7
8
-- 假设有二级索引 idx_name(name)
SELECT * FROM users WHERE name = 'Tom';

执行流程:
1. 在 idx_name 索引中查找 name='Tom'
2. 获取对应的主键值 id=100
3. 通过 id=100 在聚簇索引中查找
4. 返回完整数据行

回表开销

  • 每次二级索引查询都需要额外的聚簇索引查找
  • 如果需要大量回表,性能会下降
  • 可以使用覆盖索引避免回表

4.3 MyISAM 对比

MyISAM 使用非聚簇索引,索引和数据完全分离。

存储结构

1
2
3
4
5
索引文件 (.MYI):
B+Tree 叶子节点 → 数据行的物理地址

数据文件 (.MYD):
实际数据行

对比表格

特性 InnoDB MyISAM
索引类型 聚簇索引 非聚簇索引
数据存储 与主键索引一起 独立文件
主键查询 一次 IO 两次 IO
事务支持 ✅ 支持 ❌ 不支持
外键支持 ✅ 支持 ❌ 不支持
崩溃恢复 ✅ 支持 ❌ 不支持
并发性能 ✅ 行级锁 ❌ 表级锁
适用场景 OLTP 事务型 读多写少、无事务需求

建议

  • ✅ 现代应用优先使用 InnoDB
  • ❌ MyISAM 已在 MySQL 8.0 中被移除

五、索引使用原则

5.1 最左前缀原则

对于复合索引 (a, b, c),查询条件必须从最左边开始匹配。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 创建复合索引
CREATE INDEX idx_abc ON table(a, b, c);

-- ✅ 可以使用索引
WHERE a = 1
WHERE a = 1 AND b = 2
WHERE a = 1 AND b = 2 AND c = 3

-- ❌ 不能使用索引(跳过 a)
WHERE b = 2
WHERE c = 3
WHERE b = 2 AND c = 3

-- ⚠️ 部分使用索引(只用 a)
WHERE a = 1 AND c = 3 -- 只能用 a,c 无法使用

原理

  • B+Tree 先按 a 排序,a 相同再按 b 排序,b 相同再按 c 排序
  • 没有 a 的值,无法确定 b 和 c 的位置

最佳实践

  • 将区分度高的列放在前面
  • 将经常用于等值查询的列放在前面
  • 将范围查询的列放在后面

5.2 覆盖索引

查询的列都在索引中,无需回表。

示例

1
2
3
4
5
6
7
-- 假设有索引 idx_name_age(name, age)

-- ✅ 覆盖索引(不需要回表)
SELECT name, age FROM users WHERE name = 'Tom';

-- ❌ 需要回表
SELECT name, age, email FROM users WHERE name = 'Tom';

优势

  • ✅ 避免回表,减少 IO 次数
  • ✅ 显著提升查询性能

如何判断

1
2
EXPLAIN SELECT name, age FROM users WHERE name = 'Tom';
-- 查看 Extra 列:Using index 表示使用了覆盖索引

最佳实践

  • 尽量让常用查询使用覆盖索引
  • 不要在 SELECT * 时使用覆盖索引优化
  • 合理设计复合索引以支持覆盖查询

5.3 索引下推(ICP)

MySQL 5.6 引入的优化技术,在存储引擎层进行条件过滤。

工作原理

1
2
3
4
5
传统方式:
存储引擎 → 返回所有满足索引条件的行 → Server 层过滤其他条件

ICP 优化:
存储引擎 → 同时检查索引条件和其他条件 → 只返回完全匹配的行

示例

1
2
3
4
5
6
7
8
9
10
11
12
-- 假设有复合索引 idx_name_age(name, age)
SELECT * FROM users WHERE name LIKE '张%' AND age > 20;

-- ICP 优化前:
-- 1. 索引查找 name LIKE '张%' 的所有记录
-- 2. 回表获取完整数据
-- 3. Server 层过滤 age > 20

-- ICP 优化后:
-- 1. 索引查找 name LIKE '张%' 的记录
-- 2. 在存储引擎层同时检查 age > 20
-- 3. 只回表获取完全匹配的记录

优势

  • ✅ 减少回表次数
  • ✅ 减少网络传输
  • ✅ 提升查询性能

启用方式

1
2
-- 默认启用
SET optimizer_switch = 'index_condition_pushdown=on';

六、索引失效场景

6.1 常见失效情况

1. 违背最左前缀原则

1
2
3
4
5
6
7
-- 索引:idx_abc(a, b, c)

-- ❌ 失效:跳过 a
SELECT * FROM table WHERE b = 1;

-- ❌ 失效:跳过 a 和 b
SELECT * FROM table WHERE c = 1;

2. 在索引列上进行计算或函数操作

1
2
3
4
5
6
7
8
9
-- 索引:idx_create_time(create_time)

-- ❌ 失效:对索引列使用函数
SELECT * FROM table WHERE YEAR(create_time) = 2024;

-- ✅ 正确:使用范围查询
SELECT * FROM table
WHERE create_time >= '2024-01-01'
AND create_time < '2025-01-01';

3. 类型隐式转换

1
2
3
4
5
6
7
-- 索引:idx_phone(phone VARCHAR)

-- ❌ 失效:字符串字段用数字查询(类型转换)
SELECT * FROM users WHERE phone = 13800138000;

-- ✅ 正确:使用字符串
SELECT * FROM users WHERE phone = '13800138000';

4. LIKE 以通配符开头

1
2
3
4
5
6
7
-- 索引:idx_name(name)

-- ❌ 失效:前导通配符
SELECT * FROM users WHERE name LIKE '%Tom%';

-- ✅ 可以使用索引:后缀通配符
SELECT * FROM users WHERE name LIKE 'Tom%';

5. OR 条件中包含未索引列

1
2
3
4
5
6
7
-- 索引:idx_name(name)

-- ❌ 失效:OR 中有未索引列
SELECT * FROM users WHERE name = 'Tom' OR age = 20;

-- ✅ 可以使用索引:OR 两边都有索引
SELECT * FROM users WHERE name = 'Tom' OR email = 'tom@example.com';

6. NOT、!=、<> 操作符

1
2
3
4
5
6
7
8
9
10
-- 索引:idx_status(status)

-- ❌ 通常失效:不等于
SELECT * FROM users WHERE status != 1;

-- ❌ 通常失效:NOT IN
SELECT * FROM users WHERE status NOT IN (1, 2);

-- ✅ 可以使用索引:IN
SELECT * FROM users WHERE status IN (1, 2);

注意:MySQL 优化器在某些情况下仍会使用索引,需通过 EXPLAIN 确认。

7. IS NULL / IS NOT NULL

1
2
3
4
5
6
7
-- 索引:idx_name(name)

-- ⚠️ 可能失效:取决于数据分布
SELECT * FROM users WHERE name IS NULL;

-- ✅ 通常可以使用索引
SELECT * FROM users WHERE name IS NOT NULL;

8. ORDER BY 与索引顺序不一致

1
2
3
4
5
6
7
-- 索引:idx_name_age(name, age)

-- ❌ 失效:排序方向不一致
SELECT * FROM users ORDER BY name ASC, age DESC;

-- ✅ 可以使用索引:排序方向一致
SELECT * FROM users ORDER BY name ASC, age ASC;

6.2 如何避免

最佳实践

  1. 使用 EXPLAIN 分析

    1
    2
    EXPLAIN SELECT * FROM users WHERE name = 'Tom';
    -- 关注 type、key、rows、Extra 字段
  2. 避免在索引列上使用函数

    1
    2
    3
    4
    5
    6
    -- ❌ 不好
    WHERE DATE(create_time) = '2024-01-01'

    -- ✅ 好
    WHERE create_time >= '2024-01-01 00:00:00'
    AND create_time < '2024-01-02 00:00:00'
  3. 注意数据类型匹配

    1
    2
    3
    -- 确保查询值类型与字段类型一致
    WHERE varchar_column = 'string_value' -- 不是 123
    WHERE int_column = 123 -- 不是 '123'
  4. 合理使用通配符

    1
    2
    3
    -- 尽量避免前导通配符
    WHERE name LIKE 'Tom%' -- ✅ 可以用索引
    WHERE name LIKE '%Tom' -- ❌ 不能用索引
  5. 优化 OR 条件

    1
    2
    3
    4
    -- 改写为 UNION
    SELECT * FROM users WHERE name = 'Tom'
    UNION
    SELECT * FROM users WHERE age = 20;

七、最佳实践

7.1 为什么使用自增主键

结合 B+Tree 的特点,自增主键有以下优势:

优势 1:减少页分裂

1
2
3
4
5
自增主键插入:
Page1 [1,2,3] → Page2 [4,5,6] → Page3 [7,8,9] ← 顺序插入,极少分裂

随机主键插入:
Page1 [3,7,9] → Page2 [1,5,8] → Page3 [2,4,6] ← 随机插入,频繁分裂

优势 2:减少数据移动

  • 自增主键总是追加到末尾
  • 不需要移动已有数据
  • 提高插入性能

优势 3:提高缓存命中率

  • 顺序插入使数据在磁盘上连续存储
  • 利用操作系统预读机制
  • 提高缓冲池命中率

对比实验

1
2
3
4
5
6
7
-- 测试 100 万条数据插入性能

-- 自增主键:约 30 秒
CREATE TABLE t1 (id INT AUTO_INCREMENT PRIMARY KEY, data VARCHAR(100));

-- UUID 主键:约 120 秒(慢 4 倍)
CREATE TABLE t2 (id VARCHAR(36) PRIMARY KEY, data VARCHAR(100));

建议

  • ✅ 优先使用自增整数作为主键
  • ⚠️ 分布式系统可使用雪花算法生成递增 ID
  • ❌ 避免使用 UUID 作为主键(除非有特殊需求)

7.2 索引设计建议

应该建索引的场景

  1. 高频查询字段

    1
    2
    -- WHERE、JOIN、ORDER BY、GROUP BY 中的字段
    CREATE INDEX idx_email ON users(email);
  2. 区分度高的字段

    1
    2
    3
    -- 区分度 = 不同值数量 / 总记录数
    -- 手机号、身份证、邮箱等区分度高
    -- 性别、状态等区分度低,不适合建索引
  3. 外键字段

    1
    2
    -- JOIN 操作的关联字段
    CREATE INDEX idx_order_user_id ON orders(user_id);
  4. 排序和分组字段

    1
    2
    -- ORDER BY、GROUP BY 中的字段
    CREATE INDEX idx_create_time ON orders(create_time);

不应该建索引的场景

  1. 区分度低的字段

    1
    2
    -- 性别只有 2 个值,区分度 50%,不适合建索引
    -- 状态字段如果只有几个值,也不适合
  2. 频繁更新的字段

    1
    2
    -- 每次更新都需要维护索引,影响性能
    UPDATE users SET login_count = login_count + 1;
  3. 大文本字段

    1
    2
    3
    -- TEXT、BLOB 类型字段不适合建完整索引
    -- 可以使用前缀索引
    CREATE INDEX idx_content_prefix ON articles(content(100));
  4. 小表

    1
    2
    -- 数据量小于 1000 行的表,全表扫描更快
    -- 索引反而增加维护成本

索引数量控制

  • 单表索引建议不超过 5 个
  • 复合索引列数建议不超过 5 个
  • 定期清理无用索引

7.3 性能调优技巧

1. 使用 Explain 分析执行计划

1
EXPLAIN SELECT * FROM users WHERE name = 'Tom' AND age > 20;

关键字段说明

字段 含义 理想值
type 访问类型 system > const > eq_ref > ref > range > index > ALL
key 实际使用的索引 非 NULL
rows 扫描行数 越少越好
Extra 额外信息 Using index > Using where > Using temporary > Using filesort

2. 优化慢查询

1
2
3
4
5
6
-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1; -- 超过 1 秒的查询

-- 分析慢查询
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log

3. 监控索引使用情况

1
2
3
4
5
6
7
8
-- 查看索引统计信息
SHOW INDEX FROM users;

-- 查看未使用的索引(MySQL 5.7+)
SELECT * FROM sys.schema_unused_indexes;

-- 删除未使用的索引
DROP INDEX idx_unused ON users;

4. 定期维护索引

1
2
3
4
5
6
7
8
-- 分析表,更新统计信息
ANALYZE TABLE users;

-- 优化表,整理碎片
OPTIMIZE TABLE users;

-- 检查表完整性
CHECK TABLE users;

5. 批量插入优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 关闭自动提交
SET autocommit = 0;

-- 批量插入
INSERT INTO users (name, age) VALUES
('Tom', 20), ('Jerry', 22), ('Mike', 25);

-- 提交事务
COMMIT;

-- 或者使用 LOAD DATA
LOAD DATA INFILE '/path/to/data.csv'
INTO TABLE users
FIELDS TERMINATED BY ',';

八、总结

核心要点回顾

  1. 索引本质:帮助 MySQL 高效获取数据的数据结构(通常是 B+Tree)

  2. 索引分类

    • 按结构:B+Tree、Hash、Full-Text、R-Tree
    • 按存储:聚簇索引、二级索引
    • 按用途:普通索引、唯一索引、主键索引、复合索引
  3. B+Tree 优势

    • 树高度低(2-3 层),IO 次数少
    • 叶子节点链表,范围查询高效
    • 磁盘友好,适合数据库场景
  4. InnoDB 特性

    • 聚簇索引:主键索引叶子节点存储完整数据
    • 二级索引:叶子节点存储主键值,需要回表
    • 推荐使用自增主键
  5. 使用原则

    • 遵循最左前缀原则
    • 善用覆盖索引避免回表
    • 了解索引失效场景
  6. 最佳实践

    • 使用 EXPLAIN 分析查询
    • 合理设计索引(区分度、选择性)
    • 定期维护和优化

学习路线建议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
入门阶段:

理解索引概念和作用

掌握 B+Tree 基本原理

进阶阶段:

深入理解聚簇索引和二级索引

掌握最左前缀、覆盖索引等高级特性

熟练阶段:

能够使用 EXPLAIN 分析执行计划

具备索引设计和优化能力

专家阶段:

理解 InnoDB 内部实现细节

能够解决复杂性能问题

参考资料

#
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×