索引,是MySQL快速查询的秘籍。
如果没有索引,是怎么查询记录的呢?
首先,假如表中记录比较少,所有记录都可以存放在一个页中。
那么可以根据搜索条件,分为两种情况:
如果表中记录很多,就需要用比较多的页存储这些记录
那么,就需要两个步骤:
由于没有索引,无论是根据主键列还是其他列,都不能快速定位记录所在页,需要从第一页开始往下找,在每一页中使用二分法查找指定记录。很显然,这种方式非常消耗时间。
就以这个t_index_demo
这个表为例
CREATE TABLE t_index_demo(
c1 INT,
c2 INT,
c3 CHAR(1),
PRIMAY KEY(c1)
)ROW_FORMAT = COMPACT;
该表指定了使用COMPACT
为行格式,并且列c1
为主键。
行格式示意图简化如下:
以及记录存放在页中的示意图如下:
先往表中插入数据
INSERT INTO t_index_demo VALUES(1, 4, 'u'), (3, 9, 'd'), (5, 3, 'y');
这些记录就会按照主键值的大小形成一个单向链表
这时候,新插入一条数据。
INSERT INTO index_demo VALUES(4, 4, 'a');
这里假设每个页最多只能存放3条记录,再插入一条记录,就需要重新分配一个页了。
理论应该像上图所示,但是如果是这样的话,页10的最大主键值为5,页25的一条记录的主键值为4,5>4。
不符合下一条数据页中用户记录的主键必须大于上一页中用户记录的主键值
,因此需要将主键值为4的记录进行一次记录移动。
怎么移动呢?
这个过程,可以称为页分裂。
在每次进行表记录的增删改操作的时候,都必须通过记录移动操作来保证这个规则一直成立。
这样插入数据的操作就完成了。
那么,怎么快速定位查找的记录在哪些数据页呢?
其实和查找用户记录类似,查找用户记录的时候,MySQL为了根据根据主键值快速定位一条记录而设立了页目录。所以也可以为快速定位记录所在的数据⻚⽽建⽴⼀个别的⽬录,但是建这个⽬录必须完成下边这些事⼉:
这个目录项,由两部分组成
key
来表示。page_no
表示。如上图所示,我们将几个目录项在物理内存中连续存储,比如放在一个数组中,就能实现根据主键值快速查找某条记录的功能了。
比如需要查找主键值为20的数据。
这种目录,就是我们常用的索引!!!!
上述的是一种简单的索引方案,并不是真正的InnoDB索引方案。
在InnoDB中,由于这些目录项和用户记录长得很像,所以在InnoDB中复用了之前存储用户记录的页来存储目录项。
同时,InnoDB为了区分是目录项还是普通的用户记录,将目录项的记录头信息的record_type
属性设置为1。
在这里,就可以知道record_type
的所有含义了
0
:普通用户记录1
:⽬录项记录2
:最⼩记录3
:最⼤记录这里总结一下⽬录项记录和普通的⽤户记录的不同点:
record_type
值是1,⽽普通⽤户记录的record_type
值是0。min_rec_mask
值为1, 其他别的记录的min_rec_mask
值都是0。并且,需要注意的是,由于目录项使用的是和存储用户记录的页是一样的数据结构。
因此目录项实际是一个双向链表!!!!!而不是上面提到的目录项是连续空间!!!!
因此目录项实际是一个双向链表!!!!!而不是上面提到的目录项是连续空间!!!!
因此目录项实际是一个双向链表!!!!!而不是上面提到的目录项是连续空间!!!!
还有就是,InnoDB中一个页只有16kb大小,当表的数据太多,一个数据页已经不足以存放所有目录项的话,就需要多整一个存储目录项的页了。(这里假设了一个页最多存放4条记录)
因此,在InnoDB中,如果需要查询一条用户记录的话,就需要三个步骤
但是问题又来了,当一个表的数据非常多的时候,这也会产生非常多的目录项,那么怎么根据主键值快速定位一个目录项呢?
很简单,为这些存储⽬录项记录的⻚再⽣成⼀个更⾼级的⽬录,就像是⼀个多级⽬录⼀样,⼤⽬录⾥嵌套⼩⽬录,⼩⽬录⾥才是实际的数据。
而这种结构,就是大名鼎鼎的B+树了
不论是存放⽤户记录的数据⻚,还是存放⽬录项记录的数据⻚,我们都把它们存放到B+树这个数据结构中了
我们也称这些数据页为B+树的节点,并且我们真正的用户记录其实存放在B+树最底层的节点上。
这些节点也被称为叶⼦节点或叶节点,其余⽤来存放⽬录项的节点称为⾮叶⼦节点或者内节点,其中 B+树最上边的那个节点也称为根节点。
MySQL规定最下边的那层,也就是存放我们⽤户记录的那层为第0层,之 后依次往上加。
⼀般情况下,我们⽤到的B+树都不会超过4层,那我们通过主键值去查找某条记录最多只需要做4个⻚⾯内的查找 (查找3个⽬录项⻚和⼀个⽤户记录⻚)。
之前有提到的Page Header
部分的一个PAGE_LEVEL
属性,它记录的就是这个数据页作为节点在B+树所在的层级。
这里探讨的索引主要有
B+树本身是一个目录,或者说本身是一个索引。具有以下两个特点:
具有这两种特性的B+树称为聚簇索引,所有完整的⽤户记录都存放在这个聚簇索引的叶⼦节点处。
这种聚簇索引并不需要我们在MySQL语句中显式的使⽤INDEX 语句去创建,InnoDB存储引擎会⾃动的为我们创建聚簇索引。
另外有趣的⼀点是,在InnoDB存储引擎中,聚簇索引就是数据的存储 ⽅式(所有的⽤户记录都存储在了叶⼦节点),也就是所谓的索引即数据,数据即索引。
二级索引可以说就是另外一个B+树,这个B+树不再以主键的值进行排序。而是索引中指定的某个列作为大小作为数据页、页中记录的排序规则等等。
这个B+树与上边介绍的聚簇索引有⼏处不同
比方说需要查找c2=4的列,这样的记录会有很多条,但是我们只需要在该 树的叶子节点处定位到第一条满足搜索条件 c2斗的那条记录,然后沿着自记录组成的单向链表一直向后扫描即可。
另外,各个叶子节点 组成了双向链表,搜索完了本页面的记录后可以很顺利地跳到下一个页面中的第一条记录,然 后继续沿着记录组成的单向链表向后扫描,查找过程如下。
为什么还需要一次回表操作呢?直接把完整的用户记录放到时子节点不就好了么?
确实可以这样,但是太占内存,相当于每建一个B+树都需要将所有用户记录复制一遍,这样太浪费内存了。
因为这种按照⾮主键列建⽴的B+树需要⼀次回表操作才可以定位到完整的⽤户记录,所以这种B+树也被称为⼆级索引(英⽂名secondary index),或者辅助索引。
由于我们使⽤的是c2列的⼤⼩作为B+树的排序规则,所以我们也称这个B+树为为c2列建⽴的索引。
我们把上面聚簇索引或者二级索引的叶子节点中的记录称为用户记录。
为了区分,也把聚簇索引叶子节点中的记录称为完整的用户记录。
把二级索引叶子节点中的记录称为不完整的用户记录。
我们也可以同时以多个列的⼤⼩作为排序规则,也就是同时为多个列建⽴索引,这种就叫联合索引。
⽐⽅说我们想让B+树按照c2和c3列的⼤⼩进⾏排序,这个包含两层含义:
但是注意的是以c2和c3列的⼤⼩为排序规则建⽴的B+树称为联合索引,本质上也是⼀个⼆级索引。
它的意思与分别为c2和c3列分别建⽴索引的表述是不同的。
B+树索引不是先把存储⽤户记录的叶⼦节点都画出来,然后接着画存储⽬录项记录的内节点,实际上B+树的形成过程 是这样的:
需要注意的是,⼀个B+树索引的根节点⾃诞⽣之⽇起,便不会再移动
这样只要我们对某个表建⽴⼀个索引,那么它的根节点的⻚号便会被记录到某个地⽅,然后凡是InnoDB存储引擎需要⽤到这个索引的时候,都会从那个固定的地⽅取出根节点的⻚号,从⽽来访问这个索引。
就以为t_index_demo
这个表的数据来说
c1 | c2 | c3 |
---|---|---|
1 | 1 | ‘u’ |
3 | 1 | ‘d’ |
5 | 1 | ‘y’ |
7 | 1 | ‘a’ |
如果⼆级索引中⽬录项记录的内容只是索引列 + ⻚号的搭配的话,那么为c2列建⽴索引后的B+树应该⻓这样:
如果此时插入一个c1为9,c2为1,c3为’c’的记录,那么由于原来每个页的记录的c2列均为1,那么新插入的数据就会不知道应该放到页4还是页5。
为了让新插⼊记录能找到⾃⼰在那个⻚⾥,我们需要保证在B+树的同⼀层内节点的⽬录项记录除⻚号这个字段以外是唯⼀的。所以对于⼆级索引的内节点的⽬录 项记录的内容实际上是由三个部分构成的:
这样我们再插⼊记录(9, 1, ‘c’)时,由于⻚3中存储的⽬录项记录是由c2列 + 主键 + ⻚号的值构成的,可以先把新记录的c2列的值和⻚3中各⽬录项记录的c2列 的值作⽐较,如果c2列的值相同的话,可以接着⽐较主键值,因为B+树同⼀层中不同⽬录项记录的c2列 + 主键的值肯定是不⼀样的,所以最后肯定能定位唯⼀的 ⼀条⽬录项记录,在本例中最后确定新记录应该被插⼊到⻚5中。
对于二级索引的记录来说,是先按照二级索引列的值进行排序,如果该值相同,再按照主键值进行排序的。
所以,为c2列建立索引相当于为(c2, c1)列建立了一个联合索引。
而对于唯一二级索引(某列声明为UNIQUE)来说,也可能出现相同值的情况(为NULL),唯一二级索引的内节点的目录项也需要包括记录的主键值。
一颗B+树只需要很少的层级就可以轻松存储数亿条记录。
虽然说一个大的目录存放一个子目录看起来也是可以的,但是这样的话层级关系就会很多。因此InnoDB规定,一个数据页至少存放两条记录。
在InnoDB中,索引即数据。也就是聚簇索引的那棵B+树的叶⼦节点中已经把所有完整的⽤户记录都包含了。
但是在MyISAM就不一定了。MyISAM的索引⽅案虽然也 使⽤树形结构,但是却将索引和数据分开存储。
由于插入数据的时候没有按照主键大小排序,因此不能使用二分法查找。
而InnoDB是只需要根据主键值对聚簇索引进⾏⼀次查找就能找到对应的记录,⽽在MyISAM中却需要进⾏⼀次 回表操作,意味着MyISAM中建⽴的索引相当于全部都是⼆级索引!
MyISAM会直接在索引的叶子节点处存储该条记录在数据文件中的地址偏移量。
而InnoDB是获取主键之后再去聚簇索引中找记录。
所以,MyISAM的回表速度会比InnoDB快。
参考:《MySQL是怎样运行的:从根儿上理解 MySQL》