索引是用来提高数据库的性能的,但查询速度的提高是以插入、更新、删除的速度为代价的,这些写操作,增加了大量的IO。所以它的价值在于提高一个海量数据的检索速度。
1. 认识磁盘
MySQL 给用户提供存储服务,而存储的都是数据,数据在磁盘这个外设当中。磁盘是计算机中的一个机械设备,相比于计算机其他电子元件,磁盘效率是比较低的,在加上IO本身的特征,可以知道提高效率是 MySQL 的一个重要话题。
具体的介绍请看这篇【Linux系统与系统编程】11.磁盘文件系统_硬盘文件系统-CSDN博客
磁盘随机访问(Random Access)与连续访问(Sequential Access)
随机访问
本次IO所给出的扇区地址和上次IO给出扇区地址不连续,这样的话磁头在两次IO操作之间需 要作比较大的移动动作才能重新开始读/写数据。
连续访问
本次IO给出的扇区地址与上次IO结束的扇区地址是连续的,那磁头就能很快的开始这次 IO操作,这样的多个IO操作称为连续访问。
因此尽管相邻的两次IO操作在同一时刻发出,但如果它们的请求的扇区地址相差很大的话也只能称为随机访问,而非连续访问。
磁盘是通过机械运动进行寻址的,随机访问不需要过多的定位,故效率比较高。
2. MySQL 与磁盘交互基本单位
MySQL 作为一款应用软件,可以想象成一种特殊的文件系统。它有着更高的IO场景,为了提高基本的IO效率,MySQL进行IO的基本单位是16KB。这个基本数据单元,在MySQL这里叫做page。
MySQL 中的数据文件是以page为单位保存在磁盘当中的,而 MySQL 的 CURD 操作需要通过计算找到对应的插入位置,或者找到对应要修改或者查询的数据。而只要涉及计算,就需要CPU参与,而为了便于CPU参与,一定要能够先将数据移动到内存当中。 所以在特定时间内,数据一定是磁盘中有,内存中也有。后续操作完内存数据之后,以特定的刷新 策略,刷新到磁盘。而这时,就涉及到磁盘和内存的数据交互,也就是IO了。而此时IO的基本单位就是Page。 为了更好的进行上面的操作, MySQL 服务器在内存中运行的时候,在服务器内部,就申请了被称为 Buffer Pool 的的大内存空间,来进行各种缓存。其实就是很大的内存空间,来和磁盘数据进行IO交互。 为何更高的效率,一定要尽可能的减少系统和磁盘IO的次数。
mysql> show global status like 'innodb_page_size';
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| Innodb_page_size | 16384 | -- 16 * 1024 = 16384
+------------------+-------+
1 row in set (0.00 sec)
3. 索引的理解
3.1 初识索引
首先我们建立一张测试的数据表:
-- 创建测试表
mysql> create table user(
-> id int primary key, -- 要添加主键才能产生索引
-> age int not null,
-> name varchar(20) not null
-> );
Query OK, 0 rows affected (0.03 sec)
mysql> show create table user \G;
*************************** 1. row ***************************
Table: user
Create Table: CREATE TABLE `user` (
`id` int NOT NULL,
`age` int NOT NULL,
`name` varchar(20) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
1 row in set (0.00 sec)
-- 插入数据(不按顺序插入)
mysql> insert into user (id, age, name) values(3, 18, '杨过');
Query OK, 1 row affected (0.01 sec)
mysql> insert into user (id, age, name) values(4, 16, '小龙女');
Query OK, 1 row affected (0.01 sec)
mysql> insert into user (id, age, name) values(2, 26, '黄蓉');
Query OK, 1 row affected (0.01 sec)
mysql> insert into user (id, age, name) values(5, 36, '郭靖');
Query OK, 1 row affected (0.01 sec)
mysql> insert into user (id, age, name) values(1, 56, '欧阳锋');
Query OK, 1 row affected (0.01 sec)
-- 查看插入结果
mysql> select * from user;
+----+-----+-----------+
| id | age | name |
+----+-----+-----------+
| 1 | 56 | 欧阳锋 |
| 2 | 26 | 黄蓉 |
| 3 | 18 | 杨过 |
| 4 | 16 | 小龙女 |
| 5 | 36 | 郭靖 |
+----+-----+-----------+
5 rows in set (0.01 sec)
我们可以发现我们随机插入的数据,但是插入的数据确是按照id排好序的,这是为什么呢?这就是索引的作用。
3.2 理解索引
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
MySQL 中要管理很多数据表文件需要先描述,在组织。
在 MySQL 中所有的page都是16KB ,使用 prev 和 next 构成双向链表。MySQL 会默认按照主键给我们的数据进行排序,从上面的Page内数据记录可以看出,数据是有序且彼此关联的。
• 多个page
上面页模式在查询某条数据的时候直接将一整页的数据加载到内存中以减少硬盘IO次数,从而提高性能。可以看到现在的页模式内部采用了链表的结构,前一条数据指向后一条数据,本质上还是通过数据的逐条比较来取出特定的数据。
如果有1千万条数据的话,就一定需要多个Page来保存千万条数据,多个Page彼此使用双链表链接起来,而且每个Page内部的数据也是基于链表的。那么查找特定一条记录也一定是线性查找。这效率也太低了吧。
这就引出了多page情况:
• 页目录
对于多page情况,如果我们想要查看某一个page的话就需要遍历查询啊,这效率也有点低吧。
于是引入了页目录:
也就是说存在一个目录页来管理页目录,目录页中的数据存放的就是指向的那一页中最小的数据。其实目录页的本质也是页,普通页中存的数据是用户数据,而目录页中存的数据是普通页的地址。
可是我们每次检索数据的时候该从哪里开始呢?虽然顶层的目录页少了,但是还要遍历啊?可以在加目录页。
上述结构就是传说中的B+树!至此,我们已经给我们的表user构建完了主键索引。随便找一个id=?我们发现,现在查找的Page数一定减少了,也就意味着IO次数减少了,那么效率也就提高了。
3.3 B树 vs B+树
• B树
• B+树
我们可以看出:
• B树节点,既有数据,又有Page指针,而B+树只有叶子节点有数据,其他目录页只有键值和 Page指针。
• B+树叶子节点全部相连,而B没有。
• B+树节点不存储data,这样一个节点就可以存储更多的key。可以使得树更矮,所以IO操作次数更少。 叶子节点相连,更便于进行范围查找 。
3.4 聚簇索引 VS 非聚簇索引
MyISAM 引擎同样使用B+树作为索引结果,叶节点的data域存放的是数据记录的地址。下图为 MyISAM 表的主索引, Col1 为主键。
MyISAM 最大的特点是将索引Page和数据Page分离,也就是叶子节点没有数据,只有对应数据的地址。相较于 InnoDB 索引, InnoDB 是将索引和数据放在一起的。
MyISAM 这种用户数据与索引数据分离的索引方案,叫做非聚簇索引。InnoDB 这种用户数据与索引数据在一起索引方案,叫做聚簇索引。
下面是聚簇索引和非聚簇索引的验证:
4. 索引操作
4.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);
• 创建唯一索引
-- 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);
• 创建普通索引
-- 1.在表的定义最后,指定某列为索引
create table user8(
id int primary key,
name varchar(20),
email varchar(30),
index(name)
);
-- 2.创建完表以后指定某列为普通索引
create table user9(
id int primary key,
name varchar(20),
email varchar(30)
);
alter table user9 add index(name);
-- 3.创建一个索引名为 idx_name 的索引
create table user10(
id int primary key,
name varchar(20),
email varchar(30)
);
create index idx_name on user10(name);
• 创建全局索引
当对文章字段或有大量文字的字段进行检索时,会使用到全文索引。MySQL提供全文索引机制,但是有要求,要求表的存储引擎必须是MyISAM,而且默认的全文索引支持英文,不支持中文。如果对中文进行全文检索,可以使用sphinx的中文版(coreseek)。
CREATE TABLE articles (
id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY,
title VARCHAR(200),
body TEXT,
FULLTEXT (title,body)
)engine=MyISAM;
4.2 查询索引
-- 1.show keys from 表名;
mysql> show keys from user\G;
*************************** 1. row ***************************
Table: user
Non_unique: 0
Key_name: PRIMARY
Seq_in_index: 1
Column_name: id
Collation: A
Cardinality: 4
Sub_part: NULL
Packed: NULL
Null:
Index_type: BTREE
Comment:
Index_comment:
Visible: YES
Expression: NULL
1 row in set (0.01 sec)
ERROR:
No query specified
-- 2.show index from 表名;
mysql> show index from user\G;
*************************** 1. row ***************************
Table: user
Non_unique: 0
Key_name: PRIMARY
Seq_in_index: 1
Column_name: id
Collation: A
Cardinality: 4
Sub_part: NULL
Packed: NULL
Null:
Index_type: BTREE
Comment:
Index_comment:
Visible: YES
Expression: NULL
1 row in set (0.00 sec)
ERROR:
No query specified
-- 3.desc 表名
mysql> desc user;
+-------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| id | int | NO | PRI | NULL | |
| age | int | NO | | NULL | |
| name | varchar(20) | NO | | NULL | |
+-------+-------------+------+-----+---------+-------+
3 rows in set (0.00 sec)
4.3 删除索引
-- 1. 删除主键
alter table 表名 drop primary key;
-- 2. 删除普通索引
alter table 表名 drop index 索引名;
-- 3. drop删除
drop index name on user8;