这几天需求还没出,算是研发的空窗期,所以闲来无事,浅复习一下Mysql的索引原理,也是系统性的总结一下索引原理相关的面试知识。
当我们拿到一本新华字典,在查找某个字时,首先是根据这个字的拼音的首字母,在目录中找到对应的页码,然后直接翻到对应的页码,这样就能最快的找到这个字了。
数据库的索引就相当于这个目录。如果我们没有这个目录,想要在一整本字典中找到某个字时,就需要一页一页翻看整本字典,这样的查找效率肯定是不能接受的。
从简单结构的索引到复杂结构,进化出最适合我们的索引数据结构。
查询某个数据最快的方式,就是维护一张哈希表,它的时间复杂度是O1。
但是它的缺点也很明显:所以MySQL的Innodb引擎就不支持
二叉树的结构就比较接近实际上目录的结构,利用二分查找,每次可以排除一半的节点。
时间复杂度O(logN) 二叉树提供了二分查找的解决思路,但是包括像hashmap在内的很多数据结构,都不会真的去用纯纯的二叉树,因为二叉树在极端情况下,有可能退化成链表,时间复杂度也就回到了O(N); 另外同样二叉树作为索引时,也不支持范围查询。
那为了解决上面二叉树可能退化成链表的问题,或者在一定程度上减少二叉树的树高(二叉树的树高越高,查询的链路越长,时间也就越长),我们就需要一颗平衡二叉树。
最好实现的平衡二叉树就是红黑树,我们之前hashmap和数据结构的复习中有总结过红黑树。
红黑树已经能较快的查出某个数据了,但是如果我们数据量多的情况下,生成的一颗红黑树还是可能很高,导致查询的路径很长。
所以这个时候就有了B树,也叫做二三树,它的特点是一个节点不再只放一个数据,而是放两个数据,同时还有三个指针指向子节点,这样下来,整颗树的高度就被大大的缩减,我们每次查询时,通过比较所查条件和节点两个数据的大小关系,确定所查数据的范围,再根据相应的指针进入下一层判断。
B树优化了红黑树的树高,减少了查询时I/O的次数,但是B树的每个节点,不仅存放了索引,也将索引对应的原始数据放在了一起。
从结构上来说,不美观,也不好用;而且主要是我们通常使用上,数据是很大的,所占的存储空间远远大于索引,而磁盘每一页的容量有限,这就导致了我们每一页只能存储很少的索引和数据,这样查一个数据时,我们可能就得去找很多页。
所以我们最终就选择了B+树来存储索引,B+树就是是B树的基础上,将索引和数据分开,只有叶子节点才存储数据,而非叶子节点只放索引,不放数据,这样就使得一页磁盘上,能放更多的索引;
那为什么不把数据全部分开,还要保留叶子节点的数据呢?
还记得我们上面所有结构的通病吗--不支持范围查询
所以B+树保留了叶子节点的数据,并且用双向指针将叶子节点连了起来,这样我们查到叶子节点时,就可以直接带出前后的范围数据,而不用再去数据页查找。
看起来红黑树已经比较适合做索引结构了,但是我们上面还是有一个共性问题没有解决,那就是范围查找。当然B+树已经解决了一部分,但也只是针对叶子节点。
为什么要范围查找?一是我们实际的查询中经常会有where xx in (xx, xx) 以及order by 等条件,这种是明显需要范围查找的,然后其实等式的查找,其实也是范围查找,比如我们条件是where name = 'joke',那我们就要扫描出第一个name等于joke的记录,然后往下取数,直到name不等于joke停止,因为相近的数据我们都是放在一起 ,是一个有序的列表;然后就是我们要说的磁盘I/O。
我们的数据库存储介质是磁盘,磁盘的读写速度是很低的,而根据计算机的一个理论:局部性原理,即通常在使用某一个数据时,也会使用到他相近范围内的数据,这也是我们索引一定要追求范围查找的原因吧。
磁盘的这种读取方式也叫做预读。
所以在实际使用索引时,我们会将索引放在索引页,数据放在数据页,通过索引页找到数据页的地址,即通过目录找到页码,然后我们将那一页的数据全部读出来加载到内存,而至于究竟要使用哪些数据,反正就在让内存去操作吧。毕竟内存的性能是远远高于磁盘的,而且不是很多时候都会用到数据范围内的其他数据吗?这样就不用再去索引的磁盘进行I/O了。
前面说过,B+树非常适合做范围查询,也深度契合磁盘I/O的预读特性。在做全表扫描时,B树都只能通过中序遍历一个一个去查,而B+树直接扫一遍叶子节点就行了,找到起始节点,和结束节点,然后穿起来。
但是B树的高度是比B+矮的,所以等值查询时,如果不成功,B树能更快的退出来
无论是B树还是B+树,根或者上面几层因为被反复query,启动时,都会加载到内存。起码根节点会最先加载到内存
因为我们的查询路径都是从根节点开始,然后一直二分查找,B树找到满足条件的节点就直接去取地址对应的磁盘页数据,而B+树不同的是要一直查到叶子节点,然后去取树,所以一定程度上, B+树的查询也更加稳定。
B树中的每个二阶节点,对应的数据是一个磁盘页,因此我们取一个节点的数据时,也就将目标数据前后的数据都拿到了内存中。
B+树也是一个节点对应一个磁盘页,不过它是叶子节点,叶子节点组成了一个链表,链表前后连接的就是一页一页的数据,当然一页数据可能是多条记录,也可能是单条记录。
每一页数据页的页码实际上就是这一页数据的最小主键ID,而最小主键就构成了我们的主键目录。
但是当数据量很大时,我们的主键目录也会变的很大,二分查找性能也会变得很低,所以就产生了索引页,数据库将主键目录分到多个索引页存储,而每一个索引页的地址就是该页索引里最小主键ID。同时也有一张维护这些索引页的最小主键ID和地址的索引页。如果索引增加,就在维护索引页的上面再加一层,一层层分裂。实际上这些索引页也就构成了一颗B+树。
这种索引页+数据页组成的组成的B+树就是聚簇索引,聚簇索引是 MySQL 基于主键索引结构创建的。
上面是基于主键索引维护的一颗B+树,而当我们创建了非主键索引时,列入 name和age,则在数据库又会重新帮忙我们维护一颗新的B+树,甚至数据页也是重新维护,所以这就是不能创建过多索引的原因了。
当我们用name+age创建索引,数据页就会维护成如下这样:
在插入数据的时候,MySQL 首先会根据 name 进行排序,如果 name 一样,就根据联合索引中的 age 去排序,如果还一样,那么就会根据 主键 字段去排序。 此时每个数据页中的记录存放的实际是索引字段和主键字段,而其他字段是不存的(为什么不存放?一样的数据到处存放很浪费空间的,也没必要,所以才会有下面的索引优化)
比如我们查询sql是:
select name,age from user where name = xx and age = 11 这个时候就完美使用到了索引,并且不需要回表。
上面我们说到当查询结果只要name和age时,非主键索引的数据页就能直接返回结果,但如果我们还要查询其他字段呢?上面的数据页是只存放了name、age、id这个三个字段的,而不是像主键索引(聚簇索引) 那样,索引页和完整的数据存放在一起,所以非主键索引也叫做非聚簇索引。
对于非主键索引,我们查到数据页时,如果结果还需要其他字段,我们就需要拿着这个主键ID再去主键索引的那颗B+树再查询一次,这也就是回表
覆盖索引:通过上述我们知道,如果要查询的结果字段建立了索引,就不会触发回表操作,所以这也就诞生了覆盖索引这种优化方式-将结果字段建立组合索引,减少回表。这就是触发了覆盖索引
辅助索引:B+树叶子节点存储的地址是主键ID,就是主键索引,如果是代替主键ID的其他字段,就是辅助索引,一张表中只能有一个主键索引,但是可以有多个辅助索引。
有辅助索引是因为如果没有创建主键时,innodb就会找到第一个没有null的列代替主键。如果没有,就会用隐藏列。
1、myisam支持压缩表,即如果一个数据导入后不会再发生变化,可以使用压缩表,能极大的降低使用空间; innodb需要更多的内存,启动时会将索引根节点,频繁query的数据等缓冲到内存。
2、myisam是以文件方式存储,备份转移方便;innodb的备份恢复只能通过MySQLdump或者binlog日志
3、myisam不支持事物,innodb支持事物。
4、myisam只支持表级锁,增、删、改、查都会自动给全表上锁;innodb支持行级锁,但只有where条件带主键,才会加行级锁,非主键还是会上表级锁。
5、myisam支持全文索引,innodb不支持。
6、myisam有直接存放表的count,innodb没有
所以综合来看,myisam都是适合查询多的情况,而innodb是适合更新多的情况。
innodb共享表所有数据和索引都放在一起(data下的ibdata文件),重点是做删除操作时,会产生数据碎片;而 独享表文件放在data下某个数据库名命名的文件,空间可以回收,删除不会产生太多碎片。
SQL优化很多情况下就是索引优化,所以我们要让索引能最快的命中,首先不能失效,比如我们的or、数据库操作函数等写法都会使索引失效,不满足最左前缀原则的写法也会失效;然后就是在索引生效的情况下,能加快索引命中,比如前面说的通过覆盖索引减少回表。
在慢查询的优化中,我们通常都会借助explain来分析查询计划:
- select * from user where id>= (select id from user limit 10000,1) limit100;
(1)id:SELECT识别符。这是SELECT的查询序列号。
(2)select_type:SELECT类型:
(3)table:表名
(4)type:联接类型
(5)possible_keys:possible_keys列指出MySQL能使用哪个索引在该表中找到行。注意,该列完全独立于EXPLAIN输出所示的表的次序。这意味着在possible_keys中的某些键实际上不能按生成的表次序使用。
(6)key:key列显示MySQL实际决定使用的键(索引)。如果没有选择索引,键是NULL。要想强制MySQL使用或忽视possible_keys列中的索引,在查询中使用FORCE INDEX、USE INDEX或者IGNORE INDEX。
(7)key_len:key_len列显示MySQL决定使用的键长度。如果键是NULL,则长度为NULL。注意通过key_len值我们可以确定MySQL将实际使用一个多部关键字的几个部分。
(8)ref:ref列显示使用哪个列或常数与key一起从表中选择行。
(9)rows:rows列显示MySQL认为它执行查询时必须检查的行数。
(10)Extra:该列包含MySQL解决查询的详细信息。
最左前缀原则:复合索引从最左边开始匹配,带最左列的查询条件,则会用到索引,没有就会失效。
like查询索引有用吗?只有将%写在后面才有用
包含null值的列索引是否有效?有效,只是null在数据库层面是一个特殊的值,每个可为null的列都会额外增加一个存储空间存放他是否为null。所以为了节约空间,一般将列设置为非空,并给一个默认值最好。
为什么推荐自增主键而不是UUID?
用自己的话总结一遍的知识才记得牢固!