MySql索引原理复习-(面向面试编程)

前言

这几天需求还没出,算是研发的空窗期,所以闲来无事,浅复习一下Mysql的索引原理,也是系统性的总结一下索引原理相关的面试知识。

一、索引的本质

当我们拿到一本新华字典,在查找某个字时,首先是根据这个字的拼音的首字母,在目录中找到对应的页码,然后直接翻到对应的页码,这样就能最快的找到这个字了。

数据库的索引就相当于这个目录。如果我们没有这个目录,想要在一整本字典中找到某个字时,就需要一页一页翻看整本字典,这样的查找效率肯定是不能接受的。

MySql索引原理复习-(面向面试编程)_第1张图片

 

 

二、索引分类与进化

从简单结构的索引到复杂结构,进化出最适合我们的索引数据结构。

1. Hash索引

查询某个数据最快的方式,就是维护一张哈希表,它的时间复杂度是O1。

但是它的缺点也很明显:所以MySQL的Innodb引擎就不支持

  1. 维护成本高,每一个数据都要生成相应的hash值,并维护,而且还要解决哈希冲突;
  2. 不能进行范围查找

2. 二叉树

二叉树的结构就比较接近实际上目录的结构,利用二分查找,每次可以排除一半的节点。

MySql索引原理复习-(面向面试编程)_第2张图片

 

时间复杂度O(logN)  二叉树提供了二分查找的解决思路,但是包括像hashmap在内的很多数据结构,都不会真的去用纯纯的二叉树,因为二叉树在极端情况下,有可能退化成链表,时间复杂度也就回到了O(N); 另外同样二叉树作为索引时,也不支持范围查询

MySql索引原理复习-(面向面试编程)_第3张图片

 

3. 平衡二叉树-红黑树

那为了解决上面二叉树可能退化成链表的问题,或者在一定程度上减少二叉树的树高(二叉树的树高越高,查询的链路越长,时间也就越长),我们就需要一颗平衡二叉树。

最好实现的平衡二叉树就是红黑树,我们之前hashmap和数据结构的复习中有总结过红黑树。

MySql索引原理复习-(面向面试编程)_第4张图片

 

4. B树

红黑树已经能较快的查出某个数据了,但是如果我们数据量多的情况下,生成的一颗红黑树还是可能很高,导致查询的路径很长。

所以这个时候就有了B树,也叫做二三树,它的特点是一个节点不再只放一个数据,而是放两个数据,同时还有三个指针指向子节点,这样下来,整颗树的高度就被大大的缩减,我们每次查询时,通过比较所查条件和节点两个数据的大小关系,确定所查数据的范围,再根据相应的指针进入下一层判断。

MySql索引原理复习-(面向面试编程)_第5张图片

 

5. B+树

B树优化了红黑树的树高,减少了查询时I/O的次数,但是B树的每个节点,不仅存放了索引,也将索引对应的原始数据放在了一起。

从结构上来说,不美观,也不好用;而且主要是我们通常使用上,数据是很大的,所占的存储空间远远大于索引,而磁盘每一页的容量有限,这就导致了我们每一页只能存储很少的索引和数据,这样查一个数据时,我们可能就得去找很多页。

所以我们最终就选择了B+树来存储索引,B+树就是是B树的基础上,将索引和数据分开,只有叶子节点才存储数据,而非叶子节点只放索引,不放数据,这样就使得一页磁盘上,能放更多的索引;

那为什么不把数据全部分开,还要保留叶子节点的数据呢?

还记得我们上面所有结构的通病吗--不支持范围查询

所以B+树保留了叶子节点的数据,并且用双向指针将叶子节点连了起来,这样我们查到叶子节点时,就可以直接带出前后的范围数据,而不用再去数据页查找。

MySql索引原理复习-(面向面试编程)_第6张图片

 

看起来红黑树已经比较适合做索引结构了,但是我们上面还是有一个共性问题没有解决,那就是范围查找。当然B+树已经解决了一部分,但也只是针对叶子节点。

为什么要范围查找?一是我们实际的查询中经常会有where xx in (xx, xx) 以及order by 等条件,这种是明显需要范围查找的,然后其实等式的查找,其实也是范围查找,比如我们条件是where name = 'joke',那我们就要扫描出第一个name等于joke的记录,然后往下取数,直到name不等于joke停止,因为相近的数据我们都是放在一起 ,是一个有序的列表;然后就是我们要说的磁盘I/O。

三、磁盘I/O

我们的数据库存储介质是磁盘,磁盘的读写速度是很低的,而根据计算机的一个理论:局部性原理,即通常在使用某一个数据时,也会使用到他相近范围内的数据,这也是我们索引一定要追求范围查找的原因吧。

磁盘的这种读取方式也叫做预读

所以在实际使用索引时,我们会将索引放在索引页,数据放在数据页,通过索引页找到数据页的地址,即通过目录找到页码,然后我们将那一页的数据全部读出来加载到内存,而至于究竟要使用哪些数据,反正就在让内存去操作吧。毕竟内存的性能是远远高于磁盘的,而且不是很多时候都会用到数据范围内的其他数据吗?这样就不用再去索引的磁盘进行I/O了。

四、B树和B+树对比

  1. 前面说过,B+树非常适合做范围查询,也深度契合磁盘I/O的预读特性。在做全表扫描时,B树都只能通过中序遍历一个一个去查,而B+树直接扫一遍叶子节点就行了,找到起始节点,和结束节点,然后穿起来。

  2. 但是B树的高度是比B+矮的,所以等值查询时,如果不成功,B树能更快的退出来

  3. 无论是B树还是B+树,根或者上面几层因为被反复query,启动时,都会加载到内存。起码根节点会最先加载到内存

因为我们的查询路径都是从根节点开始,然后一直二分查找,B树找到满足条件的节点就直接去取地址对应的磁盘页数据,而B+树不同的是要一直查到叶子节点,然后去取树,所以一定程度上, B+树的查询也更加稳定

B树中的每个二阶节点,对应的数据是一个磁盘页,因此我们取一个节点的数据时,也就将目标数据前后的数据都拿到了内存中。

B+树也是一个节点对应一个磁盘页,不过它是叶子节点,叶子节点组成了一个链表,链表前后连接的就是一页一页的数据,当然一页数据可能是多条记录,也可能是单条记录。

五、索引页与数据页

1、主键索引

MySql索引原理复习-(面向面试编程)_第7张图片

 

每一页数据页的页码实际上就是这一页数据的最小主键ID,而最小主键就构成了我们的主键目录。

但是当数据量很大时,我们的主键目录也会变的很大,二分查找性能也会变得很低,所以就产生了索引页,数据库将主键目录分到多个索引页存储,而每一个索引页的地址就是该页索引里最小主键ID。同时也有一张维护这些索引页的最小主键ID和地址的索引页。如果索引增加,就在维护索引页的上面再加一层,一层层分裂。实际上这些索引页也就构成了一颗B+树。

这种索引页+数据页组成的组成的B+树就是聚簇索引,聚簇索引是 MySQL 基于主键索引结构创建的。

2、非主键索引

上面是基于主键索引维护的一颗B+树,而当我们创建了非主键索引时,列入 name和age,则在数据库又会重新帮忙我们维护一颗新的B+树,甚至数据页也是重新维护,所以这就是不能创建过多索引的原因了。

当我们用name+age创建索引,数据页就会维护成如下这样:

MySql索引原理复习-(面向面试编程)_第8张图片

 

在插入数据的时候,MySQL 首先会根据 name 进行排序,如果 name 一样,就根据联合索引中的 age 去排序,如果还一样,那么就会根据 主键 字段去排序。 此时每个数据页中的记录存放的实际是索引字段和主键字段,而其他字段是不存的(为什么不存放?一样的数据到处存放很浪费空间的,也没必要,所以才会有下面的索引优化)

比如我们查询sql是:

select name,age from user where name = xx and age = 11 这个时候就完美使用到了索引,并且不需要回表

3. 回表

上面我们说到当查询结果只要name和age时,非主键索引的数据页就能直接返回结果,但如果我们还要查询其他字段呢?上面的数据页是只存放了name、age、id这个三个字段的,而不是像主键索引(聚簇索引) 那样,索引页和完整的数据存放在一起,所以非主键索引也叫做非聚簇索引

对于非主键索引,我们查到数据页时,如果结果还需要其他字段,我们就需要拿着这个主键ID再去主键索引的那颗B+树再查询一次,这也就是回表

覆盖索引:通过上述我们知道,如果要查询的结果字段建立了索引,就不会触发回表操作,所以这也就诞生了覆盖索引这种优化方式-将结果字段建立组合索引,减少回表。这就是触发了覆盖索引

辅助索引:B+树叶子节点存储的地址是主键ID,就是主键索引,如果是代替主键ID的其他字段,就是辅助索引,一张表中只能有一个主键索引,但是可以有多个辅助索引。

有辅助索引是因为如果没有创建主键时,innodb就会找到第一个没有null的列代替主键。如果没有,就会用隐藏列。

六、对比MySQL的myisam和innodb引擎

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共享表与独享表

innodb共享表所有数据和索引都放在一起(data下的ibdata文件),重点是做删除操作时,会产生数据碎片;而 独享表文件放在data下某个数据库名命名的文件,空间可以回收,删除不会产生太多碎片。

七、sql优化

SQL优化很多情况下就是索引优化,所以我们要让索引能最快的命中,首先不能失效,比如我们的or、数据库操作函数等写法都会使索引失效,不满足最左前缀原则的写法也会失效;然后就是在索引生效的情况下,能加快索引命中,比如前面说的通过覆盖索引减少回表。

在慢查询的优化中,我们通常都会借助explain来分析查询计划:

  • explain type 如果是all,则是全表扫描,解决方式就是创建适合的索引或者让索引生效;
  • explain type如果是index,表示扫描了整个索引树,这种情况下,可能是索引生效了,但是过滤性不好,比如本来是5000数据,过滤后还有4000,索引过滤性不好,就需要多方来解决,比如索引字段选型,表设计等。
  • 使用覆盖索引,重点是id主键索引多使用,比如在分页查询中,利用子查询+覆盖索引:
- select * from user where id>= (select id from user limit 10000,1) limit100;
  • explain第一行出现的表是驱动表,一般选择小表驱动,这也是join时的原则。

2、explain查询结果简介

(1)id:SELECT识别符。这是SELECT的查询序列号。

(2)select_type:SELECT类型:

  1. SIMPLE: 简单SELECT(不使用UNION或子查询)
  2. PRIMARY: 最外面的SELECT
  3. UNION:UNION中的第二个或后面的SELECT语句
  4. DEPENDENT UNION:UNION中的第二个或后面的SELECT语句,取决于外面的查询
  5. UNION RESULT:UNION的结果
  6. SUBQUERY:子查询中的第一个SELECT
  7. DEPENDENT SUBQUERY:子查询中的第一个SELECT,取决于外面的查询
  8. DERIVED:导出表的SELECT(FROM子句的子查询)

(3)table:表名

(4)type:联接类型

  1. system:表仅有一行(=系统表)。这是const联接类型的一个特例。
  2. const:表最多有一个匹配行,它将在查询开始时被读取。因为仅有一行,在这行的列值可被优化器剩余部分认为是常数。const用于用常数值比较PRIMARY KEY或UNIQUE索引的所有部分时。
  3. eq_ref:对于每个来自于前面的表的行组合,从该表中读取一行。这可能是最好的联接类型,除了const类型。它用在一个索引的所有部分被联接使用并且索引是UNIQUE或PRIMARY KEY。eq_ref可以用于使用= 操作符比较的带索引的列。比较值可以为常量或一个使用在该表前面所读取的表的列的表达式。
  4. ref:对于每个来自于前面的表的行组合,所有有匹配索引值的行将从这张表中读取。如果联接只使用键的最左边的前缀,或如果键不是UNIQUE或PRIMARY KEY(换句话说,如果联接不能基于关键字选择单个行的话),则使用ref。如果使用的键仅仅匹配少量行,该联接类型是不错的。ref可以用于使用=或<=>操作符的带索引的列。
  5. ref_or_null:该联接类型如同ref,但是添加了MySQL可以专门搜索包含NULL值的行。在解决子查询中经常使用该联接类型的优化。
  6. index_merge:该联接类型表示使用了索引合并优化方法。在这种情况下,key列包含了使用的索引的清单,key_len包含了使用的索引的最长的关键元素。
  7. unique_subquery:该类型替换了下面形式的IN子查询的ref:value IN (SELECT primary_key FROMsingle_table WHERE some_expr);unique_subquery是一个索引查找函数,可以完全替换子查询,效率更高。
  8. index_subquery:该联接类型类似于unique_subquery。可以替换IN子查询,但只适合下列形式的子查询中的非唯一索引:value IN (SELECT key_column FROM single_table WHERE some_expr)
  9. range:只检索给定范围的行,使用一个索引来选择行。key列显示使用了哪个索引。key_len包含所使用索引的最长关键元素。在该类型中ref列为NULL。当使用=、<>、>、>=、<、<=、IS NULL、<=>、BETWEEN或者IN操作符,用常量比较关键字列时,可以使用range
  10. index:该联接类型与ALL相同,除了只有索引树被扫描。这通常比ALL快,因为索引文件通常比数据文件小。
  11. all:对于每个来自于先前的表的行组合,进行完整的表扫描。如果表是第一个没标记const的表,这通常不好,并且通常在它情况下很差。通常可以增加更多的索引而不要使用ALL,使得行能基于前面的表中的常数值或列值被检索出。

(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解决查询的详细信息。

  1. Distinct:MySQL发现第1个匹配行后,停止为当前的行组合搜索更多的行。
  2. Not exists:MySQL能够对查询进行LEFT JOIN优化,发现1个匹配LEFT JOIN标准的行后,不再为前面的的行组合在该表内检查更多的行。
  3. range checked for each record (index map: #):MySQL没有发现好的可以使用的索引,但发现如果来自前面的表的列值已知,可能部分索引可以使用。对前面的表的每个行组合,MySQL检查是否可以使用range或index_merge访问方法来索取行。
  4. Using filesort:MySQL需要额外的一次传递,以找出如何按排序顺序检索行。通过根据联接类型浏览所有行并为所有匹配WHERE子句的行保存排序关键字和行的指针来完成排序。然后关键字被排序,并按排序顺序检索行。
  5. Using index:从只使用索引树中的信息而不需要进一步搜索读取实际的行来检索表中的列信息。当查询只使用作为单一索引一部分的列时,可以使用该策略。
  6. Using temporary:为了解决查询,MySQL需要创建一个临时表来容纳结果。典型情况如查询包含可以按不同情况列出列的GROUP BY和ORDER BY子句时。
  7. Using where:WHERE子句用于限制哪一个行匹配下一个表或发送到客户。除非你专门从表中索取或检查所有行,如果Extra值不为Using where并且表联接类型为ALL或index,查询可能会有一些错误。
  8. Using sort_union(...), Using union(...), Using intersect(...):这些函数说明如何为index_merge联接类型合并索引扫描。
  9. Using index for group-by:类似于访问表的Using index方式,Using index for group-by表示MySQL发现了一个索引,可以用来查询GROUP BY或DISTINCT查询的所有列,而不要额外搜索硬盘访问实际的表。并且,按最有效的方式使用索引,以便对于每个组,只读取少量索引条目。

八、其他问题

最左前缀原则:复合索引从最左边开始匹配,带最左列的查询条件,则会用到索引,没有就会失效。

MySql索引原理复习-(面向面试编程)_第9张图片

 

like查询索引有用吗?只有将%写在后面才有用

包含null值的列索引是否有效?有效,只是null在数据库层面是一个特殊的值,每个可为null的列都会额外增加一个存储空间存放他是否为null。所以为了节约空间,一般将列设置为非空,并给一个默认值最好。

为什么推荐自增主键而不是UUID

  1. 节约空间
  2. 插入效率高(由于B+树遵循左小右大,所以自增插入数据总是在最右侧插入。而uuid则不一定,如果页16k已经写满了,那只能把页中的数据向后移,在空位中插入。频繁的移动分页会造成碎片,后续需要使用OPTIMIZE TABLE来进行碎片整理)

结尾:

用自己的话总结一遍的知识才记得牢固!

你可能感兴趣的:(java,数据结构,开发语言,数据库,mysql)