MySQL之索引

索引是数据库(以及搜索引擎等)中一个极其核心且重要的概念。理解索引是进行数据库性能优化的基础。


1. 索引是什么?

想象一下一本厚厚的《新华字典》。

  • 没有索引的情况 :如果你想查找一个汉字,比如"锁",你只能从第一页开始,一页一页地翻,直到找到它为止。这在计算机里叫做 全表扫描,数据量越大,查找速度越慢,效率极低。
  • 有索引的情况 :你会使用字典前面的"拼音检字表"或"部首检字表"。这个检字表就是 索引。它按照特定的顺序(拼音或部首)列出了所有汉字,并告诉你每个汉字在字典正文中的页码。你通过检字表快速定位到"锁"字,然后直接翻到那一页。这个过程非常快。

总之,数据库索引就是数据库中一张"目录"或"检字表",它存储了数据表中某一列或多列的值,并记录了这些值对应数据行的物理位置(或指针)。通过索引,数据库可以不必扫描整张表,就能快速找到所需的数据。


2. 为什么需要索引?

索引的核心目的只有一个:极大地提高数据查询的速度

索引的优点:
  1. 大大加快数据检索速度:这是索引最主要的好处。对于建立了索引的列,查询速度可以提升几个数量级(从秒级到毫秒级)。
  2. 保证数据唯一性:通过创建唯一性索引或主键索引,可以确保数据库表中某列(或几列组合)的值是唯一的。
  3. 加速表与表之间的连接 :对用于连接的列(通常是外键)创建索引,可以显著提高 JOIN 操作的效率。
  4. 减少排序和分组的时间 :如果查询中的 ORDER BYGROUP BY 子句的列上有索引,数据库可以利用索引的有序性,避免额外的排序操作,从而提高效率。
索引的缺点:

索引不是万能的,它也有明显的代价,理解这些代价是正确使用索引的前提。

  1. 占用磁盘空间:索引本身也是一个文件,需要占用物理存储空间。数据量越大,索引占用的空间也越多。
  2. 降低写操作的速度 :当对表中的数据进行增加(INSERT)、删除(DELETE)和修改(UPDATE)时,不仅要操作数据行,还要同时更新对应的索引文件,以维持索引的最新状态。这会增加写操作的耗时。写操作越频繁的表,索引的维护成本就越高。
  3. 创建和维护耗时:创建索引和后期维护(如索引重建)都需要时间。

所以, 索引是用 空间写性能 来换取 读性能


3. 索引的底层原理(为什么这么快?)

索引之所以快,是因为它使用了高效的数据结构。最经典、最常用的索引数据结构是 B+树

B+树索引

B+树是一种多路平衡查找树,是为磁盘等存储设备设计的一种数据结构。

B+树的结构特点:

  1. 多叉树结构:相比于二叉树只有两个子节点,B+树的每个节点可以有多个子节点。这使得树的"高度"非常低。例如,一个百万级数据的表,其B+树索引的高度可能只有3-4层。
  2. 所有数据都在叶子节点:非叶子节点只存储键值和指向下一层节点的指针,不存储实际的数据。所有的数据记录都存储在叶子节点中。
  3. 叶子节点形成有序链表:所有的叶子节点通过指针连接成一个有序的双向链表。
  4. 查询效率稳定:由于任何查询都必须从根节点走到叶子节点,所以查询的效率非常稳定,基本都等于树的高度。

B+树查询过程:

假设我们要在 id 列上查找值为 50 的记录。

  1. 从根节点开始,将 50 与根节点中的键值比较,确定下一步应该进入哪个子节点。
  2. 逐层向下,直到到达叶子节点。
  3. 在叶子节点中,通过二分查找等方式快速定位到 50,然后根据叶子节点中存储的指针(或直接存储的数据行),找到完整的数据记录。

为什么B+树适合做数据库索引?

  • 减少I/O次数:树的高度低意味着磁盘I/O次数少(数据库与磁盘的交互是按页进行的,每个节点通常就是一个页)。这是B+树最大的优势。
  • 范围查询效率高 :由于叶子节点是有序链表,当进行范围查询(如 WHERE id > 50)时,只需找到第一个符合条件的节点,然后沿着链表向后遍历即可,非常高效。
  • 查询稳定:每次查询的路径长度(树高)都差不多,性能稳定。

和其他数据结构对比:

  • 哈希索引:像Java的HashMap,通过哈希函数定位。优点是等值查询极快(O(1)),缺点是不支持范围查询和排序。InnoDB有一个自适应哈希索引,是系统自动优化的,用户不能手动创建。
  • 二叉树:在极端情况下会退化成链表,导致查询效率不稳定(O(n)),不适合。
  • 红黑树:虽然能保持平衡,但仍是二叉树,树高较高,I/O次数多,不适合磁盘存储。

4. 索引的类型

根据不同的维度,索引可以分为多种类型。

按数据结构/功能分类:
  1. 主键索引

    • 特点 :一种特殊的唯一索引,不允许有空值(NULL)。一张表只能有一个主键索引。
    • 作用:用于唯一标识表中的每一行数据。在InnoDB存储引擎中,主键索引也被称为"聚簇索引",表数据文件本身就是按主键索引的顺序组织的(即叶子节点直接包含了完整的数据行)。
    • 创建PRIMARY KEY (id)
  2. 唯一索引

    • 特点:索引列的值必须唯一,但允许有空值(可以有多个空值)。一张表可以有多个唯一索引。
    • 作用:确保某列的数据不会重复,同时提高查询速度。
    • 创建UNIQUE INDEX idx_email ON user(email)
  3. 普通索引

    • 特点:最基本的索引类型,没有任何限制(不要求唯一,也不要求非空)。
    • 作用:纯粹为了提高查询速度。
    • 创建INDEX idx_name ON user(name)
  4. 组合索引 / 复合索引

    • 特点:在多个列上创建一个索引。
    • 作用:用于多个列经常同时作为查询条件的场景。
    • 核心原则:最左前缀原则 。这是使用组合索引最重要的原则。
      • 例如,你创建了 INDEX idx_name_age ON user(name, age)
      • 这个索引可以用于以下查询:
        • WHERE name = '张三' (使用了索引的第一列)
        • WHERE name = '张三' AND age = 25 (使用了索引的全部列)
        • WHERE name = '张三' AND age > 25 (使用了索引的全部列)
      • 但不能 (或无法有效使用)用于以下查询:
        • WHERE age = 25 (没有使用索引的最左列 name,索引失效)
        • WHERE age = 25 AND name = '张三' (优化器可能会调整顺序,但最好按索引顺序写)
    • 创建INDEX idx_name_age ON user(name, age)
  5. 全文索引

    • 特点 :专门用于在文本内容(如文章、评论)中进行关键词搜索。它使用分词技术,而不是简单的 =LIKE 比较。
    • 作用 :替代效率低下的 LIKE '%keyword%' 查询。
    • 创建FULLTEXT INDEX idx_content ON article(content)
按物理实现分类(主要针对InnoDB):
  1. 聚簇索引

    • 定义 :数据行的物理存储顺序与索引的逻辑顺序相同。索引即数据,数据即索引
    • 特点
      • 一张表只有一个聚簇索引。
      • 通常是主键索引。如果表没有定义主键,InnoDB会选择第一个唯一非空索引作为聚簇索引。如果都没有,InnoDB会内部生成一个隐藏的6字节row_id作为聚簇索引。
      • 叶子节点直接存储了完整的数据行。
    • 优点:对于主键的排序和范围查询速度极快。
  2. 非聚簇索引 / 二级索引

    • 定义:索引的逻辑顺序与数据行的物理存储顺序不同。
    • 特点
      • 一张表可以有多个非聚簇索引。
      • 除了聚簇索引外的其他索引都是非聚簇索引。
      • 叶子节点存储的是 主键的值,而不是完整的数据行。
    • 回表 :当通过非聚簇索引查询数据时,如果需要访问的列不在该索引中,数据库会先在非聚簇索引的叶子节点中找到主键值,然后再拿着这个主键值去聚簇索引中查找完整的数据行。这个过程叫做 回表。回表是一次额外的I/O操作,会影响性能。

5. 索引的设计原则与实践

创建索引是一门艺术,不是越多越好。

什么情况下应该创建索引?
  1. 主键和外键列 :主键默认创建索引,外键列经常用于 JOIN,应该创建索引。
  2. 经常作为查询条件的列 :在 WHERE 子句中频繁使用的列。
  3. 经常需要排序的列 :在 ORDER BY 子句中频繁使用的列。
  4. 经常需要分组的列 :在 GROUP BY 子句中频繁使用的列。
什么情况下不应该创建索引?
  1. 数据量小的表:对于小表,全表扫描可能比走索引更快,因为索引查找本身也有开销。
  2. 更新非常频繁的表:写操作多的表,索引的维护成本会很高。
  3. 区分度低的列:列中只有很少的几个不同值(例如,性别列,只有'男'、'女'、'未知')。为这种列创建索引效果很差,数据库可能认为全表扫描更高效。一个经验法则是,列的唯一值数量超过总行数的20%时,索引效果才比较好。
导致索引失效的常见场景

即使创建了索引,如果SQL语句写得不规范,索引也可能失效,导致全表扫描。

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

    • WHERE SUBSTR(name, 1, 3) = '张' (索引失效)
    • WHERE id + 1 = 10 (索引失效)
    • 正确写法:将计算和函数移到等号右边,或者改写成可以应用索引的形式。
  2. 使用 !=<> 操作符

    • WHERE status != 1 (通常会导致索引失效)
  3. **使用 IS NULLIS NOT NULL

    • WHERE name IS NULL (索引可能失效,取决于数据库和版本)
  4. 使用 LIKE 以通配符开头

    • WHERE name LIKE '%三' (索引失效)
    • WHERE name LIKE '张%' (索引有效,这是范围查询)
    • 解决模糊查询问题:使用全文索引或专门的搜索引擎(如Elasticsearch)。
  5. 类型转换

    • 如果 id 是整型,WHERE id = '123' (字符串和数字比较,会发生隐式类型转换,可能导致索引失效)。
    • 正确写法 :保证比较的类型一致,WHERE id = 123
  6. 违反最左前缀原则

    • 对于组合索引 (a, b, c),查询条件中没有 a,则索引失效。
  7. 使用 OR 连接条件

    • WHERE name = '张三' OR age = 25 (如果 nameage 上没有单独的索引,或者 OR 连接的列不是同一个索引,则索引失效)。
    • 解决 方案 :使用 UNION ALL 替代 OR

总结

特性 描述
本质 一种用于快速查询的数据结构,牺牲空间和写性能换取读性能。
核心原理 主要使用 B+树,通过降低树高来减少磁盘I/O次数,实现高效查询。
主要类型 主键索引 (聚簇)、唯一索引普通索引组合索引 (最左前缀原则)、全文索引
关键概念 聚簇索引 (数据即索引)、非聚簇索引 (需回表)、回表(二次查询)。
设计原则 在查询频繁、区分度高、更新少的列上创建索引。避免在小表、更新频繁的表上建索引。
索引失效 避免在索引列上计算、使用 !=LIKE '%xx'OR 等导致索引失效的操作。

掌握索引,就掌握了数据库性能优化的钥匙。它要求开发者不仅要懂SQL,更要理解数据库的内部工作原理。