MySQL 索引 (Index) 是一种特殊的数据结构,存储着表中一列或多列数据的排序值和指向对应数据行的物理指针。它的主要目的是提高数据库查询的效率。通过创建索引,数据库系统可以快速定位到所需的数据行,而无需扫描整个表。然而,索引并非越多越好,不当的索引使用反而可能降低性能,因此深入理解索引的工作原理和优化策略至关重要。

核心思想:
索引的本质是一种“文件目录”或“字典检索”,它通过牺牲一定的存储空间和维护成本,来大幅提升数据检索的速度。


一、为什么需要索引?

想象一下一本没有目录的书,当你想查找某个特定主题时,你必须从头到尾翻阅整本书,效率非常低下。数据库查询也是一样。

当未对表中的列创建索引时:

  • 全表扫描 (Full Table Scan):每次查询都需要遍历表的每一行,逐一比对条件。对于小表来说可能影响不大,但对于包含数百万甚至数千万行记录的大表,全表扫描将导致灾难性的性能问题。
  • 排序性能下降:如果查询结果需要排序(ORDER BY),但没有合适的索引,数据库需要在内存或磁盘上对结果集进行排序,开销巨大。
  • 连接 (JOIN) 性能下降:多个表进行连接操作时,如果没有索引,数据库可能需要进行嵌套循环连接,导致大量的数据比对。

创建索引后:

  • 加速数据检索:索引允许数据库系统直接跳到包含所需数据的特定行,而不是扫描整个表。
  • 加速排序:如果查询的 ORDER BY 子句与索引的顺序一致,数据库可以直接读取排序后的索引,避免额外的排序操作。
  • 加速连接:在 JOIN 操作中,索引可以大大减少需要比对的数据量,提高连接效率。
  • 加速分组 (GROUP BY):与排序类似,如果 GROUP BY 子句与索引顺序一致,可以利用索引快速完成分组。
  • 唯一性约束:唯一索引 (Unique Index) 和主键索引 (Primary Key Index) 可以确保表中某列或某几列的数据不重复,提供了数据完整性保障。

二、索引类型

MySQL 支持多种索引类型,它们在数据结构和适用场景上有所不同。最常见的索引类型是 B-Tree 索引。

2.1 B-Tree (B+Tree) 索引

几乎所有的 MySQL 存储引擎(包括 InnoDB 和 MyISAM)都支持 B-Tree 索引,这也是 MySQL 中使用最广泛的索引类型。

  • 数据结构:B-Tree 索引实际上是 B+Tree 数据结构。B+Tree 是一种多路平衡查找树,所有叶子节点构成一个有序链表,且只存储行记录指针,非叶子节点只存储键值和子节点指针。这使得范围查询和全表遍历非常高效。
  • 适用场景
    • 全值匹配WHERE column = 'value'
    • 精确匹配某一列WHERE column1 = 'value1' AND column2 = 'value2'
    • 最左前缀匹配:对于复合索引 (column1, column2, column3),可以使用 WHERE column1 = 'value1'WHERE column1 = 'value1' AND column2 = 'value2',但不能单独使用 WHERE column2 = 'value2'
    • 范围查询WHERE column > 'value'WHERE column BETWEEN 'value1' AND 'value2'
    • 排序ORDER BY column ASC/DESC
    • 前缀匹配WHERE column LIKE 'prefix%' (不能是 '%prefix%')
  • 特性
    • 平衡性:B+Tree 总是保持平衡,从根节点到任何叶子节点的高度都相同。这意味着所有数据行的查找时间复杂度大致相同。
    • 所有数据都在叶子节点:所有记录的指针(对于非聚集索引)或完整数据行(对于聚集索引)都存储在叶子节点。
    • 叶子节点链表:叶子节点之间通过指针连接,形成一个有序链表,这对于范围查询非常友好,可以直接遍历叶子节点。

2.2 Hash 索引

  • 数据结构:基于哈希表实现,为键值计算哈希码,并存储哈希码和数据行指针。
  • 适用场景
    • 精确查找WHERE column = 'value'。查找速度非常快,近似 O(1)。
  • 不适用场景
    • 范围查询:不支持范围查询,因为哈希值是无序的。
    • 排序:不支持排序。
    • 最左前缀匹配:不支持。
  • 特性:只有 Memory 存储引擎支持显式创建 Hash 索引。InnoDB 存储引擎有自适应哈希索引 (Adaptive Hash Index),可以根据需要自动创建,无需手动干预。

2.3 全文索引 (Full-Text Index)

  • 数据结构:倒排索引 (Inverted Index)。它将非结构化的文本数据中的每个词与其在文档中的位置关联起来。
  • 适用场景
    • 文本搜索,例如 MATCH (column) AGAINST ('keyword'),用于在大文本字段中进行关键字搜索。
  • 特性:适用于 CHAR, VARCHAR, TEXT 类型字段。MySQL 5.6 以后 InnoDB 也支持全文索引。

2.4 空间索引 (Spatial Index)

  • 数据结构:R-Tree。
  • 适用场景
    • 地理空间数据类型(GEOMETRY, POINT, LINESTRING, POLYGON)的查询,例如查找某个区域内的点。
  • 特性:需要使用 MyISAM 存储引擎,并且字段必须非空。

三、聚集索引 (Clustered Index) 与 非聚集索引 (Secondary Index)

这是理解 InnoDB 存储引擎索引的关键。MyISAM 存储引擎不支持聚集索引。

3.1 聚集索引 (Clustered Index)

  • 定义:数据行物理存储的顺序与索引键值的顺序一致。一张表只能有一个聚集索引。
  • InnoDB 实现
    • 如果表定义了 PRIMARY KEY,则 PRIMARY KEY 就是聚集索引。
    • 如果表没有定义 PRIMARY KEY,InnoDB 会选择第一个非空的 UNIQUE 索引作为聚集索引。
    • 如果两者都没有,InnoDB 会隐式定义一个 6 字节的 ROWID 作为聚集索引。
  • 特性
    • 数据和索引存储在一起:聚集索引的叶子节点直接包含完整的用户数据行。因此,通过聚集索引查找数据非常快,因为索引结构本身就包含了所有数据。
    • 物理有序:数据的物理存储顺序根据聚集索引的键值进行排序。
    • 查找代价低:只需一次 B+Tree 查找即可获取全部数据。
  • 缺点
    • 插入新行或更新数据时,如果引起聚集索引键的改变,可能会导致数据行物理位置的移动,开销较大。
    • 一张表只能有一个,因为数据行只能按一个顺序进行物理存储。

3.2 非聚集索引 (Secondary Index) / 辅助索引

  • 定义:除聚集索引之外的所有索引都是非聚集索引。
  • InnoDB 实现:非聚集索引的叶子节点存储的不是完整的数据行,而是索引键值与对应行的聚集索引键值 (Primary Key Value)
  • 回表 (Look Back):通过非聚集索引查找数据时,首先通过非聚集索引找到对应的聚集索引键值,然后(如果是第一次查找,或者不在覆盖索引的情况下)再使用这个聚集索引键值去聚集索引中查找并获取完整的行数据。这个过程叫做“回表”。
  • 特性
    • 可以有多个非聚集索引。
    • 查找数据需要两次索引查找(一次非聚集索引,一次聚集索引)才能获取完整数据,效率略低于聚集索引。

示意图:InnoDB 聚集索引与非聚集索引

四、如何创建和管理索引

4.1 创建索引

主键索引 (PRIMARY KEY):

1
2
3
4
5
6
7
8
9
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100),
email VARCHAR(100) UNIQUE,
age INT
);

-- 或在已有表上添加
ALTER TABLE users ADD PRIMARY KEY (id);

唯一索引 (UNIQUE INDEX): 确保列中的值是唯一的,并且允许 NULL 值(但 NULL 值本身不重复)。

1
2
3
4
5
6
7
8
CREATE UNIQUE INDEX idx_email ON users (email);

-- 或在创建表时指定
CREATE TABLE products (
id INT PRIMARY KEY AUTO_INCREMENT,
product_code VARCHAR(50) UNIQUE, -- 唯一索引
product_name VARCHAR(255)
);

普通索引 (INDEX): 最基本的索引类型,没有唯一性约束。

1
CREATE INDEX idx_name ON users (name);

复合索引 (Composite Index) / 联合索引: 在多个列上创建索引。顺序很重要。

1
CREATE INDEX idx_name_age ON users (name, age);

前缀索引 (Prefix Index): 只索引列值的前缀,适用于 VARCHAR(255) 等大字符串列,可以节省空间并提高效率,但会降低索引的选择性。

1
CREATE INDEX idx_name_prefix ON users (name(10)); -- 索引 name 列的前 10 个字符

4.2 删除索引

1
2
3
DROP INDEX idx_name ON users;
ALTER TABLE users DROP INDEX idx_email; -- 对于非主键索引
ALTER TABLE users DROP PRIMARY KEY; -- 删除主键索引 (可能要先删除 AUTO_INCREMENT 属性)

4.3 查看索引

1
2
SHOW INDEX FROM users;
DESCRIBE users; -- 也会显示索引信息

五、索引优化策略与注意事项

5.1 选择合适的列创建索引

  • WHERE 条件中经常使用的列:特别是 =INBETWEEN>< 等操作符。
  • JOIN 连接的列ON 子句中的连接条件列。
  • ORDER BY 和 GROUP BY 中的列:可以避免额外的排序或分组操作。
  • 基数 (Cardinality) 大的列:即列中重复值少的列,如 ID、Email 等。基数太小的列(如性别,只有男/女)索引效果不佳,甚至可能使得优化器选择全表扫描。
  • 小数据类型:索引列的数据类型越小,索引占用的空间越少,查询效率越高。
  • 字符串列的前缀索引:对于很长的字符串列,可以考虑创建前缀索引来节省空间。

5.2 复合索引的“最左前缀原则”

非常重要!对于 (col1, col2, col3) 这样的复合索引:

  • 可以匹配 col1
  • 可以匹配 col1col2
  • 可以匹配 col1col2col3
  • 不能直接匹配 col2col3
  • 不能匹配 col2col3

示例:INDEX (name, age, city)

  • SELECT * FROM users WHERE name = 'John':使用索引。
  • SELECT * FROM users WHERE name = 'John' AND age = 30:使用索引。
  • SELECT * FROM users WHERE name = 'John' AND age = 30 AND city = 'New York':使用索引。
  • SELECT * FROM users WHERE age = 30不使用索引
  • SELECT * FROM users WHERE name = 'John' AND city = 'New York':只使用 name 部分索引。
  • SELECT * FROM users WHERE name LIKE 'J%' AND age = 30LIKE 后缀模糊查询 (e.g., 'J%') 可以走索引,但中间 ('%J%') 不行。

5.3 避免索引失效的常见情况

  • 在索引列上进行函数操作WHERE LENGTH(name) = 5,索引失效。
  • 对索引列进行数学运算WHERE age + 1 = 30,索引失效。
  • 使用 OR 连接条件:如果 OR 连接的条件中有一个没有索引,则所有索引都将失效(除非全都是索引列,且优化器认为使用索引更优)。
  • LIKE 前缀模糊查询WHERE name LIKE '%John'LIKE '%John%',索引失效。
  • 数据类型不匹配WHERE id = '123' (ID 是 INT 类型,传入字符串),可能导致隐式类型转换,使索引失效。
  • !=NOT IN 操作符:有时可能导致索引失效。
  • MySQL 优化器判断成本:即使有索引,如果查询优化器认为全表扫描的成本更低(例如,要查询的数据占了表中绝大部分),也可能不使用索引。

5.4 覆盖索引 (Covering Index)

  • 定义:如果一个查询只需要返回索引中包含的列,而不需要“回表”去查找实际的数据行,那么这个索引就是覆盖索引。
  • 优点:显著减少 I/O 操作,提高查询性能。
  • 示例
    • INDEX (name, age)
    • SELECT name, age FROM users WHERE name = 'John':这是一个覆盖索引查询,因为 nameage 都在索引中。
    • SELECT name, age, email FROM users WHERE name = 'John':这不是覆盖索引,因为它还需要 email 列,需要回表。

5.5 单列索引与复合索引的选择

  • 单列索引:适用于查询条件只涉及一列的情况,或作为复合索引的前缀。
  • 复合索引:更适合多个列组合查询的场景,遵循最左前缀原则。要仔细分析查询模式,将最常用、区分度最高的列放在复合索引的前面。

5.6 索引不是越多越好

  • 写操作开销:每次对表进行 INSERT, UPDATE, DELETE 操作时,索引也需要更新。索引越多,更新操作的开销越大,写入性能越差。
  • 存储空间增加:索引本身是数据结构,会占用磁盘空间。
  • 查询优化器开销:当有大量索引时,查询优化器需要更多的时间来评估和选择最佳的执行计划。

5.7 使用 EXPLAIN 分析查询计划

在进行索引优化时,EXPLAIN 命令是你的最佳工具。它会显示 MySQL 如何执行查询,包括是否使用索引、使用哪个索引、扫描了多少行等信息。

1
EXPLAIN SELECT * FROM users WHERE name = 'John' AND age = 30;

关键指标:

  • type:连接类型,从最优到最差依次是 system > const > eq_ref > ref > range > index > ALL。目标是达到 refrange 以上。
  • possible_keys:可能用到的索引。
  • key:实际使用的索引。
  • key_len:使用的索引长度。
  • rows:MySQL 预估为了找到所需行而扫描的行数。越小越好。
  • Extra:额外信息。
    • Using index:表示使用了覆盖索引。
    • Using where:表示WHERE条件过滤。
    • Using filesort:表示需要额外文件排序,通常可以通过索引优化避免。
    • Using temporary:表示需要使用临时表,通常可以通过索引优化或查询重写避免。

六、总结

MySQL 索引是数据库性能优化的基石。深入理解 B+Tree 索引的工作原理、聚集索引和非聚集索引的区别,以及复合索引的最左前缀原则,是编写高性能 SQL 查询和进行数据库设计的基础。通过合理地创建和管理索引,并利用 EXPLAIN 命令进行分析和验证,可以显著提升系统的查询吞吐量和响应速度,从而保障业务的高效运行。但同时也要牢记,索引并非包治百病的灵丹妙药,过度或不恰当的索引反而可能带来负面影响。