MySQL哈希索引
我们在进行搜索的时候,两个非常重要的数据结构:哈希表和平衡树。
哈希表在哈希冲突可控的范围内我们经常写的链式哈希表,它的增删查的时间复杂度是O(1),要比平衡树搜索的时间复杂度还要好,平衡树的增删查的时间复杂度是O(logn),但是平衡树对数据进行了排序。
也就是B+树索引就是把磁盘上存储的索引加载到内存上构建的数据结构,索引其实就是数据结构,MyISAM和InnoDB的索引就是在内存上构建B+树来进行搜索。哈希索引是基于哈希表来实现的,可以明显看出O(1)比O(logn)好,那为什么InnoDB和MyISAM不用哈希索引?
创建哈希索引
create index 索引名 on 表名(属性名) using hash
用show indexes from 表名
查看索引
InnoDB用的是B+树索引。
哈希索引其实就是链式哈希表,假如说用memery存储引擎(它是基于内存的存储引擎,就是数据和索引都在内存上放着),create index nameidx on student(name);
默认选择的就是哈希索引,我们给这个表用name字段建立索引的话,实际上就是构建了一个链式哈希表,根据选定的哈希函数把每一行记录的name做一个参数来求哈希值,哈希值就相当于桶的序号
把相应的求出来的哈希值模上桶的个数,就知道放在哪个桶里面。
当然了不同的值模上桶个数,可能得到同样的结果,这就是哈希冲突,解决哈希冲突怎么解决呢?就在桶里面用链表把它串起来。
假如说第一行记录name是zhangsan,它就会把zhangsan这个字符串作为一个输入参数选取一个哈希函数(比如求md5值或者CRC3值),哈希函数尽量取求出来的数比较离散的,哈希冲突会少一点,求出来以后比如在0号桶,同样方法再求linfeng比如在2号桶。
哈希表的增删查效率确实快是O(1),但是哈希表中的元素没有任何顺序可言,如果用哈希表来构建索引的话,只能进行等值比较,比如:select * from student where name=zhangsan。但是像select * from student where name like 'zhang%'是用不上索引的,因为像zhangsan、zhangli和zhangwen虽然都是以zhang开头的,但是不能确保他们都在哈希表的一个桶中,这时候需要做整表搜索是O(n)。
在哈希表中,不同元素,哪怕是15和16,通过求哈希值,模上桶的个数,最后存储的位置可能会相隔很远。如果用链式哈希表构建索引,一个桶里面的节点代表1次磁盘I/O,由于桶内元素也是没有顺序的,我们进行查找的时候都会遍历完所有的桶内节点,就会导致更多的磁盘I/O。
所以对于哈希表来说不适合做范围搜索、前缀搜索、order by排序;只适用于数据都在内存上,基于内存的索引数据来构建哈希索引,来加速数据的增删查,而且查询只是适合等值查询。哈希索引根本没有办法来减少磁盘I/O的次数。
总结:
- 由于我们绝大部分的数据都是存放在磁盘的,哈希索引没办法减少磁盘I/O的次数,从磁盘上加载数据到内存的次数太多
- 由于不同的索引值经过哈希函数计算以及取模后,最后存储的位置非常不确定,没有任何的顺序,故不适用于多数的应用场景,比如范围、模糊、排序等等
- 此外一旦哈希表扩容,就会导致所有的索引值重新计算存储位置,效率很低
实际上哈希索引使用的机会很少,因为我们做存储数据肯定是要落盘的,落盘肯定涉及到磁盘,到时候处理它肯定要花费磁盘I/O把它加载到内存上,而且我们不可能把所有数据都加载到内存上,我们应该先加载它的根节点,从根节点再进行搜索精确定位它的范围,然后再来判断加载它的下一个节点。B+树千万级的数据不超过三层,要找到一个数据不会超过三次的磁盘I/O,这是最合适的。哈希索引一般在内存上处理数据,数据不落盘的。
InnoDB自适应哈希索引
假如name是有索引的,我们不断使用下面的方式查询,那就得先访问name的二级索引树,从二级索引树上取出主键uid,然后回表,用这个uid去主键索引树上取得对应的数据
select * from student where name = "zhangsan";
select * from student where name = "gaoyang";
select * from student where name = "linfeng";
InnoDB存储引擎会做如下优化:如果检测到同样的二级索引不断被使用,那么它会根据这个二级索引,在内存中根据二级索引树(B+树)上的二级索引值,在内存上构建一个哈希索引,来加速搜索(只适用于等值比较)。
原来是图中蓝色的箭头先搜二级索引树,再通过二级索引树找到主键,最后通过主键找到主键索引树上的数据,去访问相应的数据页,需要回表。
现在是我们用这个二级索引直接构建一个哈希索引,黄色箭头就是直接等值比较搜索哈希表,直接拿到数据地址的过程。使用哈希索引O(1)的时间复杂度就访问到哈希索引name,然后取出data即可(对于InnoDB来说应该是直接取得数据,而不是拿到数据地址后再访问)。省了二级索引树和主键索引树的搜索过程,相当于是岁B+树上频繁使用二级索引并且进行回表操作的优化。
实际上哈希索引生成以及维护也是相当复杂耗费性能的,我们查看相关参数指标,如果哈希索引能够提高我们查询效率,那我们需要它;如果它已经成为我们性能的损耗,我们可以适时的把哈希索引关闭掉。在MySQL官方文档中也提到了,自适应哈希索引并不能绝对的在任何场景下,都保证提升二级索引的查找效率。
在MySQL5.7版本中,自适应哈希索引是分区的。要是在一个数组一个链表中进行并行操作,那只能是加锁控制了,因为会出现静态条件、线程安全问题;要是在不同的桶中去操作,不用把整个哈希表都锁住,加分段锁就可以。
默认自适应哈希索引是开启的:
在MySQL5.7以前,操作哈希表是只有一把锁的,锁的粒度太大,效率很低。在MySQL5.7以后,每个分区都会有自己的锁,锁的粒度减小,要是各个线程在同一个分区(一个分区管理一个或多个桶)进行并发操作,就需要加锁。要是在不同的分区操作,就不用加锁。
哈希索引总是基于B+树数据结构上存储的二级索引的内容来构建的。
在并发环境中,如果同一个分区等待的线程过多,这个时候需要考虑关闭自适应哈希索引
我们通过show engine innodb status\G
命令查看两个关键信息:
- RW-latch等待线程的数量,自适应哈希索引默认分配了8个分区,若某个分区等待的线程数量过多,则需要考虑关闭自适应哈希索引
- 使用自适应哈希索引搜索的频率低于使用二级索引树搜索的频率,也需要考虑关闭自适应哈希索引