前面已经学习了很多sql的语法等,现在需要了解下我们输入了一条select查询语句,mysql是怎么找到该查询语句对应的数据的?另外,又是怎么快速地找到的呢?
这里就可以引出索引的概念了:索引是存储引擎用于快速找到数据记录的一种数据结构。
不知道你看清楚上面的概念了么,索引是一种数据结构!
接下来通过例子,来慢慢理解,索引是一种数据结构,到底是什么意思…
比如此时,我们有一个employees员工表,当我们输入select 语句以后,应该怎么找到这条数据呢?接下来来分析下。
select * from employees where employee_id=100;
首先先创建一个简单一点的表index_demo表,这个表里只有3个字段:c1,c2,c3,其中c1字段是主键。
CREATE TABLE index_demo(
c1 int,
c2 int,
c3 char(1),
) ROW_FORMAT = Compact;
这里采用的行格式是Compact,也就是每一行数据实际保存的格式:
创建了表之后,开始插入数据并开始查找,首先插入3条数据:
INSERT INTO index_demo VALUES
(1, 4, 'u'),
(3, 9, 'd'),
(5, 3, 'y');
mysql加载磁盘中的数据记录是按页来加载的,其中一页的大小是16KB,因此我们加载一次,最多能加载16KB的数据。
虽然我们只插入了3条数据,假设这3条数据已经到了16KB,那么就可以填满一页了,示意图如下:
可以看到,此时数据根据主键(c1)的大小,在一页中串联成一个单向链表了。如果要查找某一条数据,可以在这一页中进行顺序查找。
由于这3条数据已经填满一页了,此时再插入一条数据的话,会重新分配一页的地址来存储新的数据记录。存储好之后,由于我们是根据主键来进行排序的,所以还需要看下该条记录是否需要移动,如果需要记录移动的话,那么需要根据主键大小进行记录移动,这个过程称为分页。
比如此时又插入了一条数据。那么需要先将主键值为5的记录移动到一个新分配的页28,然后再将主键值为4的记录插入到页10。
INSERT INTO index_demo VALUES
(4, 4, 'a');
好了,接下来随着数据库记录的增加,数据库中新分配的页也会越来越多,大致如下图:
1)由于每一页在磁盘中实际可能不是连续的,因此每一页之间使用的双向链表连接。
2)至于每一页里面,由于单链表查询速度慢,因此可以再维护一个数组,用来记录每条数据的地址。如果需要查询的话,通过二分查找,会更快地找到这条该页中的这条数据。
因此如果要查找(20,2,'e’)这条数据。
1)可以先找到页10 ,然后通过2分查找发现没找到;
2)然后再通过链表找到下一页28,再通过二分查找发现还是没找到;
3)然后再通过链表找到下一页9,再通过二分查找,此时可以找到,这是返回该条数据记录。
在1.3中,可以看到多页查找存在一个很明显的问题,就是数据量增加以后,一个一个地顺序查找,速度太慢。
因此可以给每一页做一个目录项,每个目录项包括以下2个部分:
因此1.3中的多页此时可以用下图表示。
此时还是查找(20,2,'e’)这条数据的话。
1)在目录项的这一页中,通过2分查找找到目录项3。因为主键20大于目录项3的最小主键12,小鱼目录项4的最小主键209。
3)此时到目录项3中的页9中,通过2分查找可以找到这条记录。
此时可以看到,1.3中是通过一页一页地查找,每次都要加载磁盘的一页数据,耗时多。而加了目录之后,只需要加载2次磁盘的页就找到了数据。
需要说明的是,加载磁盘页的耗时,要远远大于内存中的耗时,两者的量级至少在10以上。因此此时不用纠结程序中的某个算法的时间复杂度是0(n)还是O(n2)。因为如果磁盘加载页的次数多的话,耗时是远远大于在内存中执行程序的时间。
此时,基于目录项记录的页的数据结构如下图。
可以看到第1层是目录项记录,第2层是数据记录。
目录项记录只有主键的最小值和对应页的物理地址。而数据记录则真正含有这条记录的数据。他们的区分是根据前面介绍的record_type属性进行的区分:
需要注意的是,数据记录由于包含数据,所以在一页中(16KB)存放的目录记录数往往是大于存在数据记录的数。
比如一条数据记录的大小是160B,那么一页磁盘可以存放100条数据记录。
由于一条目录记录只有该页最小主键值和该页物理地址,就2个值,假设一条的大小是16B,那么一页磁盘可以存放1000条记录。
因此此时2层的目录结构可以存放的数据记录数是:1000 * 100 = 10,0000条,也就是10万条记录。
因此10万条记录,我们在第一层中,通过2分查找可以快速定位到是那一页物理地址,然后在该页中再通过二分查找可以快速找到这条数据。因此10万条数据,大概需要加载2次磁盘页就可以找到了。
按照上面的举例,如果超过10万条数据了呢?那一个目录项记录肯定就不够了。比如如果有1亿条数据,那么就按照上面的例子的话,就需要1000个目录项记录页了:
这时如果要查找某一条数据,那么就又需要在第一层的目录项记录页这里一个一个地查找了,每一次都要加载磁盘页,这样速度非常慢。
因此可以参考上面的方法,再增加一层:目录项记录页的目录页。如下图:
查找的方法和前面类似,此时又多了一层,按照上面的例子,此时可以存储的数据数为:
1000(第1层目录项页) × 1000 (第2层目录项页)* 100 = 1,0000,0000条数据,即1亿条数据。
当然如果数据量更大,还可以继续增加层数。再增加一层的话,就可以存储1000亿条数据,对于一般的业务来说,这已经非常多了,因此一般索引的层数不会超过4层。
上面已经分析了,怎么创建一种数据结构来快速地找到数据库中的数据记录,这种数据结构大致如下图:
这种数据结构,它的名称就是B+树。
不论是存放用户记录的数据页,还是存放目录项记录的数据页,我们都把它们存放到B+树这个数据结构中,所以我们也称这些数据页为节点。从图中可以看出,实际用户记录其实都是存放在B+树的最底层的节点上,这些节点也被称为叶子节点,其余用来存放目录项的节点称为非叶子节点或者内节点,其中B+树最上边的那个节点也称为根节点。
一般情况下,我们用到的B+树都不会超过4层!
虽然上面举例过了,这里再总结一遍。
假设一条数据记录大小是160B,那么一个磁盘页(16K)最多可以存放100条数据。而目录页由于只需要存放数据记录的最小主键值和数据记录页的地址,因此一个磁盘页存放的目录项数据肯定比数据项个数多,假设能存放1000条。
因此1000亿条数据,通过主键值去查找最多只需要加载4次磁盘页(3次目录项页、1次用户数据记录页)就可以找到数据,并且每一个页面内还有Page Directory(页目录),也就是可以通过二分法快速定位,不用通过链表一个一个地查询。
通过第一小节,已经分析B+树在mysql中检索数据记录的整个过程,此时再来理解索引的概念和优缺点,可能就更好理解了。不然一上来就看着一大堆的文字描述,可能就会很懵。
所以需要说明,为什么要建索引?
从上面可以知道,建索引的目的是减少磁盘I/0的次数,加快查询效率。
索引是帮助mysql高校获取数据的数据结构,因此索引是数据结构。
索引是在存储引擎中实现的,因此每种存储引擎的索引不一定完全相同,并且每种存储引擎不一定支持所有索引类型。
同时,存储引擎可以定义每个表的最大索引数和最大索引长度。所有存储引擎支持每个表至少16个索引,总索引长度至少为256字节。有些存储引擎支持更多的索引数和更大的索引长度。
索引按照物理实现方式,可以分为2种:聚簇索引和非聚簇索引。也把非聚簇索引称为二级索引或者辅助索引。
聚簇索引并不是一种单独的索引类型,而是一种数据存储方式(所有的用户记录都存储在叶子节点),也就是所谓的索引即数据,数据即索引。
这种聚簇索引并不需要我们在mysql中显示地使用INDEX语句去创建,InnoDB存储引擎会自动地为我们创建聚簇索引。
优点:
缺点:
限制:
上边介绍的聚簇索引只能在搜索条件是主键时才能发挥作用,因为B+树中的数据都是按照主键进行排序的。那如果我们想以别的列作为搜索条件怎么办呢?
可以多建几颗B+树。
不同的B+树中的数据采用不同的排序规则,比如可以用上面例子中的c2列的大小作为数据页再建一颗B+树。
回表的概念:
根据这个以c2列大小排序的B+树只能确定我们要查找记录的主键值,所以如果想查找到完整用户记录的话,还是需要到聚簇索引中再查一遍,这个过程称为回表。
因为这种按照非主键列建立的B+树需要一次回表操作才可以定位到完整的用户记录,所以这种B+树也被称为二级索引(secondary index),或者辅助索引。
非聚簇索引的存在不影响数据子啊聚簇索引中的组织,所以一张表可以有多个非聚簇索引。
小结:
联合索引可以理解为非聚簇索引中的一种,只是它是同时为多个列建立索引。
一个B+树索引的根节点自诞生之日起,便不会再移动。也就是每当为一张表建立B+树索引时,就会创建一个根节点页面,该页面一开始存放的是用户记录,当该页满了之后,就会发生页分裂,这样用户数据就到了第2层,那么根节点页面就变为了目录项记录页了。
更通俗一点的解释就是,上面介绍的B+树,它是由上慢慢向下创建的。
如果非叶子节点,也就是内节点目录项记录是完全一致的,比如下图。
那么新来一条数据:0,1,‘c’,就不知道该插入到那一页了。
此时就需要保证在B+树的同一层内节点的目录项记录除页号这个字段以外是唯一的,那么此时可以加上主键值,这样内节点的目录项记录就一定是唯一的了:
InnoDB的一个数据页至少存放2条记录,不然上面介绍的B+树结构的方案就没有任何意义了。
MyISAM引擎使用B+树作为索引结构,但是它的叶子节点的data域存放的是数据记录的地址。
InnoDB中索引即数据(.idb),也就是聚簇索引的那颗B+树的叶子节点中包含了完整的用户数据记录。
MyISAM虽然也使用树形结构,但是却将索引和数据分开存储。
如下图就是一个以col1为主键的索引文件的存储格式。
如下图就是一个以col2建立的二级索引。
MyISAM的索引方式都是非聚簇的。而InnoDB除了有非聚簇之外,还包含一个聚簇索引。
小结:
了解不同存储引擎的索引实现方式,对于正确使用和优化索引都非常有帮助。
举例1:知道了InnoDB的索引实现后,就很容易明白为什么不建议使用过长的字段作为主键。因为所有二级索引都引用主键索引,过长的主键会令二级索引变得过大。
举例2:在InnoDB中,用非单调的字段作为主键不是一个好主意。非单调的主键会造成在插入新纪录时,数据文件为了维持B+树的特性而频繁地分裂调整,十分低效,使用自增字段作为主键则是一个很好的选择。