理解b+tree的索引机制
我们最常接触到的 InnoDB 存储引擎中的 B+ 树索引,那为什么使用 B+ 树索引?
简单而言:B+树更加“矮胖”,叶子节点存放了整张表的所有行数据并用一个双向链表来进行连接,非叶子节点并不存储行数据,是为了能存储更多索引键,从而降低B+树的高度,进而减少IO次数。下面详细分析一下。
理解为什么要减少磁盘IO次数
一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。这样的话,索引查找过程中就要产生磁盘I/O消耗,相对于内存存取,I/O存取的消耗要高几个数量级,所以评价一个数据结构作为索引的优劣最重要的指标就是在查找过程中磁盘I/O操作次数的渐进复杂度。换句话说,索引的结构组织要尽量减少查找过程中磁盘I/O的存取次数。
数据库系统将B+ 树索引的一个节点的大小设为等于一个页,这样每个节点只需要一次I/O就可以完全载入。换言之,如果在一个节点里面尽量多的存储更多的数据,那一次磁盘读取操作就会读取更多数据,那我们查找数据的时间也会大幅度降低。
为了达到这个目的,在实际实现时还需要使用如下技巧:
每次新建节点时,直接申请一个磁盘页的空间,这样就保证一个节点物理上也存储在一个页里,加之计算机存储分配都是按页对齐的,就实现了一个node只需一次I/O。
系统从磁盘读取数据到内存时是以磁盘块位基本单位的,位于同一磁盘块中的数据会被一次性读取出来,而不是按需读取。
InnoDB存储引擎使用页作为数据读取单位,页是其磁盘管理的最小单位,默认page大小是16k。
系统的一个磁盘块的存储空间往往没有那么大,因此InnoDB每次申请磁盘空间时都会是若干地址连续磁盘块来达到页的大小16KB。这里数据库系统巧妙利用了磁盘预读,其实就是利用了局部性原理,具体过程是:对于每个文件的第一个读请求,系统读入所请求的磁盘块并读入紧随其后的少数几个磁盘块(通常是三个),这时的预读称为同步预读。
我们知道对大部分操作系统来说,磁盘块是4kb,那么算上预读的3个:
4kb(磁盘块的大小)+12kb(预读三个磁盘块)=16kb
另外,B+ 树的阶数是等于键值的数量的,如果我们的 B+ 树一个节点可以存储 1000 个键值,那么 3 层 B+ 树可以存储 1000×1000×1000=10 亿个数据。一般根节点是常驻内存的,所以一般我们查找 10 亿数据,只需要 2 次磁盘 IO。
理解了这个后再来看下面的。
二叉树为什么不可行
无论是二叉树还是红黑树,都会因为树的深度过深而造成io次数变多,影响数据读取的效率。
平衡二叉树可以解决二叉树“线性链表”的问题,可是还有另外一个问题:每个节点只存储一个键值和数据的。那说明什么?说明每个磁盘块仅仅存储一个键值和数据!那如果我们要存储海量的数据呢?
可以想象到二叉树的节点将会非常多,高度也会极其高,我们查找数据时也会进行很多次磁盘 IO,我们查找数据的效率将会极低
多路平衡查找树(Balance Tree)
从下图可以看出,B 树相对于平衡二叉树,每个节点存储了更多的键值(key)和数据(data),并且每个节点拥有更多的子节点,子节点的个数一般称为阶,上述图中的 B 树为 3 阶 B 树,高度也会很低。
基于这个特性,B 树查找数据读取磁盘的次数将会很少,数据的查找效率也会比平衡二叉树高很多。
B 树的特点:
1. 所有键值分布在整颗树中
2. 搜索有可能在非叶子结点结束,在关键字全集内做一次查找,性能逼近二分查找
3. 每个节点最多拥有m个子树
4. 根节点至少有2个子树
5. 分支节点至少拥有m/2颗子树(除根节点和叶子节点外都是分支节点)
6. 所有叶子节点都在同一层、每个节点最多可以有m-1个key,并且以升序排列
缺点:
每个节点都有key,同时也包含data,而每个页存储空间是有限的,如果data比较大的话会导致每个节点存储的key数量变少
当存储的数据量很大的时候会导致深度较大,增大查询时磁盘io次数,进而影响查询性能
B+ 树
B+ 树是对 B 树的进一步优化。让我们先来看下 B+ 树的结构图:
B+ 树非叶子节点上是不存储数据的,仅存储键值,而 B 树节点中不仅存储键值,也会存储数据;
因为 B+ 树索引的所有数据均存储在叶子节点,而且数据是按照顺序排列的;那么 B+ 树使得范围查找,排序查找,分组查找以及去重查找变得异常简单。而 B 树因为数据分散在各个节点,要实现这一点是很不容易的。
有心的读者可能还发现上图 B+ 树中各个页之间是通过双向链表连接的,叶子节点中的数据是通过单向链表连接的。
聚集和非聚集索引
简单概括:
聚集索引就是以主键创建的索引
非聚集索引就是以非主键创建的索引
区别:
聚集索引在叶子节点存储的是表中的数据
非聚集索引在叶子节点存储的是主键和索引列
使用非聚集索引查询出数据时,拿到叶子上的主键再去查到想要查找的数据。(拿到主键再查找这个过程叫做回表)
其他
非聚集索引也叫做二级索引
非聚集索引在建立的时候也未必是单列的,可以多个列来创建索引。
此时就涉及到了哪个列会走索引,哪个列不走索引的问题了(最左匹配原则)
创建多个单列(非聚集)索引的时候,会生成多个索引树(所以过多创建索引会占用磁盘空间)
在创建多列索引中也涉及到了一种特殊的索引-->覆盖索引
我们前面知道了,如果不是聚集索引,叶子节点存储的是主键+列值
最终还是要“回表”,也就是要通过主键再查找一次。这样就会比较慢
覆盖索引就是把要查询出的列和索引是对应的,不做回表操作!
比如说:现在我创建了索引(username,age),在查询数据的时候:select username , age from user where username = 'Java3y' and age = 20。
很明显地知道,我们上边的查询是走索引的,并且,要查询出的列在叶子节点都存在!所以,就不用回表了~
所以,能使用覆盖索引就尽量使用吧~
InnoDB 要求表必须有主键(MyISAM 可以没有),如果没有显式指定,则 MySQL系统会自动选择一个可以唯一标识数据记录的列作为主键,如果不存在这种列,则MySQL 自动为 InnoDB 表生成一个隐含字段作为主键,类型为长整形。
同时,请尽量在 InnoDB 上采用自增字段做表的主键。因为 InnoDB 数据文件本身是一棵B+Tree,非单调的主键会造成在插入新记录时数据文件为了维持 B+Tree 的特性而频繁的分裂调整,十分低效,而使用自增字段作为主键则是一个很好的选择。如果表使用自增主键,那么每次插入新的记录,记录就会顺序添加到当前索引节点的后续位置,当一页写满,就会自动开辟一个新的页。
联合索引列选择原则
经常用的列优先(最左匹配原则)、选择性高的列优先(离散度高原则)、宽度小的列优先(最少空间原则)。
带索引的模糊查询优化
在上面已经提到,使用LIKE进行模糊查询的时候,'%aaa%'不会使用索引(aaa%才可以),也就是索引会失效。如果是这种情况,只能使用全文索引来进行优化。
总结
关于索引优化原则,可以做如下总结。
全值匹配心上人(这是基本原则),最左前缀要遵行(联合索引一般都围绕最左前缀优化);
带头大哥活才行(联合索引从最左边字段开始使用),中间兄弟规矩行(不能跳过中间的字段,跳过后索引无效);
索引列上少计算(索引列上尽量不要进行计算),范围之后全完蛋(where后面使用范围查询的之后的索引无效);
like百分最右写(%号写最右边,写左边会导致索引失效),覆盖索引别写星(尽量避免select*这样的语句,能写索引列最好);
空值不等还有or,索引失效最无情(is null,is not null,!=,<>,or会导致索引无效);
关于索引优化原则,不同的sql版本会有不同,并且需要了解索引机制并结合explain的各项参数分析,不断体会。