【MySQL数据库学习】MySQL索引(下)


🔥承渊政道: 个人主页
❄️个人专栏: 《C语言基础语法知识》 《数据结构与算法》 《C++知识内容》 《Linux系统知识》 《算法刷题指南》 《测评文章活动推广》 《大模型语言路线学习》 《MySQL数据库学习》
✨逆境不吐心中苦,顺境不忘来时路!✨ 🎬 博主简介:

在上一篇文章中,我们已经对 MySQL 索引的基本概念、常见类型以及底层数据结构有了初步认识.理解索引是什么、为什么能够提升查询效率,是学习 MySQL 性能优化的第一步.但在实际开发中,仅仅知道索引的概念还远远不够,更重要的是要知道索引应该如何设计、什么时候会生效、什么时候又会失效.很多 SQL 查询明明已经建立了索引,却依然执行缓慢;有些表添加了多个索引,查询性能没有明显提升,反而影响了写入效率.这些问题的背后,往往涉及联合索引、最左前缀原则、覆盖索引、回表查询、索引下推以及执行计划分析等内容.只有真正理解这些机制,才能在面对慢查询时快速定位问题,并写出更加高效的 SQL.本篇文章是 MySQL 索引学习的下篇,将围绕索引的进阶使用和性能优化展开,重点介绍索引的使用规则、常见失效场景、联合索引的设计思路以及如何通过执行计划判断索引是否被合理使用.希望通过本文的学习,能够帮助你从"会建索引"进一步提升到"会用索引、会分析索引、会优化索引".废话不多说,下面跟着小编的节奏🎵一起去疯狂的学习吧!

目录

1.索引的理解

建立测试表

插入多条记录

查看插入结果

思考一下:为何IO交互要是Page?

为何MySQL和磁盘进行IO交互的时候,要采用Page的方案进行交互呢?用多少,加载多少不香吗?

如上面的5条记录,如果MySQL要查找id=2的记录,第一次加载id=1,第二次加载id=2,一次一条记录,那么就需要2次IO.如果要找id=5,那么就需要5次IO.但如果这5条(或者更多)都被保存在一个Page中(16KB,能保存很多记录),那么第一次IO查找id=2的时候,整个Page会被加载到MySQL的Buffer Pool中,这里完成了一次IO.但是往后如果在查找id=1,3,4,5等,完全不需要进行IO了,而是直接在内存中进行了.所以,就在单Page里面,大大减少了IO的次数.你怎么保证,用户一定下次找的数据,就在这个Page里面?我们不能严格保证,但是有很大概率,因为有局部性原理.往往IO效率低下的最主要矛盾不是IO单次数据量的大小,而是IO的次数.

理解单个Page

MySQL 中要管理很多数据表文件,而要管理好这些文件,就需要先描述,再组织,我们目前可以简单理解成一个个独立文件是有一个或者多个Page构成的.

不同的Page在MySQL 中,都是16KB,使用 prev 和 next 构成双向链表因为有主键的问题, MySQL 会默认按照主键给我们的数据进行排序,从上面的Page内数据记录可以看出,数据是有序且彼此关联的.

为什么数据库在插入数据时要对其进行排序呢?我们按正常顺序插入数据不是也挺好的吗?插入数据时排序的目的,就是优化查询的效率.页内部存放数据的模块,实质上也是一个链表的结构,链表的特点也就是增删快,查询修改慢,所以优化查询的效率是必须的.

正式因为有序,在查找的时候,从头到后都是有效查找,没有任何一个查找是浪费的,而且,如果运气好,是可以提前结束查找过程的.

理解多个Page

通过上面的分析,我们知道,上面页模式中,只有一个功能,就是在查询某条数据的时候直接将一

整页的数据加载到内存中,以减少硬盘IO次数,从而提高性能.但是,我们也可以看到,现在的页

模式内部,实际上是采用了链表的结构,前一条数据指向后一条数据,本质上还是通过数据的逐条

比较来取出特定的数据.如果有1千万条数据,一定需要多个Page来保存1千万条数据,多个Page彼此使用双链表链接起来,而且每个Page内部的数据也是基于链表的.那么,查找特定一条记录,也一定是线性查找.这效率也太低了.

页目录

我们在看《C程序设计》这本书的时候,如果我们要看<指针章节>,找到该章节有两种做法

从头逐页的向后翻,直到找到目标内容.通过书提供的目录,发现指针章节在234页(假设),那么我们便直接翻到234页.同时,查找目录的方案,可以顺序找,不过因为目录肯定少,所以可以快速提高定位.本质上,书中的目录,是多花了纸张的,但是却提高了效率.所以,目录,是一种"空间换时间的做法".

单页情况

针对上面的单页Page,我们能否也引入目录呢?当然可以!

那么当前,在一个Page内部,我们引入了目录.比如,我们要查找id=4记录,之前必须线性遍历4次,

才能拿到结果.现在直接通过目录23,直接进行定位新的起始位置,提高了效率.现在我们可以再次正式回答上面的问题了,为何通过键值 MySQL 会自动排序?可以很方便引入目录

多页情况

MySQL 中每一页的大小只有16KB,单个Page大小固定,所以随着数据量不断增大,16KB 不可能存下所有的数据,那么必定会有多个页来存储数据.

在单表数据不断被插入的情况下,MySQL 会在容量不足的时候,自动开辟新的Page来保存新的数据,然后通过指针的方式,将所有的Page组织起来.

需要注意,上面的图,是理想结构,大家也知道,目前要保证整体有序,那么新插入的数据,不一定会

在新Page上面,这里仅仅做演示.这样,我们就可以通过多个Page遍历,Page内部通过目录来快速定位数据.可是,貌似这样也有效率问题,在Page之间,也是需要 MySQL 遍历的,遍历意味着依旧需要进行大量的IO,将下一个Page加载到内存,进行线性检测.这样就显得我们之前的Page内部的目录,有点杯水车薪了.

那么如何解决呢?解决方案,其实就是我们之前的思路,给Page也带上目录.使用一个目录项来指向某一页,而这个目录项存放的就是将要指向的页中存放的最小数据的键值.和页内目录不同的地方在于,这种目录管理的级别是页,而页内目录管理的级别是行.其中,每个目录项的构成是:键值+指针.

存在一个目录页来管理页目录,目录页中的数据存放的就是指向的那一页中最小的数据.有数据,就可通过比较,找到该访问那个Page,进而通过指针,找到下一个Page.

其实目录页的本质也是页,普通页中存的数据是用户数据,而目录页中存的数据是普通页的地址.

可是,我们每次检索数据的时候,该从哪里开始呢?虽然顶层的目录页少了,但是还要遍历啊?不用担心,可以在加目录页

这就是传说中的B+树啊!没错,至此,我们已经给我们的表user构建完了主键索引.

随便找一个id=?我们发现,现在查找的Page数一定减少了,也就意味着IO次数减少了,那么效率也就提高了.

总结一下

Page分为目录页和数据页.目录页只放各个下级Page的最小键值.查找的时候,自定向下找,只需要加载部分目录页到内存,即可完成算法的整个查找过程,大大减少了IO次数.


2.InnoDB在建立索引结构来管理数据的时候,其他数据结构为何不行?

严格来说,不是其他数据结构完全不行 ,而是它们不适合 InnoDB 这种基于磁盘存储、需要高效查询和范围查询的数据库场景.

InnoDB 的索引底层主要使用的是 B+ 树.

原因可以先用一句话概括:

数据库索引的核心目标,是尽量减少磁盘 IO,同时支持高效的等值查询、范围查询和排序。

一、数组为什么不合适?

数组的特点是:连续存储,按下标访问很快.

如果我们知道数组下标:

text 复制代码
arr[5]

访问非常快.

但是数据库查询通常不是按下标查,而是按字段值查:

sql 复制代码
SELECT * FROM user WHERE id = 100;

如果用数组存储数据,要查 id = 100,可能需要从头到尾扫描.

text 复制代码
1 -> 2 -> 3 -> 4 -> 5 -> ... -> 100

这就会变成全表扫描.

数组的问题

问题 说明
查询效率低 按字段值查找时可能需要遍历
插入删除成本高 中间插入或删除数据,需要移动大量元素
不适合动态变化 数据库数据经常增删改

所以数组不适合作为数据库索引结构.

二、链表为什么不合适?

链表插入和删除比较方便.

text 复制代码
A -> B -> C -> D

但是链表最大的问题是:查询太慢.

如果要查某个数据,只能从头节点开始一个一个找.

text 复制代码
查找 C:

A -> B -> C

如果数据量很大,比如100万条,最坏情况可能要查100万次.

链表的问题

问题 说明
查询效率低 只能顺序遍历
不支持快速定位 无法像树结构一样二分查找
磁盘 IO 多 数据分散时读取成本高

所以链表也不适合做数据库索引.

三、哈希表为什么不合适?

哈希表查等值查询很快.

例如:

sql 复制代码
SELECT * FROM user WHERE id = 100;

如果用哈希索引,可以通过哈希函数直接定位.

text 复制代码
hash(100) -> 数据位置

看起来很快,但哈希表有一个致命问题:不适合范围查询.

比如:

sql 复制代码
SELECT * FROM user WHERE id > 100;

或者:

sql 复制代码
SELECT * FROM user WHERE id BETWEEN 100 AND 200;

哈希表中的数据不是按大小顺序排列的.

可能是这样:

text 复制代码
hash(1)   -> 位置 7
hash(100) -> 位置 2
hash(200) -> 位置 9
hash(50)  -> 位置 4

数据被打散了,无法顺着读.

哈希表的问题

问题 说明
不支持范围查询 >、<、BETWEEN 效率差
不支持排序 ORDER BY 无法利用有序结构
不适合最左前缀匹配 联合索引场景受限
哈希冲突 不同 key 可能映射到同一位置

所以哈希表适合等值查询,但不适合作为 InnoDB 的主要索引结构.

四、二叉搜索树为什么不合适?

二叉搜索树的特点是:

text 复制代码
左子树 < 根节点 < 右子树

例如:

text 复制代码
        8
       / \
      4   12
     / \  / \
    2  6 10 14

查找数据时,可以不断比较大小,效率比数组和链表高.

但是普通二叉搜索树有一个严重问题:可能退化成链表.

如果插入的数据是有序的:

text 复制代码
1, 2, 3, 4, 5, 6

树可能变成:

text 复制代码
1
 \
  2
   \
    3
     \
      4
       \
        5

这时候查询效率就退化成链表了.

二叉搜索树的问题

问题 说明
可能退化 极端情况下变成链表
树高较高 数据量大时层级很多
磁盘 IO 多 每访问一层可能产生一次磁盘 IO

所以普通二叉搜索树不适合数据库索引.

五、平衡二叉树为什么也不合适?

平衡二叉树,比如 AVL 树,可以避免退化成链表.

它会通过旋转保持树的平衡.

例如:

text 复制代码
        8
       / \
      4   12
     / \  / \
    2  6 10 14

查询效率确实不错,时间复杂度是:

text 复制代码
O(log n)

但是它仍然不适合 InnoDB 的磁盘索引.

原因是:树太高了.

平衡二叉树每个节点最多只有两个子节点.

text 复制代码
一个节点最多分出 2 条路

如果数据量非常大,树的高度会比较高.

假设有1000万条数据,二叉树高度大约是:

text 复制代码
log2(10000000) ≈ 24

也就是说,一次查询可能要访问 20 多层节点.

在数据库中,节点通常存储在磁盘页中,访问一层就可能产生一次磁盘 IO.

磁盘 IO 是非常慢的.

平衡二叉树的问题

问题 说明
每个节点分叉少 每个节点最多两个子节点
树高较高 数据量大时层数多
磁盘 IO 多 查询路径长
不适合磁盘页存储 一个节点放不了太多索引项

所以 AVL 树、红黑树这类结构适合内存中的数据结构,比如 Java 的 TreeMap,但不适合作为 InnoDB 的磁盘索引结构.

六、红黑树为什么不合适?

红黑树也是一种自平衡二叉搜索树.

Java 中很多结构都用红黑树,比如:

java 复制代码
TreeMap
HashMap 红黑树化后的桶

红黑树在内存中表现很好.

但是它还是二叉树:

text 复制代码
每个节点最多只有 2 个子节点

所以它也有和 AVL 树类似的问题:

树高偏高,访问节点次数多,磁盘 IO 次数多。

数据库最怕的不是CPU 比较,而是磁盘 IO.

在内存中比较20次可能很快,但如果每一层都要读磁盘,那性能就很差.

所以红黑树适合内存索引,不适合作为 InnoDB 的磁盘索引.

七、B 树为什么还不如 B+ 树?

B 树已经比二叉树更适合数据库了.

因为 B 树是多叉树,一个节点可以有多个 key 和多个子节点.

text 复制代码
        [20 | 40]
       /    |    \
   <20   20~40   >40

这样树的高度会大大降低.

但是 InnoDB 最终选择的是 B+ 树,而不是普通 B 树.

区别在于:

B 树

B 树的非叶子节点和叶子节点都可以存储数据.

text 复制代码
        [索引 + 数据]
       /      |      \
 [索引+数据] [索引+数据] [索引+数据]

B+ 树

B+ 树只有叶子节点存储完整数据,非叶子节点只存储索引 key.

text 复制代码
        [索引]
       /      \
    [索引]   [索引]
      |        |
 [完整数据] -> [完整数据] -> [完整数据]

八、为什么 B+ 树更适合 InnoDB?

1.B+ 树层级更低,磁盘 IO 更少

InnoDB 默认数据页大小是 16KB.

B+ 树的非叶子节点只存索引 key 和指针,不存完整数据.

所以一个节点中可以放更多索引项.

也就是说,一个节点可以分出更多叉.

text 复制代码
一个节点能存更多 key
        ↓
每层能定位更多数据
        ↓
树的高度更低
        ↓
磁盘 IO 更少

这非常重要.

因为数据库查询最怕磁盘 IO 多.

2.B+ 树非常适合范围查询

B+ 树的叶子节点之间通过链表连接.

text 复制代码
[1,2,3] -> [4,5,6] -> [7,8,9] -> [10,11,12]

如果执行范围查询:

sql 复制代码
SELECT * FROM user WHERE id BETWEEN 100 AND 200;

B+ 树可以先定位到 100,然后沿着叶子节点链表向后扫描.

text 复制代码
找到 100 -> 101 -> 102 -> ... -> 200

这种方式非常高效.

而哈希表不支持这种天然有序的范围扫描.

3.B+ 树查询性能更稳定

B+ 树所有数据都在叶子节点.

也就是说,不管查哪条数据,基本都要从根节点走到叶子节点.

text 复制代码
根节点 -> 中间节点 -> 叶子节点

查询路径比较稳定.

而 B 树中,有些数据可能在根节点,有些在中间节点,有些在叶子节点.

查询路径不完全一致.

对于数据库来说,稳定的查询性能更重要.

4.B+ 树更适合排序

因为 B+ 树的叶子节点本身是有序的,所以可以很好地支持:

sql 复制代码
ORDER BY id;

如果查询条件和索引匹配,MySQL 可以直接按照索引顺序读取数据,减少额外排序.

5.B+ 树适合全表扫描和区间扫描

因为叶子节点之间有链表连接,所以扫描整张表时可以顺着叶子节点读.

text 复制代码
叶子节点1 -> 叶子节点2 -> 叶子节点3 -> 叶子节点4

这种方式比在树中反复跳转更适合磁盘读取.

B树

B+树


3.聚簇索引VS非聚簇索引

在 InnoDB 中,索引可以分为两类:

类型 也叫 核心区别
聚簇索引 Clustered Index 索引和数据放在一起
非聚簇索引 Secondary Index / 二级索引 索引和数据分开存放

一句话理解:

聚簇索引的叶子节点存放完整数据;非聚簇索引的叶子节点存放主键值。

一、聚簇索引是什么?

聚簇索引是 InnoDB 中最重要的索引.

在InnoDB 里,表数据本身就是按照聚簇索引组织存储的.

通常情况下,主键索引就是聚簇索引.

例如:

sql 复制代码
CREATE TABLE user (
    id INT PRIMARY KEY,
    name VARCHAR(20),
    age INT
);

这里的 id 是主键,所以 InnoDB 会基于 id 建立聚簇索引.

聚簇索引的结构

聚簇索引底层是 B+ 树.

它的叶子节点存放的是完整的一行数据.

text 复制代码
聚簇索引 B+ 树

        [id=10]
       /       \
   [id=5]     [id=20]
      |          |
  叶子节点     叶子节点
      |          |
完整行数据     完整行数据

假设表中有这些数据:

id name age
1 张三 18
2 李四 20
3 王五 22

聚簇索引的叶子节点中存放的是:

text 复制代码
id = 1, name = 张三, age = 18
id = 2, name = 李四, age = 20
id = 3, name = 王五, age = 22

也就是说:

找到聚簇索引的叶子节点,就找到了完整的数据行。

二、非聚簇索引是什么?

非聚簇索引也叫二级索引辅助索引.

它也是 B+ 树结构,但是它的叶子节点不存完整数据行,而是存放:

text 复制代码
索引字段值 + 主键值

例如:

sql 复制代码
CREATE TABLE user (
    id INT PRIMARY KEY,
    name VARCHAR(20),
    age INT,
    INDEX idx_name(name)
);

这里有两个索引:

索引 类型
PRIMARY KEY(id) 聚簇索引
idx_name(name) 非聚簇索引

非聚簇索引的结构

idx_name(name) 的叶子节点大概存放:

text 复制代码
name = 张三, id = 1
name = 李四, id = 2
name = 王五, id = 3

注意,它并不直接存完整行数据.

它存的是当前索引字段和对应的主键值.

三、查询时有什么区别?

1.通过聚簇索引查询

例如:

sql 复制代码
SELECT * FROM user WHERE id = 1;

因为 id 是主键,也是聚簇索引.

查询过程:

text 复制代码
通过 id 找到聚簇索引叶子节点
        ↓
直接拿到完整行数据

这种查询非常快。

text 复制代码
id = 1, name = 张三, age = 18

2.通过非聚簇索引查询

例如:

sql 复制代码
SELECT * FROM user WHERE name = '张三';

name 上有普通索引 idx_name.

查询过程:

text 复制代码
先查 idx_name 非聚簇索引
        ↓
找到 name = 张三 对应的主键 id = 1
        ↓
再根据 id 去聚簇索引中查完整数据

四、聚簇索引和非聚簇索引的核心区别

对比项 聚簇索引 非聚簇索引
是否存完整数据
叶子节点存什么 完整行数据 索引字段 + 主键值
一个表可以有几个 只能有一个 可以有多个
常见形式 主键索引 普通索引、唯一索引、联合索引
查询完整行是否回表 不需要 通常需要
查询速度 主键查询很快 可能需要二次查询
数据存储顺序 按聚簇索引组织 不决定整行数据顺序

MyISAM 存储引擎-主键索引

MyISAM 引擎同样使用B+树作为索引结果,叶节点的data域存放的是数据记录的地址.上图为 MyISAM表的主索引,Col1 为主键.

其中,MyISAM 最大的特点是,将索引Page和数据Page分离,也就是叶子节点没有数据,只有对应数据的地址.相较于InnoDB索引,InnoDB 是将索引和数据放在一起的.

其中,MyISAM 这种用户数据与索引数据分离的索引方案,叫做非聚簇索引

其中,InnoDB 这种用户数据与索引数据在一起索引方案,叫做聚簇索引

当然,MySQL 除了默认会建立主键索引外,我们用户也有可能建立按照其他列信息建立的索引,一般这种索引可以叫做辅助(普通)索引.

对于MyISAM ,建立辅助(普通)索引和主键索引没有差别,无非就是主键不能重复,而非主键可重复.下图就是基于 MyISAM 的 Col2 建立的索引,和主键索引没有差别.

同样,InnoDB 除了主键索引,用户也会建立辅助(普通)索引,我们以上表中的 Col3 建立对应的辅助索引如下图:

可以看到,InnoDB 的非主键索引中叶子节点并没有数据,而只有对应记录的key值.

所以通过辅助(普通)索引,找到目标记录,需要两遍索引:首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录.这种过程,就叫做回表查询!为何 InnoDB 针对这种辅助(普通)索引的场景,不给叶子节点也附上数据呢?原因就是太浪费空间了.


4.索引操作

4.1创建主键索引

bash 复制代码
第一种方式:在创建表的时候,直接在字段名后指定 primary key
第二种方式:在创建表的最后,指定某列或某几列为主键索引
第三种方式:创建表以后再添加主键

create table user1(id int primary key, name varchar(30));

create table user2(id int, name varchar(30), primary key(id));

create table user3(id int, name varchar(30));
alter table user3 add primary key(id);

主键索引的特点:

一个表中,最多有一个主键索引,当然可以使符合主键.

主键索引的效率高(主键不可重复).

创建主键索引的列,它的值不能为null,且不能重复.

主键索引的列基本上是int.


4.2唯一索引的创建

bash 复制代码
第一种方式:在表定义时,在某列后直接指定unique唯一属性。
第二种方式:创建表时,在表的后面指定某列或某几列为unique
第三种方式:创建表以后再添加唯一主键

create table user4(id int primary key, name varchar(30) unique);

create table user5(id int primary key, name varchar(30), unique(name));

create table user6(id int primary key, name varchar(30));
alter table user6 add unique(name);

唯一索引的特点:

一个表中,可以有多个唯一索引.

查询效率高.

如果在某一列建立唯一索引,必须保证这列不能有重复数据.

如果一个唯一索引上指定not null,等价于主键索引.


4.3普通索引的创建

bash 复制代码
第一种方式:在表的定义最后,指定某列为索引
第二种方式:创建完表以后指定某列为普通索引
第三种方式:创建一个索引名为 idx_name 的索引

create table user8(id int primary key,
name varchar(20),
email varchar(30),
index(name) 
);

create table user9(id int primary key, name varchar(20), email
varchar(30));
alter table user9 add index(name); 

create table user10(id int primary key, name varchar(20), email
varchar(30));
create index idx_name on user10(name);

普通索引的特点:

一个表中可以有多个普通索引,普通索引在实际开发中用的比较多.

如果某列需要创建索引,但是该列有重复的值,那么我们就应该使用普通索引.


4.4全文索引的创建

当对文章字段或有大量文字的字段进行检索时,会使用到全文索引.MySQL提供全文索引机制,但是有要求,要求表的存储引擎必须是MyISAM,而且默认的全文索引支持英文,不支持中文.如果对中文进行全文检索,可以使用sphinx的中文版(coreseek).

查询有没有database数据

如果使用如下查询方式,虽然查询出数据,但是没有使用到全文索引.

可以用explain工具看一下,是否使用到索引

如何使用全文索引呢?

通过explain来分析这个sql语句


4.5查询索引

bash 复制代码
第一种方法: show keys from 表名
第二种方法: show index from 表名;
第三种方法(信息比较简略): desc 表名;

4.6删除索引

bash 复制代码
第一种方法-删除主键索引: alter table 表名 drop primary key;
第二种方法-其他索引的删除: alter table 表名 drop index 索引名; 索引名就是show keys
from 表名中的 Key_name 字段
mysql> alter table user10 drop index idx_name;
第三种方法方法: drop index 索引名 on 表名
mysql> drop index name on user8;

4.7索引创建原则

索引不是越多越好.索引的本质是用额外的存储空间 换取更快的查询速度.

创建索引时要记住一句话:

索引要建立在查询频繁、区分度高、能减少扫描范围的字段上。

一、给经常作为查询条件的字段建索引

如果某个字段经常出现在 WHERE 条件中,就比较适合建立索引.

例如:

sql 复制代码
SELECT * FROM user WHERE phone = '13800138000';

如果 phone 经常用于查询,可以创建索引:

sql 复制代码
CREATE INDEX idx_phone ON user(phone);

适合建索引的字段:

sql 复制代码
WHERE phone = ?
WHERE username = ?
WHERE order_no = ?
WHERE user_id = ?

不常用于查询的字段,不建议随便建索引.

二、给区分度高的字段建索引

字段的区分度越高,索引效果越好.

比如:

字段 区分度 是否适合建索引
身份证号 很高 适合
手机号 很高 适合
订单号 很高 适合
性别 很低 不太适合
状态 较低 不太适合

例如 gender 字段只有两个值:

text 复制代码
男、女

即使建了索引,MySQL 也可能还是要扫描大量数据,效果并不好.

判断区分度可以用:

sql 复制代码
SELECT COUNT(DISTINCT 字段名) / COUNT(*) FROM 表名;

结果越接近 1,说明区分度越高.

三、给经常排序的字段建索引

如果字段经常用于 ORDER BY,可以考虑建立索引.

例如:

sql 复制代码
SELECT * FROM article ORDER BY create_time DESC;

可以创建索引:

sql 复制代码
CREATE INDEX idx_create_time ON article(create_time);

这样 MySQL 可以直接按照索引顺序读取数据,减少额外排序.

四、给经常分组的字段建索引

如果字段经常用于 GROUP BY,也可以考虑建立索引.

例如:

sql 复制代码
SELECT user_id, COUNT(*) 
FROM orders 
GROUP BY user_id;

可以创建索引:

sql 复制代码
CREATE INDEX idx_user_id ON orders(user_id);

索引本身是有序的,对分组统计有一定帮助.

五、给经常连接的字段建索引

多表关联查询时,连接字段最好建立索引.

例如:

sql 复制代码
SELECT *
FROM orders o
JOIN user u ON o.user_id = u.id;

这里 orders.user_iduser.id 都应该有索引.

通常 user.id 是主键,已经有索引.

所以重点是给外键字段建索引:

sql 复制代码
CREATE INDEX idx_user_id ON orders(user_id);

否则关联查询可能会非常慢.

六、联合索引优先考虑最左前缀原则

如果多个字段经常一起查询,可以使用联合索引.

例如:

sql 复制代码
SELECT * FROM user 
WHERE name = '张三' AND age = 18;

可以创建联合索引:

sql 复制代码
CREATE INDEX idx_name_age ON user(name, age);

联合索引要遵守最左前缀原则.

对于索引:

sql 复制代码
CREATE INDEX idx_name_age_city ON user(name, age, city);

可以命中的情况:

sql 复制代码
WHERE name = '张三';

WHERE name = '张三' AND age = 18;

WHERE name = '张三' AND age = 18 AND city = '北京';

不太能充分命中的情况:

sql 复制代码
WHERE age = 18;

WHERE city = '北京';

WHERE age = 18 AND city = '北京';

因为没有从最左边的 name 开始使用.

七、主键索引建议短、递增、稳定

InnoDB 中,主键索引就是聚簇索引.

主键设计建议:

原则 说明
主键越短,二级索引占用空间越小
递增 减少页分裂,提高插入效率
稳定 主键不要频繁修改
非空唯一 主键必须保证唯一性

推荐:

sql 复制代码
id BIGINT PRIMARY KEY AUTO_INCREMENT

不太推荐直接使用很长的 UUID 作为主键:

sql 复制代码
uuid CHAR(36) PRIMARY KEY

因为 UUID 较长且随机,容易导致索引占用空间大、插入时页分裂较多.

八、索引数量不要太多

索引不是越多越好.

索引太多会带来几个问题:

问题 说明
占用磁盘空间 每个索引都是一棵 B+ 树
降低写入速度 增删改时需要维护索引
增加优化器选择成本 MySQL 需要判断使用哪个索引
可能产生冗余索引 多个索引功能重复

例如已经有联合索引:

sql 复制代码
CREATE INDEX idx_name_age ON user(name, age);

通常不需要再单独创建:

sql 复制代码
CREATE INDEX idx_name ON user(name);

因为 idx_name_age 已经可以支持:

sql 复制代码
WHERE name = '张三';

九、索引创建原则总结

原则 说明
经常查询的字段建索引 常出现在 WHERE
区分度高的字段建索引 过滤效果更好
排序字段可建索引 优化 ORDER BY
分组字段可建索引 优化 GROUP BY
关联字段建索引 优化 JOIN
多条件查询用联合索引 注意最左前缀原则
避免索引过多 写入成本会增加
避免低区分度字段单独建索引 如性别、状态
避免长字段直接建索引 可使用前缀索引
避免索引失效写法 函数、计算、隐式转换等
主键短、递增、稳定 对 InnoDB 很重要
用 EXPLAIN 验证 不靠感觉判断

一句话总结

索引应该建在高频查询、区分度高、过滤能力强的字段上;联合索引要遵守最左前缀原则,同时避免索引过多和索引失效。


5.复合索引

复合索引也叫联合索引,指的是一个索引中包含多个字段.

例如:

sql 复制代码
CREATE INDEX idx_name_age ON user(name, age);

这个索引不是给 nameage 分别创建两个索引,而是创建了一个组合索引:

text 复制代码
(name, age)

可以理解为:先按照 name 排序,name 相同的情况下,再按照 age 排序.

一、复合索引示例

假设有一张用户表:

sql 复制代码
CREATE TABLE user (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(50),
    age INT,
    city VARCHAR(50)
);

创建复合索引:

sql 复制代码
CREATE INDEX idx_name_age_city ON user(name, age, city);

这个索引包含三个字段:

text 复制代码
name -> age -> city

它在 B+ 树中大致按照这个顺序排序:

text 复制代码
先按 name 排序
name 相同,再按 age 排序
age 相同,再按 city 排序

二、复合索引的核心原则:最左前缀原则

复合索引最重要的规则是:

查询条件必须从索引最左边的字段开始使用,才能有效命中索引.

例如有索引:

sql 复制代码
CREATE INDEX idx_name_age_city ON user(name, age, city);

这个索引相当于具备下面几种能力:

text 复制代码
name
name + age
name + age + city

可以使用索引的情况

sql 复制代码
SELECT * FROM user WHERE name = '张三';

可以用到 name.

sql 复制代码
SELECT * FROM user WHERE name = '张三' AND age = 18;

可以用到 name + age.

sql 复制代码
SELECT * FROM user 
WHERE name = '张三' AND age = 18 AND city = '北京';

可以用到 name + age + city.

不能很好使用索引的情况

sql 复制代码
SELECT * FROM user WHERE age = 18;

不能充分使用 idx_name_age_city,因为缺少最左边的 name.

sql 复制代码
SELECT * FROM user WHERE city = '北京';

也不能充分使用,因为没有从 name 开始.

sql 复制代码
SELECT * FROM user WHERE age = 18 AND city = '北京';

同样不能充分使用,因为跳过了最左字段 name.

三、为什么必须遵守最左前缀原则?

因为复合索引的排序方式是有顺序的.

对于索引:

text 复制代码
(name, age, city)

它不是分别按照 nameagecity 各自独立排序,而是:

text 复制代码
先按 name 排序
name 相同,再按 age 排序
age 相同,再按 city 排序

例如:

text 复制代码
张三 18 北京
张三 20 上海
李四 18 北京
李四 22 深圳
王五 19 广州

如果你查:

sql 复制代码
WHERE name = '张三'

可以快速定位,因为最外层就是按 name 排序的.

如果你查:

sql 复制代码
WHERE age = 18

就不好定位,因为 age 不是全局有序的,它只是在 name 相同的情况下才有序.

所以复合索引必须从最左字段开始使用.

四、复合索引可以形成覆盖索引

如果查询的字段都在复合索引中,就不需要回表.

例如有索引:

sql 复制代码
CREATE INDEX idx_name_age ON user(name, age);

查询:

sql 复制代码
SELECT name, age 
FROM user 
WHERE name = '张三';

这个查询只需要 nameage,而这两个字段都在索引里,所以可以直接从索引中返回结果.

这种情况叫:

覆盖索引

覆盖索引可以减少回表,提高查询效率.

但是下面这个查询可能需要回表:

sql 复制代码
SELECT * 
FROM user 
WHERE name = '张三';

因为 SELECT * 需要完整行数据,而普通复合索引的叶子节点并不保存完整数据.

五、复合索引常见使用场景

1.多条件查询

sql 复制代码
SELECT * FROM user 
WHERE name = '张三' AND age = 18;

适合:

sql 复制代码
CREATE INDEX idx_name_age ON user(name, age);

2.条件查询 + 排序

sql 复制代码
SELECT * FROM orders
WHERE user_id = 1001
ORDER BY create_time DESC;

适合:

sql 复制代码
CREATE INDEX idx_user_time ON orders(user_id, create_time);

3.条件查询 + 分组

sql 复制代码
SELECT user_id, status, COUNT(*)
FROM orders
WHERE user_id = 1001
GROUP BY user_id, status;

适合:

sql 复制代码
CREATE INDEX idx_user_status ON orders(user_id, status);

4.覆盖索引

sql 复制代码
SELECT user_id, status, create_time
FROM orders
WHERE user_id = 1001 AND status = 1;

适合:

sql 复制代码
CREATE INDEX idx_user_status_time 
ON orders(user_id, status, create_time);

六、复合索引总结

重点 说明
复合索引 一个索引包含多个字段
也叫 联合索引
核心规则 最左前缀原则
字段顺序 非常重要
适合场景 多条件查询、排序、分组、覆盖索引
注意事项 范围查询后面的字段通常不能充分利用
不建议 盲目创建过长复合索引

一句话总结:

复合索引就是把多个字段组合成一棵 B+ 树,查询时要尽量从最左字段开始使用,字段顺序决定了索引能否高效生效。


6.索引最左匹配原则

结论:

对于联合索引,MySQL 会从索引最左边的字段开始连续匹配;如果跳过左边字段,后面的字段通常不能用于高效定位。

例如有联合索引:

sql 复制代码
CREATE INDEX idx_a_b_c ON user(a, b, c);

这个索引的顺序是:

text 复制代码
a -> b -> c

它可以理解为:

text 复制代码
先按 a 排序
a 相同,再按 b 排序
b 相同,再按 c 排序

所以它能很好支持:

text 复制代码
a
a + b
a + b + c

但不能直接高效支持:

text 复制代码
b
c
b + c

1.可以命中索引的情况

假设索引是:

sql 复制代码
idx_a_b_c(a, b, c)

可以使用 a

sql 复制代码
SELECT * FROM user WHERE a = 1;

可以使用 a + b

sql 复制代码
SELECT * FROM user WHERE a = 1 AND b = 2;

可以使用 a + b + c

sql 复制代码
SELECT * FROM user WHERE a = 1 AND b = 2 AND c = 3;

这些都符合最左匹配原则。

2.不能很好使用索引的情况

跳过最左字段 a

sql 复制代码
SELECT * FROM user WHERE b = 2;

这个查询不能很好使用 idx_a_b_c

因为索引是先按 a 排序的,b 不是全局有序的.

只查 c

sql 复制代码
SELECT * FROM user WHERE c = 3;

也不能很好使用这个联合索引.

跳过中间字段

sql 复制代码
SELECT * FROM user WHERE a = 1 AND c = 3;

这种情况通常只能较好使用到:

text 复制代码
a

因为中间跳过了 b,所以 c 不能继续用于完整的索引定位.

3.WHERE 条件书写顺序不重要

这个很多人容易误解.

有索引:

sql 复制代码
CREATE INDEX idx_a_b ON user(a, b);

下面两条 SQL 通常都可以使用联合索引:

sql 复制代码
SELECT * FROM user WHERE a = 1 AND b = 2;
sql 复制代码
SELECT * FROM user WHERE b = 2 AND a = 1;

虽然第二条 SQL 里 b 写在前面,但 MySQL 优化器会调整条件顺序.

真正重要的是:

联合索引本身的字段顺序,而不是 WHERE 后面的书写顺序。

4.范围查询会影响后续字段

假设有索引:

sql 复制代码
CREATE INDEX idx_a_b_c ON user(a, b, c);

查询:

sql 复制代码
SELECT * FROM user
WHERE a = 1
  AND b > 10
  AND c = 3;

通常索引使用情况是:

text 复制代码
a 可以用于等值匹配
b 可以用于范围匹配
c 通常不能继续用于精确定位

也就是一般认为使用到:

text 复制代码
a + b

c 不能继续像等值条件那样缩小索引扫描范围.

常见范围条件包括:

sql 复制代码
>
<
>=
<=
BETWEEN
LIKE 'abc%'

注意:MySQL 可能会通过 索引条件下推 ICPc 做过滤,但这和"继续用于精确定位索引范围"不是一回事.

5.LIKE 的最左匹配

可以使用索引

sql 复制代码
SELECT * FROM user WHERE name LIKE '张%';

因为它从字符串最左边开始匹配.

通常不能很好使用普通 B+ 树索引

sql 复制代码
SELECT * FROM user WHERE name LIKE '%三';
sql 复制代码
SELECT * FROM user WHERE name LIKE '%张三%';

因为前面是不确定的,无法从索引树中快速定位.

6.例子总结

索引:

sql 复制代码
CREATE INDEX idx_a_b_c ON user(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 不符合最左匹配
WHERE c = 3 不符合最左匹配
WHERE b = 2 AND c = 3 不符合最左匹配
WHERE a = 1 AND c = 3 通常只较好使用 a
WHERE a = 1 AND b > 2 AND c = 3 通常使用到 a + b

7.怎么验证是否走索引?

EXPLAIN

sql 复制代码
EXPLAIN
SELECT * FROM user
WHERE a = 1 AND b = 2 AND c = 3;

重点看这几个字段:

字段 含义
key 实际使用的索引
key_len 使用索引的长度,可辅助判断用了几个字段
rows 预计扫描行数
Extra 是否有 Using indexUsing whereUsing filesort

核心记忆

联合索引:

text 复制代码
(a, b, c)

能高效匹配:

text 复制代码
a
a + b
a + b + c

不能直接高效匹配:

text 复制代码
b
c
b + c

最关键的是:

从最左字段开始,连续匹配;不能跳过中间字段;遇到范围查询后,后面的字段通常不能继续用于精确定位。


7.索引覆盖

索引覆盖 ,也叫 覆盖索引,指的是:

查询需要的字段,都能直接从索引中拿到,不需要再回到聚簇索引中查询完整数据行。

它的核心价值是:

减少回表,提高查询效率。

一、先看一个例子

有一张表:

sql 复制代码
CREATE TABLE user (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(50),
    age INT,
    city VARCHAR(50),
    INDEX idx_name_age(name, age)
);

这里有一个普通索引:

sql 复制代码
idx_name_age(name, age)

在 InnoDB 中,普通二级索引的叶子节点大致存:

text 复制代码
name + age + 主键id

二、什么情况是索引覆盖?

执行查询:

sql 复制代码
SELECT name, age
FROM user
WHERE name = '张三';

这条 SQL 查询需要的字段是:

text 复制代码
name, age

查询条件需要的字段是:

text 复制代码
name

这些字段都在索引 idx_name_age(name, age) 里面.

所以 MySQL 可以直接从索引中拿到结果:

text 复制代码
idx_name_age 索引
      ↓
直接返回 name、age

不需要回表.

这就是索引覆盖.

三、什么情况不是索引覆盖?

还是这张表:

sql 复制代码
CREATE TABLE user (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(50),
    age INT,
    city VARCHAR(50),
    INDEX idx_name_age(name, age)
);

执行:

sql 复制代码
SELECT *
FROM user
WHERE name = '张三';

虽然 name 可以用到 idx_name_age 索引,但是 SELECT * 需要查询:

text 复制代码
id, name, age, city

idx_name_age 中没有 city 字段.

所以 MySQL 的过程是:

text 复制代码
先查 idx_name_age
      ↓
找到主键 id
      ↓
回表到聚簇索引
      ↓
查询完整行数据

这就不是索引覆盖.

四、索引覆盖和回表的关系

在 InnoDB 中:

非覆盖索引查询

sql 复制代码
SELECT *
FROM user
WHERE name = '张三';

执行过程:

text 复制代码
二级索引 idx_name_age
        ↓
找到主键 id
        ↓
回表查询聚簇索引
        ↓
拿到完整数据

覆盖索引查询

sql 复制代码
SELECT id, name, age
FROM user
WHERE name = '张三';

注意:虽然索引是:

sql 复制代码
idx_name_age(name, age)

但 InnoDB 的二级索引叶子节点会保存主键 id.

所以这个查询需要的:

text 复制代码
id, name, age

都能从二级索引中拿到.

执行过程:

text 复制代码
二级索引 idx_name_age
        ↓
直接返回 id、name、age

不用回表.

五、如何判断是否使用了覆盖索引?

使用 EXPLAIN.

sql 复制代码
EXPLAIN
SELECT id, name, age
FROM user
WHERE name = '张三';

重点看 Extra 字段.

如果看到:

text 复制代码
Using index

通常说明使用了覆盖索引.

示例:

text 复制代码
key: idx_name_age
Extra: Using index

表示 MySQL 直接通过索引完成查询,不需要回表.

六、索引覆盖的好处

1.减少回表

这是最大好处.

普通二级索引查询完整数据时,通常需要:

text 复制代码
查二级索引一次
查聚簇索引一次

覆盖索引可以省掉第二次查询.


2.减少磁盘 IO

索引页通常比数据页更小.

如果只扫描索引,不读取完整数据行,IO 成本更低.

3.提高查询速度

尤其是数据量大、查询频繁的场景,覆盖索引优化效果明显.

例如:

sql 复制代码
SELECT order_no, status, create_time
FROM orders
WHERE user_id = 1001;

可以考虑创建:

sql 复制代码
CREATE INDEX idx_user_order_status_time
ON orders(user_id, order_no, status, create_time);

这样查询字段都在索引中,就可能形成覆盖索引.

七、覆盖索引总结

问题 答案
覆盖索引是什么 查询字段都在索引里,不需要回表
核心作用 减少回表,降低 IO
典型标志 EXPLAINExtra 出现 Using index
常见优化方式 避免 SELECT *,设计合适联合索引
是否越多越好 不是,索引过多会影响写入和占用空间

一句话记忆:

覆盖索引就是:查询要什么,索引里就有什么,MySQL 直接查索引就能返回结果,不用回表。

🚀真正的勇者不是流泪的人,而是含泪奔跑的人!


敬请期待下一篇文章内容


每日心灵鸡汤: 别让坏情绪,偷走你的今天!

人这一生,总会遇到不顺心的事.可能是一句刺耳的话,一次突然的否定,一段没有回应的关系,或者某个努力之后依然不理想的结果.很多时候,真正让人疲惫的不是事情本身,而是我们在心里反复咀嚼它、放大它、困住自己.其实,生活从来不会因为你难过就暂停,也不会因为你焦虑就立刻变好.与其把时间浪费在消耗自己的情绪里,不如试着把注意力拉回当下.该吃饭就好好吃饭,该休息就安心休息,该努力的事情继续去做.你越能稳住自己,就越不会被外界轻易打乱节奏.不要让别人的一句话,定义你的价值;也不要让一时的不如意,否定你一直以来的努力.真正成熟的人,不是没有情绪,而是懂得不被情绪牵着走.今天不管发生了什么,都请给自己一点温柔.把烦恼放一放,把心态调一调.明天的太阳依旧会升起,而你,也依然值得拥有新的开始.