MySQL 索引:原理、分类与操作指南
MySQL-索引
1. 什么是索引?
1.1 概念
一个非常贴切的比喻是:索引就像一本书的目录。
-
没有索引(目录):当你想在书中查找某个特定内容时,你只能从第一页开始,一页一页地往后翻,直到找到为止。这在数据库中被称为全表扫描(Full Table Scan),效率非常低。
-
有索引(目录):你可以通过目录快速定位到内容所在的章节和页码,然后直接翻到那一页。索引就是帮助数据库引擎快速找到数据的一种数据结构。
官方定义:索引是帮助 MySQL 高效获取数据的 排好序 的 数据结构。
这个定义包含了三个关键点:
-
高效获取数据:目的就是为了加快查询速度。
-
排好序:索引中的数据是按照某种规则排序的,这是实现快速查找(如二分查找、范围检索)的基础。
-
数据结构:它本质上是一种数据结构,MySQL 中最常用的是 B+Tree。
1.2 索引的优缺点
1.2.1 优点
- 大大加快了数据的检索速度(核心优势)。
1.2.2 缺点
-
需要占用额外的磁盘空间。
-
会降低数据写入(增、删、改)的速度。因为当数据发生变化时,索引也需要同时进行维护和更新。
2. 索引的分类
2.1 从物理存储上划分
-
聚集索引
-
定义:表数据行的物理存储顺序与索引键值的逻辑顺序相同。一个表 只能有一个 聚集索引。
-
InnoDB 的实现:InnoDB 的表必然有一个聚集索引。
-
如果定义了主键(PRIMARY KEY),那么主键就是聚集索引。
-
如果没有主键,则选择第一个唯一的非空索引(UNIQUE NOT NULL)作为聚集索引。
-
如果都没有,InnoDB 会隐式创建一个隐藏的
ROWID
作为聚集索引。
-
-
叶子节点:存储的是 完整的数据行(通常是按照 id 值顺序存储的)。
-
-
非聚集索引(二级索引/辅助索引)
-
定义:索引的逻辑顺序与数据行的物理存储顺序无关。一个表可以有多个非聚集索引。
-
叶子节点:存储的不是数据行本身,而是其 非聚簇索引存储的是索引列的字段值 + 对应数据行的主键值。
-
回表查询:当使用非聚集索引进行查询时,首先在非聚集索引的 B+Tree 中找到对应的主键值,然后再用这个主键值到聚集索引的 B+Tree 中查找完整的行数据。这个额外查找聚集索引的过程就叫做 回表。
-
示例:
user
表:id
(主键), name
, age
。
-
在
id
上建立了聚集索引。 -
在
age
上建立了非聚集索引。 -
执行
SELECT * FROM user WHERE age = 30;
-
在
age
索引的 B+Tree 中找到age=30
的节点,获取对应的主键id
(例如id=10
)。 -
用
id=10
到id
主键索引的 B+Tree 中查找,获取完整的行数据(被称为回表查询)。
-
2.2 从逻辑和功能上划分
-
主键索引(PRIMARY KEY)
- 特殊的唯一索引,不允许为空。一张表只能有一个。
-
唯一索引(UNIQUE KEY)
- 索引列的值必须唯一,但允许有空值。
-
普通索引(INDEX/KEY)
- 最基本的索引,没有任何限制,仅用于加速查询。
-
复合索引
-
由多个列组合而成的索引。
-
CREATE INDEX idx_name_age ON user(name, age);
-
最左前缀原则:这是复合索引最重要的特性。查询时,必须从索引的最左列开始,并且不能跳过中间的列。
-
WHERE name = ‘张三’
(使用索引) -
WHERE name = ‘张三’ AND age = 20
(使用索引) -
WHERE age = 20
(不使用索引,因为跳过了name
) -
WHERE name LIKE ‘张%’
(使用索引)
-
-
-
全文索引(FULLTEXT)
- 用于全文搜索,主要用于查找文本中的关键字,而不是直接比较索引中的值。适用于
MATCH AGAINST
操作。
- 用于全文搜索,主要用于查找文本中的关键字,而不是直接比较索引中的值。适用于
3. 建立共识
-
磁盘中扇区通常是 512 字节,而操作系统与磁盘的 IO 过程通常以 8 个扇区,也就是 4KB 大小进行交互。
-
而 MySQL和磁盘进行数据交互的基本单位是 16KB。
-
MySQL 中的数据文件,是以 page 为单位保存在磁盘当中的。
-
单个 page 中不仅存储数据记录,还存储索引信息(这里的索引信息不是指B+树那种用于在页与页之间导航的索引(如指向其他页的指针和键值),而是指页内的元数据和定位信息),用来优化在单个 page 中查找数据的效率。
-
一个数据页(Data Page)内部确实有自己的一套页内组织结构(例如,一个叫做 Slot Array 或 行偏移量表 的结构),用来快速定位页内的每一条记录。这确实优化了在单个Page内的查找效率。
-
只有叶子节点保存数据,非叶子节点只保存索引信息。
-
-
MySQL 的 CURD 操作,都需要通过计算,找到对应的插入位置,或者找到对应要修改或者查询的数据。而只要涉及计算,就需要CPU参与,而为了便于CPU参与,一定要能够先将数据移动到内存当中,所以在特定时间内,数据一定是磁盘中有,内存中也有。后续操作完内存数据之后,以特定的刷新策略,刷新到磁盘。而这时,就涉及到磁盘和内存的数据交互,也就是IO了。而此时IO的基本单位就是Page(16KB)。
-
为了更好的进行上面的操作, MySQL 服务器在内存中运行的时候,在服务器内部,就申请了被称为 Buffer Pool 的的大内存空间(内存池),来进行各种缓存。其实就是很大的内存空间,来和磁盘数据进行IO交互。
4. 索引为什么能够提高查询速度?
索引的核心在于它通过特定的数据结构(如 B+Tree)将无序的数据变得有序,从而可以使用高效的查找算法。
以最常用的 B+Tree 索引为例:
-
有序性:B+Tree 的叶子节点存储了所有的数据(或数据的指针),并且这些叶子节点形成了一个有序的双向链表。
-
多路平衡查找树:B+Tree 是一个矮胖的树,它有很多分支(路)。这意味着在庞大的数据集中,只需要很少的几次 I/O 操作(比如 3-4 次)就能从根节点定位到目标数据,避免了全表扫描的巨额 I/O 开销。
简单过程:
假设我们在 age
字段上建立了索引,要查找 age = 30
的所有记录。
-
数据库引擎从索引树的根节点开始。
-
比较根节点的值,决定下一步要查找哪个分支(比如,去
20-40
这个分支)。 -
再比较分支节点的值,继续向下,直到找到
age=30
的叶子节点。 -
从叶子节点中获取到对应数据行的位置(或直接获取数据),然后返回结果。
这个过程远比从磁盘中逐行扫描要快得多。
4.1 一个问题?
Mysql 索引如果是主键我可以理解每个索引中存储的就是主键值,那如果是年龄呢?比如age=30,万一表中很多age=30的,那怎么存储?
4.1.1 核心概念:二级索引的存储结构
在InnoDB存储引擎中(这是MySQL最常用的引擎),所有索引都使用B+树结构。关键的区别在于:
-
主键索引(聚簇索引):叶子节点存储的是完整的数据行。
-
二级索引(如你创建的
age
索引):叶子节点存储的是两部分内容:-
索引列的值(在你的例子中就是
age
的值,比如30
) -
对应数据行的主键值
-
所以,一个age
索引的B+树叶子节点里,存的不是整行数据,而是 (age, 主键)
这样的组合。
4.1.2 当 age=30 有很多条时,如何存储?
假设我们有一张表 users
:
CREATE TABLE users (
id INT PRIMARY KEY, -- 主键
name VARCHAR(100),
age INT,
INDEX idx_age (age) -- 为age字段创建的二级索引
);
表中数据如下:
id(主键) | name | age |
---|---|---|
1 | 张三 | 30 |
3 | 李四 | 30 |
5 | 王五 | 25 |
7 | 赵六 | 30 |
9 | 孙七 | 28 |
那么,idx_age
这个索引在底层可能的存储结构(简化版B+树)是这样的:
叶子节点:
[ (25, 5) ] -> [ (28, 9) ] -> [ (30, 1), (30, 3), (30, 7) ]
-
每个条目都是
(age, id)
。 -
所有
age=30
的记录会被聚集存储在同一个或相邻的叶子节点中,并按主键id
排序(即1, 3, 7
)。
4.1.3 查询过程是怎样的?
SELECT * FROM users WHERE age = 30;
-
索引查找:存储引擎首先在
idx_age
这棵B+树中快速定位到age=30
的叶子节点。 -
获取主键列表:它找到了所有
(30, 1)
,(30, 3)
,(30, 7)
这样的条目。 -
回表查询:由于
SELECT *
需要获取所有列的数据,而idx_age
索引中只存储了age
和id
。因此,引擎必须拿着找到的每一个主键值(1, 3, 7
),回到主键索引(聚簇索引) 的B+树中再去查找一遍,以获取完整的数据行。 -
返回结果:将从主键索引中取出的完整行数据返回给客户端。
这个过程被称为 回表。这也是为什么在有很多重复值的列上建索引,查询效率可能不高的原因之一,因为会产生大量的回表操作。
举个例子:低索引场景
执行 SELECT * FROM users WHERE gender = '男';
-
在
gender
索引中,我们找到的不是一个点,而是一个范围。假设表中有10000条记录,其中5000条是男性。 -
数据库会定位到索引中第一个
gender='男'
的条目,然后沿着叶子节点的链表向后扫描,依次取出5000个对应的主键ID(比如id=8, 23, 47, 109, ... , 9897
)。 -
现在,数据库需要根据这 5000个分散的主键,回到聚簇索引中进行 5000次 回表查询。
这5000次回表查询,意味着磁盘磁头需要在聚簇索引的B+树上进行5000次近乎随机的定位。即使这些ID在 gender
索引的 B+ Tree 中是顺序排列的,但它们在聚簇索引(按主键物理存储)的 B+ Tree 中的位置是完全无序、分散的。
4.1.4 如何优化这种情况?(覆盖索引)
如果你的查询不需要获取所有列,可以创建一个覆盖索引 来避免回表。
例如,你只查询 id
和 age
:
SELECT id, age FROM users WHERE age = 30;
这时,因为 idx_age
索引的叶子节点上已经包含了查询所需的所有数据(age
和 id
),存储引擎在 idx_age
索引里就能拿到结果,不需要再回表。这个查询会非常快。
你甚至可以为了特定查询创建更优化的覆盖索引:
# 比如你经常根据年龄查名字
SELECT name FROM users WHERE age = 30;
# 可以创建一个包含name的索引
CREATE INDEX idx_age_name ON users (age, name);
这个 idx_age_name
索引的叶子节点存储的是 (age, name, id)
,上面的查询同样不需要回表。
4.1.5 总结
特性 | 主键索引(聚簇索引) | 二级索引(如age索引) |
---|---|---|
叶子节点存储内容 | 完整的数据行 | 索引列的值 + 主键值 |
如何处理重复值 | 主键本身唯一,无重复 | 将重复的索引值与它们各自对应的不同主键组合在一起存储 |
查询流程 WHERE age=30 | 不适用,除非主键是age | 1. 在age索引树找到所有(30, 主键) 2. 回表:用主键回主键索引树取完整数据 |
所以,age=30
有很多条时,索引中存储的是多个 (30, 主键1)
,(30, 主键2)
… 这样的条目,并通过B+树高效地链接在一起。查询时,先通过索引找到这些主键,然后再通过主键去获取实际数据。
5. 索引操作
5.1 创建主键索引
1️⃣ 第一种方式
-- 在创建表的时候,直接在字段名后指定 primary key
create table user1(id int primary key, name varchar(30));
2️⃣ 第二种方式
-- 在创建表的最后,指定某列或某几列为主键索引
create table user2(id int, name varchar(30), primary key(id));
3️⃣ 第三种方式
create table user3(id int, name varchar(30));
-- 创建表以后再添加主键
alter table user3 add primary key(id);
主键索引的特点:
-
一个表中,最多有一个主键索引,当然也可以是复合主键。
-
主键索引的效率最高(主键不可重复)。
-
创建主键索引的列,它的值不能为 null,且不能重复。
-
主键索引的列基本上是int。
5.2 创建唯一键索引
1️⃣ 第一种方式
-- 在表定义时,在某列后直接指定unique唯一属性。
create table user4(id int primary key, name varchar(30) unique);
2️⃣ 第二种方式
-- 创建表时,在表的后面指定某列或某几列为unique
create table user5(id int primary key, name varchar(30), unique(name));
3️⃣ 第三种方式
create table user6(id int primary key, name varchar(30));
alter table user6 add unique(name);
唯一索引的特点:
-
一个表中,可以有多个唯一索引。
-
查询效率高。
-
如果在某一列建立唯一索引,必须保证这列不能有重复数据。
-
如果一个唯一索引上指定not null,等价于主键索引。
5.3 创建普通索引
1️⃣ 第一种方式
create table user8(id int primary key, name varchar(20),email varchar(30),index [nikename] (name) --在表的定义最后,指定某列为索引
);
2️⃣ 第二种方式
create table user9(id int primary key, name varchar(20), email varchar(30));
alter table user9 add index(name); --创建完表以后指定某列为普通索引
3️⃣ 第三种方式
create table user10(id int primary key, name varchar(20), email varchar(30));
-- 创建一个索引名为 idx_name 的索引
create index idx_name on user10(name);
普通索引的特点:
-
一个表中可以有多个普通索引,普通索引在实际开发中用的比较多。
-
如果某列需要创建索引,但是该列有重复的值,那么我们就应该使用普通索引。
5.4 查询索引
1️⃣ 第一种方式
show keys from tablename;
2️⃣ 第二种方式
show index from tablename;
5.5 删除索引
5.5.1 删除主键索引
alter table 表名 drop primary key;
5.5.2 其他索引删除
alter table tablename drop index 索引名
索引名就是 show keys from 表名
中的 Key_name 字段。
5.5.3 还有一种方法
drop index 索引名 on 表名
5.6 索引创建的原则
-
比较频繁作为查询条件的字段应该创建索引。
-
唯一性太差的字段不适合单独创建索引,即使频繁作为查询条件。
-
更新非常频繁的字段不适合作创建索引。
-
不会出现在where子句中的字段不该创建索引。