数据库表中存储的数据都是以记录为单位的,如果在查询数据时直接一条条遍历表中的数据记录,那么查询的时间复杂度将会是 O(N)
。
索引
的价值在于提高海量数据的检索速度,只要执行了正确的创建索引的操作,查询速度就可能提高成百上千倍。当一张表创建索引后,在数据库底层就会为表中的数据记录构建特定的数据结构,后续在查询表中数据时就能通过查询该数据结构快速定位到目标数据。
索引虽然提高了数据的查询速度,但在一定程度上也会降低数据增删改的效率,因为这时在对表中的数据进行增删改操作时,除了需要进行对应的增删改操作之外,可能还需要对底层建立的数据结构进行调整维护。
常见的索引分为:主键索引(primary key)
、唯一索引(unique)
、普通索引(index)
、全文索引(fulltext)
。
索引的价值
先创建一张海量表,在查询的时候,看看没有索引时有什么问题?
-- index_data.sql
drop database if exists `bit_index`;
create database if not exists `bit_index` default character set utf8;
use `bit_index`;
-- 构建一个8000000条记录的数据
-- 构建的海量表数据需要有差异性,所以使用存储过程来创建, 拷贝下面代码就可以了,暂时不用理解
-- 产生随机字符串
delimiter $$
create function rand_string(n INT)
returns varchar(255)
begin
declare chars_str varchar(100) default
'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ';
declare return_str varchar(255) default '';
declare i int default 0;
while i < n do
set return_str =concat(return_str,substring(chars_str,floor(1+rand()*52),1));
set i = i + 1;
end while;
return return_str;
end $$
delimiter ;
-- 产生随机数字
delimiter $$
create function rand_num( )
returns int(5)
begin
declare i int default 0;
set i = floor(10+rand()*500);
return i;
end $$
delimiter ;
-- 创建存储过程,向雇员表添加海量数据
delimiter $$
create procedure insert_emp(in start int(10),in max_num int(10))
begin
declare i int default 0;
set autocommit = 0;
repeat
set i = i + 1;
insert into EMP values ((start+i)
,rand_string(6),'SALESMAN',0001,curdate(),2000,400,rand_num());
until i = max_num
end repeat;
commit;
end $$
delimiter ;
-- 雇员表
CREATE TABLE `EMP` (
`empno` int(6) unsigned zerofill NOT NULL COMMENT '雇员编号',
`ename` varchar(10) DEFAULT NULL COMMENT '雇员姓名',
`job` varchar(9) DEFAULT NULL COMMENT '雇员职位',
`mgr` int(4) unsigned zerofill DEFAULT NULL COMMENT '雇员领导编号',
`hiredate` datetime DEFAULT NULL COMMENT '雇佣时间',
`sal` decimal(7,2) DEFAULT NULL COMMENT '工资月薪',
`comm` decimal(7,2) DEFAULT NULL COMMENT '奖金',
`deptno` int(2) unsigned zerofill DEFAULT NULL COMMENT '部门编号'
);
-- 执行存储过程,添加8000000条记录
call insert_emp(100001, 8000000);
上述的SQL中创建了一个名为index_demon的数据库,并在该数据库中创建了一个名为EMP的员工表,并向表中插入了800w条记录。
将上述SQL保存到文件中,然后在MySQL中使用source命令依次执行文件中的SQL即可。
通过desc命令可以看到,目前EMP员工表中没有建立任何索引
这里我们看到每次查询指定工号的员工信息,都需要花费5秒多的时间。
当我们给员工表中的工号建立索引后,数据库底层就会为员工表中的数据构建特定的数据结构。
这时再查询EMP表中指定工号的员工信息,可以看到几乎检测不到查询时耗费的时间:
这是因为给员工工号创建索引后再根据员工工号来查询数据,这时就可以直接通过底层建立的数据结构来快速定位到目标数据,从而提高数据的检索速度,这就是索引的价值。
磁盘中的一个盘片
说明一下:
定位扇区
定位扇区时采用的是 CHS寻址
方式:
简单来说,CHS寻址方式就是先通过H确定数据所在的盘面,再通过C确定数据所在的磁道,最后通过C确定数据所在的磁道,最后通过S定位到目标扇区。
MySQL与磁盘交互的基本单位
MySQL 作为一款应用软件,可以想象成一种特殊的文件系统。它有着更高的 IO 场景,所以为了提高基本的 IO 效率, MySQL 进行 IO 的基本单位是 16KB(后面统一使用 InnoDB 存储引擎讲解)。
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)
也就是说,磁盘这个硬件设备的基本单位是 512 字节,而 MySQL InnoDB 引擎使用 16KB 进行 IO 交互。即 MySQL 和磁盘进行数据交互的基本单位是 16KB 。这个基本数据单元,在 MySQL 这里叫做 page(注意和系统的 page 区分)。
因此所谓的操作系统和磁盘交互的基本单位是4KB,指的是内核缓冲区与磁盘之间是以4KB为单位进行交互的,而MySQL的Buffer Pool和磁盘实际并不是直接交互的,因此所谓的MySQL与磁盘交互的基本单位是16KB,指的是MySQL的Buffer Pool与内核缓冲区之间是以16KB为单位进行交互的。只不过在说的时候更关注的是MySQL和磁盘之间的关系,所以直接说的是MySQL与磁盘交互的基本单位是16KB,相当于忽略了中间的内核缓冲区。
创建一个用户表,表当中包含用户的id、年龄和姓名,并将用户的id设置为主键。如下:
mysql> create table user(
-> id int primary key,
-> age int not null,
-> name varchar(16) not null
-> );
Query OK, 0 rows affected (0.02 sec)
创建完表后向表中插入一些数据,并插入数据时没有按照主键大小顺序进行插入,如下:
但最终我们查看表中数据时,却发现显示出来的数据是按照主键进行有序排列的。
为什么MySQL与磁盘交互的基本单位是Page?
也就是说,MySQL与磁盘进行交互时以Page为基本单位,可以减少与磁盘IO交互的次数,进而提高IO的效率。
MySQL 中要管理很多数据表文件,而要管理好这些文件就需要先描述再组织,我们目前可以简单理解成一个个独立文件是有一个或者多个 Page 构成的。
因为有主键的问题, MySQL 会默认按照主键给我们的数据进行排序。从上面的 Page 内数据记录可以看出,数据是有序且彼此关联的。
为什么数据库在插入数据时要对其进行排序呢?我们按正常顺序插入数据不是也挺好的吗?插入数据时排序的目的,就是优化查询的效率。
页内部存放数据的模块,实质上也是一个链表的结构,链表的特点也就是增删快,查询修改慢,所以优化查询的效率是必须的。正是因为有序,在查找的时候,从头到尾都是有效查找,没有任何一个查找是浪费的,而且如果运气好,是可以提前结束查找过程的。
单个Page内创建页内目录
Page结构体内部存储的数据记录是以单链表的形式组织起来的,当页内部的数据量增多时,本质在页内部进行的还是线性遍历,效率低下。这时可以在Page结构体内部引入页内目录,将Page结构体内部存储的数据记录按照主键划分为若干区域,页内目录中就存储着这若干区域的最小键值。在Page结构体内部引入页内目录后,在页内部查询数据时就可以先通过页内目录找到目标数据所在区域的起始记录,然后再从该记录开始向后遍历找到目标记录。
那么当前,我们在一个 Page 内部引入了目录。比如,我们要查找 id=4 记录,之前必须线性遍历 4 次才能拿到结果。现在直接少量的遍历找到目录 2[3],直接进行定位新的起始位置,提高了效率。现在我们可以再次正式回答上面的问题了,MySQL 为何会通过键值会自动排序?因为只有 Page 内部的数据是有序的,才能够方便引入页目录,提高查找速度。
多个Page
随着数据量不断增大,单个Page中无法存下所有数据,这时就需要用到多个Page来存储数据。这时在查询数据时就需要先遍历Page双链表确定目标数据在哪一个Page,然后再在该Page内部找到目标数据。
Page之上创建页目录
虽然在单个Page内部能够通过页内目录来快速定位数据,但在遍历Page双链表寻找目标Page时本质进行的还是线性遍历。这时可以给各个Page结构体也建立页目录,页目录中的每个目录项都指向一个Page,而这个目录项存放的就是其指向的Page中存放的最小数据的键值。
在给各个Page结构体建立页目录后,在查询数据时就可以先通过遍历页目录找到目标数据所在的Page,然后再在该Page内部找到目标数据。
这里的页目录与之前的页内目录的区别在于,页目录管理的是一个个的Page,而页内目录管理的是一条条的记录。此外,页内目录与其管理的多条记录是保存在同一个Page中的,而页目录是重新申请的一个Page结构体来保存的。
随着数据量不断增大,Page变得越来越多,这时一个页目录无法管理所有的Page,这时就需要更多个的页目录。这些页目录也是一个个的Page结构体,只不过这些Page结构体中存放的不是数据记录,而是各个Page的目录信息。但是在MySQL看来,无论Page当中存储的是什么数据,都应该被管理起来,因此这些Page页目录也需要用双链表连接起来。
页目录之上再次创建页目录
就算给各个Page结构体也建立了页目录,但随着数据量不断增大,页目录的数量也会越来越多,这时在遍历页目录寻找目标Page时本质进行的还是线性遍历。
类似的,我们可以不断在页目录之上再创建页目录,最终就一定能够得到一个入口页目录,这时在查询数据时就可以从入口页目录开始不断查询页目录,最终找到目标数据所在的Page,然后再在该Page内部找到目标数据。
其实这就是传说中的 B+ 树啊!没错,至此我们已经给我们的表 user 构建完了主键索引。索引本质就是 B+ 树。随便找一个 id,我们可以发现,现在查找的 Page 数一定减少了,也就意味着 IO 次数减少了,那么效率也就提高了。其实操作系统中的页表本质上也是B+树结构。
除了InnoDB存储引擎所采用的B+树结构,索引结构还可以采用哪些数据结构呢?
聚簇索引(Clustered Index)
聚簇索引是将表的数据行直接存储在索引中,而不是将数据和索引分开存储。在聚簇索引中,数据行的物理存储顺序与索引顺序一致,因此表的数据行实际上是按照聚簇索引的键值顺序来排列的。每个表只能有一个聚簇索引,通常是主键索引,因为主键是表中的唯一标识。由于数据行和索引行在聚簇索引中存储在一起,因此聚簇索引能够提供较快的数据检索性能,特别是在范围查询和排序操作时。
非聚簇索引(Non-Clustered Index)
非聚簇索引是将索引和数据分开存储。在非聚簇索引中,索引行包含了索引键值以及指向对应数据行的指针(一般是数据行的物理地址或主键值)。每个表可以有多个非聚簇索引,用于加速特定列的检索。由于数据和索引在非聚簇索引中分开存储,因此非聚簇索引的数据行在物理上是随机散落存储的。因此,当进行范围查询或排序操作时,非聚簇索引的性能可能较聚簇索引稍慢。
MyISAM 引擎同样使用 B+ 树作为索引结果,叶节点的data域存放的是数据记录的地址。下图为 MyISAM 表的主索引
, Col1 为主键。
其中, MyISAM 最大的特点是,将索引 Page 和数据 Page 分离,也就是叶子节点没有数据,只有对应数据的地址。相较于 InnoDB 索引, InnoDB 是将索引和数据放在一起的。
对于 MyISAM
,建立辅助(普通)索引
和主键索引没有差别,无非就是主键不能重复,而非主键可重复。下图就是基于 MyISAM 的 Col2 建立的索引,和主键索引没有差别。
InnoDB存储引擎的普通索引
采用的也是B+树结构,但普通索引的B+树中的键值可以重复,并且B+树的叶子结点中存储的不是数据记录,而是对应数据记录的主键值。当根据普通索引查询数据时,会先查找普通索引对应的B+树找到目标记录的主键值,然后再查找主键索引对应的B+树找到目标记录,这个过程就叫做回表查询。
当采用innoDB存储引擎创建表时,在数据库对应的目录下会新增两个文件。如下:
当采用MyISAM存储引擎创建表时,在数据库对应的目录下会新增三个文件。如下:
说明一下:
xxx.frm
文件,该文件中存储的是表结构相关的信息。xxx.ibd
文件,该文件中存储的是索引和数据相关的信息,这就是所谓的聚簇索引,索引和数据是存储在同一个文件中的。xxx.MYD
文件和一个xxx.MYI
文件,其中xxx.MYD
文件中存储的是数据相关的信息,而xxx.MYI
文件中存储的是索引相关的信息,这就是所谓的非聚簇索引, 索引和数据是分开存储的。方式一
创建表时,直接在对应的字段名后指定primary key。如下:
mysql> create table user1(
-> id int primary key,
-> name varchar(20)
-> );
方式二
在创建表的最后,指定某列或者某几列为主键索引
mysql> create table user2(
-> id int,
-> name varchar(20),
-> primary key(id)
-> );
方式三
创建表后,使用alter命令给指定字段添加主键索引。
主键索引的特点:
方式一
在创建表时,直接在对应的字段名后指定unique。
mysql> create table user4(
-> id int primary key,
-> name varchar(20) unique
-> );
方式二
在创建表的最后,指定某一列或某几列为唯一索引。
mysql> create table user5(
-> id int primary key,
-> name varchar(20),
-> unique(name)
-> );
方式三
在创建表后,使用alter命令给指定字段添加唯一索引。
唯一索引的特点:
NOT NULL
属性,则等价于主键索引。方式一
在创建表的最后,指定某列或某几列为普通索引。
mysql> create table user7(
-> id int primary key,
-> name varchar(20),
-> index(name)
-> );
方式二
创建表后,使用alter命令给指定字段添加普通索引。
方式三
创建表后,使用create命令给指定字段创建普通索引,并指定索引名。
普通索引的特点:
全文索引比较常见的索引案例就是对文章中的词进行搜索,比如下面创建一个文章表,表当中包含文章的id、文章名称、文章内容,并在创建表的最后通过fulltext
给title
和body
列创建全文索引。
下面向表当中插入一些测试数据。
如果要查询那些文章中包含database关键字,我们可以通过模糊匹配进行查找。
实际上这种查找方式并没有用到全文索引,在SQL语句前面加上explain,可以看到key对应的值为NULL,表示这条SQL在执行的过程中没有用到任何索引。
如果要通过全文索引来查询,需要使用 match against
进行搜索。如下:
在这条SQL语句前面加上explain,可以看到key对应的值为title,表示这条SQL在执行过程中用到了索引名为title的索引。如下:
说明一下:
方式一
使用 show keys from
表明 SQL 查询,比如查询 articles表中的索引信息。
说明一下:
Table
: 表示创建索引的表的名称。Non_unique
: 表示该索引是否是唯一索引,如果是则为0,如果不是则为1。Key_name
: 表示索引的名称。Seq_in_index
: 表示该列在索引中的位置,如果索引是单列的,则该列的值为1,如果索引是复合索引,则该列的值为每列在索引定义中的顺序。Column_name
: 表示定义索引的列字段。Collation
: 表示列以何种顺序存储在索引中,“A”表示升序,NULL表示无分类。Cardinality
: 索引中唯一值数目的估计值。基数根据被存储为整数的统计数据计数,所以即使对于小型表,该值也没有必要是精确的。基数越大,当进行联合时,MySQL使用该索引的机会就越大。Sub_part
: 表示列中被编入索引的字符的数量,若列只是部分被编入索引,则该列的值为被编入索引的字符的数目,若整列被编入索引,则该列的值为NULL。Packed
: 指示关键字如何被压缩。若没有被压缩,则值为NULL。Null
: 用于显示索引列中是否包含NULL,若包含则为YES,若不包含则为NO。Index_type
: 显示索引使用的类型和方法(BTREE、FULLTEXT、HASH、RTREE)。Comment
: 显示评注。方式二
使用 show index from 表名
SQL 查询,比如查询articles表中的索引信息。
方式三
使用 desc 表名
SQL查询(信息比较简略)。
创建一个用户表用于测试索引的删除,表中包含用户的id、姓名和邮箱,并将这三列分别设置为主键索引、唯一索引和普通索引。如下:
删除主键索引
使用 alter table 表名 drop primary key
SQL即可删除主键索引。
alter table user10 drop primary key;
删除非主键索引
使用 alter table 表名 drop index 索引名
SQL即可删除指定的非主键索引。
此外,也可以使用 drop index 索引名 on 表名
SQL也可以删除指定的非主键索引。
drop index email on user10;
说明一下:
索引创建的原则如下:
时刻要记住,创建索引的目的就是为了提高查询的效率。