什么是索引
在面对海量数据问题时,不同的语言都有类似的解决方法,比如构建一个数据结构来存储数据 ,这个数据结构也许是查找十分方便,如二叉树,堆,并查集等,也许是插入十分方便,如顺序表,链表,栈等,也有的数据可以采用一些特殊的算法来查找 ,比如有序的数据内可以通过二分查找来寻找对应的数据,简而言之,面对海量数据问题,一般有两种解决方法:建立特殊的数据结构存储或者采用特殊的算法来寻找数据。
而数据库应该是接触海量数据问题最多的了,因为数据库不仅需要提供用户存储数据,还要快速响应用户的查询,因此mysql也为海量数据问题提供了相应的解决方法------索引。
MySQL中的索引
- 主键索引(primary key)
- 唯一键索引(unique)
- 普通索引(index)
- 全文索引(fulltext)
因此从上述的解决海量数据的方法来看,我们能够明白,索引实际上就是一种特殊的数据结构,通过索引来查询数据能够十分快速的获得目标数据。
一般来说,采用了索引来查询的速度比没采用索引的速度快好几个数量级,特别是在海量数据的情况下来说。
认识硬件
之前在初识MySQL数据库时,曾了解过数据库实际上就是在磁盘上创建一个文件夹,而表就是一个文件夹内部的文件,而这些文件都是存储在磁盘上的。
也就是说mysql的数据实际上也是存储在磁盘上的,因此想要提升效率,也必须理解硬件方面的结构。
磁盘结构

一个磁盘就是由很多个盘片组成,每一个盘片上都有很多个同心圆**,这些同心圆就是磁道(柱面就是磁道)**。
而一个磁盘又分为很多段,每一段又被称为一个扇区。
如果操作系统想要提取文件,只需要知道文件对应的柱面、磁头、和扇区即可,这就是 CHS 方式
不过软件层面上使用的是 LBA方式,即将物理地址在逻辑上变成线性地址,然后通过文件的 inode 以及系统的转换来寻找文件。
一般来说,一个扇区的大小是512字节,但是这个数据太少了,为了减少寻找次数,操作系统一般一次提取 4k 大小的数据,不仅与硬件进行了解耦,也减少了磁盘的访问。
除此之外,操作系统的IO又分为随机访问和连续访问。
随机访问:上一次IO的扇区地址和本次的扇区地址不连续,磁头需要较大的动作来寻找扇区
连续访问:两次IO的扇区地址连续,很容易就能够进行IO
因此如果有进行文件IO的操作时,最好是连续IO。
MySQL基本交互单位
MySQL作为一个应用软件,可以想象成一个特殊的文件系统,为了提高IO效率,它一般会一次让OS提取16k的数据给它,也就是说MySQL和磁盘交互的基本单位是 16k,这个单位MySQL中叫page。
建立共识
- mysql一次以page为单位保存到磁盘中
- mysql 的 CURD操作都需要通过计算来找到文件插入的位置或者找到想要插入的数据
- 由于涉及了计算,因此需要CPU参与,为了能够让CPU参与,一定需要将数据移动到内存中,而MySQL在运行时,会申请一个名为 Buffer Pool 的内存空间,用来和磁盘数据进行数据交换
- 为了更高的效率,一定需要减少系统和磁盘IO的次数
一般这个申请的空间大小是128M大小,可能不同的引擎不同。
可以进入到 /etc/my.cnf 中查看大小。

采用page进行IO交互的原因
根据局部性原理,一般一次搜索的数据,下一次搜索所需要的数据有很大的概率在一个page中,因此采用page进行IO交互。
索引的理解
我们直接创建一个带有主键的表,然后无序的插入数据。

最后查询的时候发现数据是有序的。

也就是说一个page内部存储的数据是按索引进行排序的,如果没有索引,那么是插入的顺序就是查询的顺序。
单个page
在linux中,操作系统对于任何东西都是通过先描述再组织的方式来控制的。
而mysqld内部有很多文件,想管理好这些文件,就需要先描述再组织,也就是一个文件是由多个page来构成的。
而在mysql中,这些page是采用了某种方式关联在一起,让mysql进行管理。
一般单个的page之间采用的是链式结构关联,一个page内部还有 prev 指针和 next 指针,关联其不同的page。

而一个page中存储的数据,都会按索引进行排序,如果这个数据没有索引,那么mysql会给它添加一个默认的索引,然后按默认索引进行排序(就是按插入顺序排序)。
这种有序的存储方式能够使得一个page内部的查询变得快速而有效。
多个page
mysql的buffer pool 中肯定不止一个page,因为一个page只有16k的大小,不能存储所有数据,一般会有很多个page,这些page互相之间都是通过指针进行关联。
而上面说过,一个 buffer pool 是128M,而一个 page 是16k,因此一个 pool 内部可能有好几千上万个page。

而一个page内部就能够存储16k的数据了,转换成字节就有 16 * 1024 = 16384 字节了,单是一个page内部就有很多条数据了,更何况一个buffer pool 中还有很多个page。
这样的管理方式能够减少磁盘IO次数,也提高了效率,但是依旧不够。
如果单纯采用链表的方式来管理page,那就只能用线性遍历的方式来查找数据,这样效率太慢了
因此mysql实际上对page和数据采用了目录的方式进行管理。
页目录
在一个page中占用一点空间,构建一个目录项,每次寻找数据都通过目录来查找。
单表情况
单表情况下,可以通过目录来快速的遍历数据,比如规定目录1中指向的是第1到2条数据的首位置,目录2指向的是3到5数据的首位置。
这样我们想查找第四条数据,就可以通过目录2直接找到第三条数据,然后再进行线性遍历。
而我们需要使用具有一定顺序的索引才能建立目录,因此mysql按索引顺序来插入顺序也是为了能够更好的使用目录。
多表情况
一个buffer pool 中一定有很多page,因此这些page也能通过一个目录来进行管理。
- 使用一个page内部的目录来指向某一页,这个目录就是存储的要指向的页中存储的最小值
- 和页内目录不同,这种目录项管理的是页,而页内目录管理的是行

而如果我们发现遍历这个目录页也很麻烦,我们还能添加目录页的目录。

这实际上就是一个B+树,也就是说,mysql内部管理page的结构实际上是采用B+树的方式管理的
而此时我们还想查询表中的某一个数据,只需要通过索引就能够快速的查找到对应主键的数据存储在哪个page中,并且能够从page中快速获取到数据。
总结
- page分为目录页和数据页,目录页只存储下个page的最小键值
- 查找的时候,自顶向下查找,只用加载部分目录页即可找到数据。
选择B+树的原因
从目前来说,我们知道MySQL底层管理数据是采用的B+树管理,但是为什么选择B+树而不是其他数据结构呢?
- 链表:链表一眼pass,不仅只能线性遍历,而且需要进行大量的磁盘IO才能获取到数据
- 二叉搜索树:二叉搜索树的查询时间复杂度是 O(logN) 级别的,但是在极端条件下,会退化成线性结构
- AVL树或者红黑树:虽然这两个树可以保证结构不会退化成线性结构,但是对比多阶B+树,这两棵树高度过高,而高度过高就意味着有更多的IO操作
- HASH表:其实MySQL中有使用HASH的存储引擎,不过在面对范围查找的时候,需要一个一个查找下来,因此只是有的存储引擎采用了HASH;
- B树:B树的节点既有数据,又有page指针,而B+树只有叶子节点存储数据,其他节点都存储的键值和page页,对比B+树,**B树的IO次数更多,**因此采用B+树,而且B树的叶子节点不相连,不利于范围查找
一个个对比下来,还是B+树好用,刚开始查询数据,只用保存一个目录项,然后通过目录项来一次性获取数据。
聚簇索引和非聚簇索引
- 聚簇索引:索引和数据不分离的就是聚簇索引
- 非聚簇索引:索引和数据分离的就是非聚簇索引
在MySQL中有多种存储引擎,有一个是Myisam引擎和innodb引擎,其中Myisam是非聚簇索引,innodb是聚簇索引。
Myisam

可以看到,非聚簇索引的叶子节点存储的是数据对应的地址,而非数据,一般都是通过这个主键获取到地址来直接找到对应的数据。
Innodb

而Innodb则是索引和数据存储在一起的。
那么这两种方法有什么区别呢?
聚簇索引和非聚簇索引的区别
一般用户创建表的时候,除了主键索引,有可能还会创建辅助索引,如果是通过主键索引来查询数据,那么聚簇索引和非聚簇索引没什么不同,但是通过辅助索引查询数据就出现了区别。
非聚簇索引的主键索引和非主键索引都一样,都是直接通过索引来获取地址,比如MyIsam中通过辅助索引 col2 来查询数据,它跟使用主键索引查询数据一样。

而如果是聚簇索引则不同,若是聚簇索引通过辅助索引来查询数据,那么聚簇索引就是获取辅助索引对应的主键索引。

然后再通过主键索引获取数据,这种操作就是回表查询。
这样就节省了空间,并且由于B+树的查询速度很快,因此对于性能的损失很低。
而Innodb针对辅助索引不添加数据的原因就是太损失空间了。
通过上面可知,当一个表中有多个索引时,一般会按索引来创建多个B+树索引 ,这样按照不同的索引查询数据就能够快速的获取数据。
索引操作
创建主键索引
- 创建表的时候设定主键索引
- create table table_name(列名1 primary key,列名2);
- 在创建表的最后指定某一列或者多列为主键索引
- create table table_name(列名1,列名2, primary key (列名1...));
- 创建表之后再添加主键
- create table table_name(列名1,列名2);
- alter table table_name add primary key (列名);
通过这三种方式能够创建主键索引,不过需要注意的是主键不能为空,也不可重复,设定的时候需要考虑。
主键索引的特点
- 一个表中,最多只有一个主键索引
- 主键索引不可重复
- 主键索引的列不可为null
- 主键索引基本上都是int类型的
唯一索引的创建
-
创建表的时候直接设定unique
- create table table_name(列名1 unique,列名2 unique);
-
创建表的时候,在表的后面指定unique
- create table table_name(列名1,列名2,unique(列名1...));
-
创建表之后,再添加unique
- create table table_name(列名1,列名2);
- alter table table_name add unique(列名...);
唯一键的特点
- 一个表中可以有多个唯一索引
- 查询效率高
- 唯一键索引所在列不可有重复数据
- 如果指定一个唯一键not null,等价于主键索引
普通索引的创建
- 创建表的后面直接设定普通索引
- create table table_name(列名1,列名2,index(列名1...));
- 创建表后指定普通索引
- create table table_name(列名1,列名2);
- alter table table_name add index(列名);
- 创建表后添加普通索引
- create table table_name(列名1,列名2);
- create index index_name on table_name(列名);
普通索引的特点
- 在开发中,普通索引的使用较多,一个表中可以有多个普通索引
- 如果一个列值可以重复,就可以使用普通索引
全文索引
全文索引是针对文章或者大量字段所用的索引,只能在MyIsam引擎才能使用该索引,该索引不支持中文,只支持英文。
我们先创建一个采用MyIsam引擎的表,然后来看看全文索引的作用。

如果直接使用select和where子句筛选,那么就没用到全文索引。
可以通过explain工具来查看是否使用全文索引

发现key值为null,也就是说没有用全文索引
使用全文索引来查询数据
select * from table_name where mathc (全文索引) against ('查询的字段');
通过explain能够发现确实采用了全文索引。

查询所有索引
-
show keys from table_name;
-
show index from table_name;
-
desc table_name;
删除索引
- 主键的删除:alter table table_name drop primary key;
- 其他索引的删除: alter table table_name drop index 索引名,该索引名就是查询索引时中的key_name字段
- drop index 索引名 on 表名
索引设立规则
- 频繁用作查询的字段可以用作索引
- 唯一性太差的字段不适合用作索引,即便它复合条件一
- 更新频繁的字段不适合创建索引
- 不会出现在where子句的字段不适合作为索引
其他的概念
- 复合索引:在之前讲解主键的时候,我们知道可以将两个列名当作主键索引,这种就是复合索引。
- 索引最左匹配原则:在创建复合索引的时候,只会按照最左边的索引来建立索引。
- 比如下图使表中的qq和name当作index索引,但是查看后发现,两个索引都只按照qq索引

- 索引覆盖:一般数据查询由于有最左匹配原则,所以在按索引查询的时候,要么就按照最左的索引查询,或者最左索引+剩下的索引匹配,而不能单独只有右边的索引。
总结
本文讲解了索引实际上就是一种特殊的数据结构,当表中有一个索引那么就会mysql底层就会根据索引创建一个B+树来存储索引和数据的键值对。然后根据是否是聚簇索引来判别是否存储数据还是存储地址。
还了解到mysql每次提取数据都是按一个page提取的(16k),也明白了索引采用B+树的理由。
其次本文也讲解了聚簇索引和非聚簇索引的区别,也讲解了各种索引的创建,删除和查看。
相信本文对各位同学会提供一定的帮助。