Mysql - MySQL索引(复合索引、覆盖索引、索引下推、前缀索引)

目录

存储引擎与底层实现的数据结构

InnoDB主键索引和二级索引

复合索引(一棵B+树过滤过个条件)

覆盖索引(不用回标)

索引下推(减少回表次数)

前缀索引(长字符串索引优化)


存储引擎与底层实现的数据结构

     数据结构 - 索引怎么选择合适的数据结构?中分析过能作为索引的数据结构主要有散列表(Hash表)、红黑树、跳表、B+树(B树)以及有序数组,并且分析了它们适合场景。Mysql的索引与存储引擎相关,但是Mysql内常用的存储引擎有InnoDB、MyiSAM、Memory,在Mysql5.5版本后,InnoDB已经作为默认的存储引擎,并且很多互联网公司基本都要求只能使用InnoDB存储引擎。Memory作为临时表的默认存储引擎,所以研究的重点基本就是InnodDB和Memory引擎,也就是基本关注B+树和散列表的数据结构索引。他们底层支持的数据结构如下图:

Mysql - MySQL索引(复合索引、覆盖索引、索引下推、前缀索引)_第1张图片

    InnoDB存储引擎本身只支持B+树,之前分析过B+树比较适合磁盘存储。B+树是多路平衡搜索树,最佳实践N值为1200左右,树高为4时就可存储 1200的3次方,此时已经存储了17亿数据了。由于第一层数据总是在内存中,那么相当于17亿数据,最多查询磁盘3次,如果第二层刚好也在内存中,那么对多查询2次磁盘。也就是说InnoDB的最底层数据结构是B+树,B+树可能存储在内存中可能存储在磁盘中,存储的单元是数据页(操作系统数据缓存页),即数据缓存页是内存和磁盘的链接点。              磁盘 -> 数据页 -> 内存 

    而查询数据所在的行到达在内存中还是在磁盘中,由服务器所在的 InnoDB缓存页大小决定,Buffer Pool由 my.cnf配置中的参数控制:

     innodb_buffer_pool_size:引擎的缓存池大小,一般为物理内存的 60% - 80%;

     innodb_buffer_pool_instances:IBP(InnoDB Buffer Pool)的个数,Linux系统,如果 innodb_buffer_pool_size 大小超过 1GB,innodb_buffer_pool_instances 值就默认为 8;否则,默认为 1;

    缓存的大小有限,那么只能将数据命中率最高的放入缓存中,Mysql使用的LRU缓存淘汰算法。LRU可以理解成一个链表,链表的节点就是数据缓存页,刚被访问过的放到链表一头,最老被访问过的放到一头,当有新的数据缓存页被访问加入时,从最老的一头淘汰。但是链表本身删除节点的时间复杂度是O(1),但是访问时间复杂度是O(N),性能很低。怎么才能提高访问性能呢? 一般会使用散列表进行访问,即整个LRU由散列表+双向链表组成,类似于Java中的LinkedHashMap的数据结构。

    由于磁盘局部性原理,访问数据页时有预读数据页的功能,即我们从磁盘中获取到了多余的数据页,加入LRU的话就是浪费存储空间。还有我们可能会(处理报表等sql)对一个历史数据表进行整表分页全表查询一遍,那么也会对LRU照成冲击,可能需要很长的时间才能让缓存命中率恢复。针对预读数据页和冷数据扫描的情况,Mysql对LRU进行了改造,将LRU链分成 5/8的young区和 3/8的old区。当数据需要淘汰时直接从old区的末尾开始,而当新访问数据页时先判断在缓存中是否存在,如果不存在则直接将数据添加到old区,否则当满足下面配置时才会真正移动到young区域:    innodb_old_blocks_time:mysql缓存页使用的LRU,old区升级为young区的时间判断。默认值1000,单位毫秒;

Mysql - MySQL索引(复合索引、覆盖索引、索引下推、前缀索引)_第2张图片

    而对于数据库而言,数据页或者数据行本身的存储结构如下图  表空间 -> 段(叶子点段、非叶子点段、回滚段)-> 区 -> 页 -> 行

Mysql - MySQL索引(复合索引、覆盖索引、索引下推、前缀索引)_第3张图片

    表空间:数据库由一个或者多个表空间组成;表是一个逻辑容器,表空间存储的对象是段,在一个表空间中有一个或者多个段,但是一个段只能属于一个表空间,如上图。

    段:段由一个或者多个区组成,段不要求区与区之间相邻,当创建数据库或者索引时就会创建相应的段。

    区:在InnoDB中,一个区会分配64个连续的页。

    页:默认大小是16KB

 

InnoDB主键索引和二级索引

    InnoDB索引由B+树构成,B+树由非叶子节点和叶子节点组成。非叶子节点不存储数据,而叶子节点存储数据。InnoDB中主键索引叶子节点存储的是整个表的行数据信息,称为聚簇索引,而非主键索引(也叫二级索引)的叶子节点存储的是主键的值(内存地址、指针)。

    如果我们在表上为非主键创建索引时,相当于是维护了一棵二级索引的B+树,经过优化器判断后知道要查找该二级索引B+树。该B+树的非叶子节点存储的是索引字段的值,叶子节点存储的是主键索引B+树的值,查询过程就是两棵B+树(两次时间复杂度为O(logN)的查询过程)。二级索引的根据主键id到主键索引上的查询过程叫做回表

    如果是根据主键id查询那么就是直接查询主键B+树,时间复杂度是O(logN)。

    如果查询的字段没有索引,那么就是在主键的B+树上,进行树的遍历匹配值过程,时间复杂度是O(N)。

Mysql - MySQL索引(复合索引、覆盖索引、索引下推、前缀索引)_第4张图片

 

复合索引(一棵B+树过滤过个条件)

    覆盖索引就是将一个表的多个字段创建同一个索引,即在B+树上非叶子节点上存储的就是多个有序字段的值。有序就是最左原则,也是面试的高频点。创建语句:

ALTER TABLE 表名称 ADD INDEX idx_索引名称 (字段1, 字段2, 字段N);

   关于复合索引经常的面试题就是(最左原则),那是因为在B+树上,每个节点都是基于创建索引时的有序字段,只要理解了复合索引的B+树就理解的最左:

Mysql - MySQL索引(复合索引、覆盖索引、索引下推、前缀索引)_第5张图片

   在表中创建了一个复合索引 A,B,C,下面哪些查询条件可以使用索引(如 where A = ? and B = ?and C = ?):

   A,B,C  可以走索引

   AB       可以走索引

   A          可以走索引

   BC       不能走索引

   AC       不能走索引

   CBA     可以走索引,因为在优化器阶段,可以将查询条件顺序调整为:ABC

 

覆盖索引(不用回标)

   上面的使用二级索引再查询主键索引时涉及到了回标的操作,如果查询的条数比较很多,查询的字段也比较多,那么回标操作的代价就比较大。如果查询的字段以及在二级索引中(二级索引中包含了主键ID),那么就没有必要进行回表,称为覆盖索引。所以一般要求我们在写select语句时不要使用select * ,因为这种情况肯定是不能走覆盖索引的。并且覆盖索引往往伴随着复合索引,因为我们一般查询的字段不止 单索引字段和主键。

 

索引下推(减少回表次数)

    比如一张表有 name 和 age 两个字段的复合索引,当我们执行查询条件 where name = “XXX” and age = ‘XX’ and ... 时,在Mysql 5.6的版本中,在复合索引的中匹配到了 name字段值后,则会直接到主键索引上去根据age字段值进行过滤。而在Mysql 5.6之后,在满足了name字段的值后,会继续在复合索引上根据age 字段的值进行过滤,满足条件再回表,减少回表的次数,称为 索引下推(Index Condition PushDown)

前缀索引(长字符串索引优化)

    添加索引时使用 alter table t add index indexName(字段名),默认为全字段索引。但是如果索引的字段为字符串,并且字符长度非常大,比如长度为 100等,那么建立的全字段索引将是一棵非常庞大的B+树。此时,如果字符串的区分度非常大,比如字符串前N位就可以排除大部分的数据,那么我们就可以为该字段建立一个前缀索引。 如:alter table t add index indexName(email(8))

    只是创建前缀索引,如果该字段的字符串只有 5位,而索引的是前 8位,前缀索引本身也不知道是不是真正的全字段信息了,那么就会执行回表操作(即回表操作一定会执行),获取整个字段的信息。所以,使用了前缀索引,一定不可能是覆盖索引。

    由于数据是变动的,并且我们自己根据业务不能完全推断出建立多长的前缀索引是最优的,那么我们根据自己能接受的最大区分度,比如 5%,执行查询操作如下:

select 
    count(distinct left(email,4))as L4,
    count(distinct left(email,5))as L5,
    count(distinct left(email,6))as L6,
    count(distinct left(email,7))as L7,
from t;

前缀索引的优化操作:

1)、倒序

    如果前面的区分度不够大,而后面的区分度比较大,此时我们可以 对该字段进行排序的存、查询操作,比如身份证前面的区分度很低。reverse的过程可以在数据库中处理,也可以在java中先处理好

insert into t(。。。id_card)values(。。。reverse(id_card字段值));

select * from t  where id_card = reverse(id_card字段值);

2)、增加hash字段

    利用散列函数,增加一个hash值 字段,只是需要去均衡增加字段的代价和长字符串索引的代价【如果字符串本身非常长,又要基于精确查询】。

alter table t add id_card_crc int unsigned, add index(id_card_crc);

select field_list from t where id_card_crc=crc32('id_card字段值') and id_card='id_card字段值'   // 使用crc函数处理值的插入、查询

 

    以上两种对前缀索引的改造方案,都只支持精确查找,好处就是节约了空间,节省了扫描的行数。

    从占用的额外空间来看,倒序存储方式在主键索引上,不会消耗额外的存储空间,而 hash 字段方法需要增加一个字段。当然,倒序存储方式使用 4 个字节的前缀长度应该是不够的,如果再长一点,这个消耗跟额外这个 hash 字段也差不多抵消了。在 CPU 消耗方面,倒序方式每次写和读的时候,都需要额外调用一次 reverse 函数,而 hash 字段的方式需要额外调用一次 crc32() 函数。如果只从这两个函数的计算复杂度来看的话,reverse 函数额外消耗的 CPU 资源会更小些。从查询效率上看,使用 hash 字段方式的查询性能相对更稳定一些。因为 crc32 算出来的值虽然有冲突的概率,但是概率非常小,可以认为每次查询的平均扫描行数接近 1。而倒序存储方式毕竟还是用的前缀索引的方式,也就是说还是会增加扫描行数。

 

 

你可能感兴趣的:(数据库,B+树,索引下推,复合索引,覆盖索引,聚簇索引)