1. 索引优化基础:B+树索引原理、聚簇索引与非聚簇索引、索引类型选择、复合索引最左前缀原则

索引这东西,说白了就是数据库的「目录」。没有索引,查数据就是全表扫描——一张表几百万行,一行一行看,想想都头疼。我刚开始做项目那会儿,就吃过这个亏。一个简单的用户查询,跑了十几秒,被领导当众点名。从那以后,我养成了一个习惯:写SQL之前,先想索引。

这一章,咱们把索引的底裤扒干净。从B+树原理开始,到聚簇与非聚簇的区别,再到怎么选索引类型,最后聊聊复合索引那个最容易踩坑的「最左前缀原则」。嗯,内容不少,但都是硬货。

1.1 B+树索引原理

为什么MySQL的InnoDB引擎选B+树,而不是二叉树、红黑树或者哈希表?

原因很简单:磁盘I/O太慢了。内存里跑得飞快的算法,到了磁盘上,一次I/O就把性能打回原形。B+树的设计,就是冲着「减少磁盘I/O次数」去的。

B+树有几个关键特征:

  • 非叶子节点只存键值,不存数据。这样每个节点能存更多的键,树的高度就矮。一般3到4层就能搞定几千万条数据。
  • 叶子节点存数据,并且用链表串联。范围查询的时候,顺着链表走就行,不用回溯。
  • 所有数据都在叶子节点。查询任何一条数据,走的路径长度都一样,稳定。

举个例子。一张用户表有1000万条记录,主键ID是自增整数。B+树大概3层就能覆盖。查ID=123456这条记录,只需要3次磁盘I/O。换成二叉树?树高20多层,20多次I/O,差距就出来了。

核心结论:B+树用「矮胖」的结构,换来了稳定的查询性能。这是关系型数据库的基石。

下面这张图,是我自己画的一个B+树结构示意。你可以看到,非叶子节点只存键,叶子节点存完整数据,叶子之间用箭头串联。

B+树索引结构示意图 根节点 [50 | 150] 内节点 [20 | 35] 内节点 [80 | 120] 内节点 [180 | 220] 10 | 数据 25 | 数据 40 | 数据 70 | 数据 100 | 数据 140 | 数据 200 | 数据 图例说明: 🔵 蓝色 = 根节点,只存键值,不存数据 🟢 绿色 = 叶子节点,存完整数据行,用红色箭头串联 🟩 青色 = 内节点,起路由作用,指向下一层 查询路径:根 → 内节点 → 叶子节点,3次I/O即可定位数据

1.2 聚簇索引与非聚簇索引

这是面试高频题,也是实际开发中最容易混淆的概念。我直接说人话:

  • 聚簇索引:数据行和索引存在一起。找到索引,就找到了数据。InnoDB的主键索引就是聚簇索引。
  • 非聚簇索引:索引和数据分开存。索引里存的是「数据行的地址」或「主键值」。找到索引后,还得再查一次数据。

你想想看,聚簇索引就像一本字典的正文——拼音和释义写在一起。非聚簇索引就像字典后面的偏旁部首索引——查到页码,再翻到正文去看。

对比维度 聚簇索引 非聚簇索引
数据存储 叶子节点存完整数据行 叶子节点存主键值或行指针
数量限制 一个表只能有一个 一个表可以有多个
查询速度 主键查询极快,无需回表 可能需要回表(二次查询)
插入性能 顺序插入快,随机插入可能页分裂 相对稳定
典型引擎 InnoDB MyISAM、InnoDB的二级索引

我的经验:建表时一定要显式定义主键。如果没定义,InnoDB会偷偷选一个唯一非空列,或者自己生成一个6字节的隐藏主键。这玩意儿你控制不了,性能还差。我曾经接手过一个遗留系统,表没主键,查询慢得离谱。加上自增主键后,性能直接翻倍。

1.3 索引类型选择

MySQL里常见的索引类型有这些:

  • 主键索引:聚簇索引,唯一且非空。一个表一个。
  • 唯一索引:保证列值唯一,允许NULL。可以多个。
  • 普通索引:只加速查询,不约束唯一性。
  • 全文索引:用于大文本的模糊搜索,比如文章内容。
  • 空间索引:地理数据专用,用的少。

怎么选?我个人的习惯是:

  1. 主键用自增整数。UUID做主键?别闹。UUID是随机字符串,插入时B+树要频繁分裂,性能血崩。
  2. 业务上需要唯一的列,加唯一索引。比如手机号、身份证号。既能加速查询,又能防重复。
  3. 高频查询条件加普通索引。比如按状态、按分类查询的字段。
  4. 少用全文索引。全文索引有专门的语法(MATCH AGAINST),而且维护成本高。小项目用LIKE %keyword% 凑合,大项目上Elasticsearch。

避坑指南:我曾经在一个订单表上,给「状态」字段加了索引。结果状态只有3个值(待支付、已支付、已取消),区分度极低。MySQL优化器觉得走索引还不如全表扫描快,直接忽略索引。这就是典型的「低选择率索引」陷阱。记住:索引不是越多越好,区分度低于20%的字段,加索引意义不大。

1.4 复合索引最左前缀原则

这是索引优化里最核心、也最容易翻车的地方。

复合索引,就是多个字段联合建一个索引。比如:

CREATE INDEX idx_user_status ON user (status, created_at, type);

这个索引的排序规则是:先按status排,status相同再按created_at排,最后按type排。

最左前缀原则的意思是:查询条件必须从索引的最左边开始匹配,才能用到这个索引。跳过了左边的列,右边的列就用不上。

来看几个例子:

查询条件 是否走索引 说明
WHERE status = 1 ✅ 走索引 匹配最左列,完美
WHERE status = 1 AND created_at > '2024-01-01' ✅ 走索引 匹配前两列,没问题
WHERE status = 1 AND created_at > '2024-01-01' AND type = 1 ✅ 走索引 三列全匹配,最优
WHERE created_at > '2024-01-01' ❌ 不走索引 跳过了status,索引失效
WHERE type = 1 ❌ 不走索引 跳过了前两列,完全用不上
WHERE status = 1 AND type = 1 ⚠️ 部分走索引 status用到了索引,type用不到(因为跳过了created_at)

核心原则:复合索引的字段顺序,决定了它能覆盖哪些查询。把区分度最高、查询最频繁的字段放最左边。我一般把「等值查询的字段」放前面,「范围查询的字段」放后面。

举个例子。假设我们经常查「某个状态下的用户,按创建时间排序」。那索引顺序应该是 (status, created_at),而不是 (created_at, status)。为什么?因为status是等值查询,created_at是范围查询。等值查询放前面,能过滤掉大部分数据,范围查询的压力就小了。

嗯,这里要注意:范围查询(>、<、BETWEEN、LIKE)后面的列,索引会失效。比如 WHERE status = 1 AND created_at > '2024-01-01' AND type = 1,type这一列就用不到索引。因为created_at是范围查询,MySQL在找到第一个满足created_at条件的记录后,就停止索引匹配了。

怎么解决?两个办法:

  • 调整字段顺序:把范围查询的字段放最后。
  • 拆成两个索引:一个用于等值查询,一个用于范围查询。但别拆太多,索引维护也有成本。

我的习惯:建复合索引前,先列出这个表上最频繁的3到5个查询SQL。然后分析每个SQL的WHERE条件,找出公共字段。把公共字段放最左边,剩下的按区分度从高到低排。这样建出来的索引,覆盖度最高。

最后说一句:索引不是银弹。写SQL的时候,多用 EXPLAIN 看看执行计划。type字段是ALL就是全表扫描,是ref或range说明用上了索引。养成这个习惯,你离「SQL优化高手」就不远了。