0
点赞
收藏
分享

微信扫一扫

Mysql<八> Mysql中为什么使用B+树作为数据的存储结构,索引如何优化?

捌柒陆壹 2022-04-18 阅读 36
mysql

文章目录

前言

概念

索引是帮助MySQL高效获取数据的数据结构。更通俗的说,数据库索引好比是一本书前面的目录,能加快数据库的查询速度。

一般来说索引本身也很大,不可能全部存储在内存中,因此索引往往是存储在磁盘上的文件中的(可能存储在单独的索引文件中,也可能和数据一起存储在数据文件中)。

我们通常所说的索引,包括聚簇索引、覆盖索引、组合索引、前缀索引、唯一索引等,没有特别说明,默认都是使用B+树结构组织(多路搜索树)的索引。

优点缺点

优势

  • 可以提高数据检索的效率,降低数据库的IO成本,类似于书的目录。
  • 通过索引列对数据进行排序,降低数据排序的成本,降低了CPU的消耗。
  • 被索引的列会自动进行排序,包括【单列索引】和【组合索引】,只是组合索引的排序要复杂一些。
  • 如果按照索引列的顺序进行排序,对应order by语句来说,效率就会提高很多。

劣势

  • 索引会占据磁盘空间。
  • 索引虽然会提高查询效率,但是会降低更新表的效率。比如每次对表进行增删改操作,MySQL不仅要保存数据,还有保存或者更新对应的索引文件。

索引的分类

按照索引关节子的数量分类:

  • 单列索引
    1、主键索引;
    2、普通索引
    3、唯一性索引;
    4、全文索引;
    5、空间索引;
    6、前缀索引;
# 主键索引:
ALTER TABLE table_name ADD PRIMARY KEY (column_name);
# 普通索引:
ALTER TABLE table_name ADD INDEX index_name (column_name) ;
# 唯一性索引:
CREATE UNIQUE INDEX index_name ON table(column_name) ;
# 前缀索引:
ALTER TABLE table_name ADD INDEX index_name (column1(length));
  • 组合索引

组合索引的使用,需要遵循最左前缀原则(最左匹配原则,后面详细讲解)。

一般情况下,建议使用组合索引代替单列索引(主键索引除外,具体原因后面知识点讲解)。

ALTER TABLE table_name ADD INDEX index_name (column1,column2);

其他的索引操作语句还有:

  • 删除索引
DROP INDEX index_name ON table
  • 查看索引
SHOW INDEX FROM table_name \G

索引的数据结构

索引需要满足什么基本需求?

索引的数据结构,至少需要支持两种最常用的查询需求:

  1. 等值查询:根据某个值查找数据,比如: select * from t_user where age=76;
  2. 范围查询:根据某个范围区间查找数据,比如: select * from t_user where age>=76 andage<=86;
  3. 排序
  4. 分组

同时需要考虑时间和空间因素:性价比高

  • 在执行时间方面,我们希望通过索引,查询数据的时间尽可能小;
  • 在存储空间方面,我们希望索引不要消耗太多的内存空间和磁盘空间。

考虑以下数据结构:

Hash表

Hash表,常见的数据结构之一。我们使用Hash表存储表数据Key可以存储索引列,Value可以存储行记录或者行磁盘地址。Hash表在等值查询时效率很高,时间复杂度为O(1);

  • 但是不支持范围快速查找,范围查找时还是只能通过扫描全表方式
  • 数据结构比较稀疏,不适合做聚合,不适合做范围等查找

使用场景:
对查询并发要求很高,K/V内存数据库,缓存

二叉查找树
  • 二叉树特点:每个节点最多有2个分叉,左子树和右子树数据顺序左小右大。
  • 二叉树的检索复杂度和树高相关:理想状态下效率可以达到O(logn)
  • 那是不是任何列使用二叉树效率都会提升呢?答案是否定的。

极端情况下,二叉查找树会构建成为单向链表=查找全表扫描。
对磁盘不友好【一旦变成了全表扫描,磁盘io将是极其沉重】

红黑树

红黑树是一个近似平衡二叉树,在建立mysql的索引的时候,要谨慎
平衡二叉树是采用二分法思维,平衡二叉查找树除了具备二叉树的特点,最主要的特征是树的左右两个子树的层级保持平衡。在插入删除数据时通过左旋/右旋操作保持二叉树的平衡,不会出现左子树很高、右子树很矮的情况。
使用平衡二叉查找树查询的性能接近于二分查找法,时间复杂度是 O(log2n)。
unique key 为什么不用红黑树,反正只存一个主键?
平衡二叉树存在的问题

  1. 时间复杂度和树高相关:树有多高就需要检索多少次,每个节点的读取,都对应一次磁盘 IO 操作【瓶颈】。
  • 磁盘每次寻道时间为10ms,在表数据量大时,对响应时间要求高的场景下,查询性能就会出现瓶颈。
  • 举例:1百万的数据量,log2n约等于20次磁盘IO,时间20*10=0.2s
  1. 平衡二叉树不支持范围查询快速查找范围查询时需要从根节点多次遍历,查询效率极差
    如何减少IO操作次数呢?
B树

多想要减少耗时的IO操作,就要尽量降低树的高度。每个节点存储多个元素,在每个节点尽可能多的存储数据。每个节点可以存储1000个索引(16k/16=1000),这样就将二叉树改造成了多叉树,通过增加树的叉树,将树从高瘦变为矮胖。

主要特点:

  1. B树的节点中存储着多个元素,每个内节点有多个分叉。
  2. 节点中的元素包含键值和数据,节点中的键值从大到小排列。也就是说,在所有的节点都储存数据
  3. 父节点当中的元素不会出现在子节点中。
  4. 所有的叶子结点都位于同一层,叶节点具有相同的深度,叶节点之间没有指针连接。

以下面的B树为例,我们的键值为表主键,具备唯一性。
在这里插入图片描述
B树如何查询数据?:假如我们查询值等于15的数据。查询路径磁盘块1->磁盘块2->磁盘块7。
优点:

  • 磁盘IO次数会大大减少。
  • 磁盘块是一次性读取的,块中数据比较是在内存中进行的,比较的耗时可以忽略不计。
  • B树的高度一般2至3层就能满足大部分的应用场景,所以使用B树构建索引可以很好的提升查询的效率。

缺点:

  • B树不支持范围查询的快速查找:如果我们想要查找15和26之间的数据,查找到15之后,需要回到根节点重新遍历查找,需要从根节点进行多次遍历,查询效率有待提高。
  • 空间占用较大:如果data存储的是行记录,行的大小随着列数的增多,所占空间会变大。一个页中可存储的数据量就会变少,树相应就会变高,磁盘IO次数就会变大。
B+树

在B树基础上,MySQL在B树的基础上继续改造,使用B+树构建索引。B+树和B树最主要的区别在于非叶子节点是否存储数据的问题以及叶子节点的指针链接

  • B树:非叶子节点和叶子节点都会存储数据。
  • B+树:只有叶子节点才会存储数据,非叶子节点至存储键值。叶子节点之间使用双向指针连接,最底层的叶子节点形成了一个双向有序链表。

B+树的最底层叶子节点包含所有索引项
在这里插入图片描述

  • 等值查询:假如我们查询值等于15的数据。查询路径磁盘块1->磁盘块2->磁盘块5。
  • 范围查询:假如我们想要查找15和26之间的数据。
    查找路径是磁盘块1->磁盘块2->磁盘块5。
    首先查找值等于15的数据,将值等于15的数据缓存到结果集【三次磁盘IO】。
    查找到15之后,底层的叶子节点是一个有序列表,我们从磁盘块5,键值15开始向后遍历筛选所有符合筛选条件的数据。
    第四次磁盘IO:根据磁盘5后继指针到磁盘中寻址定位到磁盘块6,将磁盘6加载到内存中,在内存中从头遍历比较,15<17<26,15<26<=26,将data缓存到结果集。

对比:
优点:

  • 继承了B树的优点【多叉树的优点】
  • 保证等值和范围查询的快速查找

MySQL索引实现

首先对两种引擎去分析:

Myisam引擎的索引

首先建立表:

CREATE TABLE `t_user_myisam` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(20) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_age` (`age`) USING BTREE
) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
insert into t_user_myisam values(15,'Nick',5);
insert into t_user_myisam values(18,'Zero',22);
insert into t_user_myisam values(20,'Tom',34);
insert into t_user_myisam values(30,'Nick',55);
insert into t_user_myisam values(49,'Mary',22);
insert into t_user_myisam values(50,'James',77);
insert into t_user_myisam values(56,'John',89);
insert into t_user_myisam values(77,'Lily',100);

Myisam的引擎建立的表的数据文件如下:
在这里插入图片描述
FRM文件:包含表的结构的信息。
MYD文件:包含表的数据信息。
MYI文件:包含表的索引信息。

其中的而索引文件的数据结构就是B+树。

在这里插入图片描述
如图,可以理解为MYI就是B+树,MYD就是表数据。MYI中含有叶子节点存储的是表中对应行记录的地址信息

主键索引
  • 等值查询数据分析
 select * from t_user_myisam where id=30;
  • 范围查询
select * from t_user_myisam where id between 30 and 49;
辅助查询

在 MyISAM 中,辅助索引和主键索引的结构是一样的,没有任何区别,叶子节点的数据存储的都是行记录的磁盘地址。只是主键索引的键值是唯一的,而辅助索引的键值可以重复。查询数据时,由于辅助索引的键值不唯一,可能存在多个拥有相同的记录,所以即使是等值查询,也需要按照范围查询的方式在辅助索引树中检索数据。

总结
  • 索引时使用的B+树中键值是主键的是主键查询;键值不是主键的是辅助查询。
  • Myisam引擎中的索引如果是主键索引,由于具有唯一性,等值查询时根据叶子结点的地址直接找到;范围查询时就需要在等值查询(最小值)后再叶子节点的链表中遍历查询。
  • Myisam引擎中的而索引如果时辅助索引,建立的辅助索引的结构也和主键查询的结构一致(默认B+树)。也就是说辅助索引的叶子节点也是相关行记录的地址。不同的是,辅助查询的键值不唯一,无论等值查询还是范围查询,用的都是再叶子节点往后遍历的方法查询。

INNIDB引擎的索引

创建表的SQL语句:

CREATE TABLE `t_user_innodb` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(20) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_age` (`age`) USING BTREE
) ENGINE=InnoDB;
insert into t_user_innodb values(15,'Nick',5);
insert into t_user_innodb values(18,'Zero',22);
insert into t_user_innodb values(20,'Tom',34);
insert into t_user_innodb values(30,'Nick',55);
insert into t_user_innodb values(49,'Mary',22);
insert into t_user_innodb values(50,'James',77);
insert into t_user_innodb values(56,'John',89);
insert into t_user_innodb values(77,'Lily',100);

INNODB的创建的数据如下:
在这里插入图片描述
其中的FRM文件为表的结构信息;IBD文件为表的索引和数据信息。
在这里插入图片描述
可以看到与Myisam不同,叶子节(主键索引)点存储的是一个行记录的信息

聚簇索引
  • 主键索引的叶子节点会存储数据行,辅助索引只会存储主键值
  • InnoDB要求表必须有一个主键索引(MyISAM 可以没有)。
  • 除了聚簇索引,其他的都是辅助索引。
    创建规则:
  • 等值查询
select * from t_user_innodb where id=30;

在这里插入图片描述

  • 范围查询
select * from t_user_innodb where id between 30 and 49;

可以看到,因为在主键索引中直接存储了行数据,所以InnoDB在使用主键查询时可以快速获取行数据。
当表很大时,与在索引树中存储磁盘地址的方式相比,因为不用再去磁盘中获取数据,所以聚簇索引通常可以节省磁盘IO操作

辅助索引
  • 除聚簇索引之外的所有索引都称为辅助索引,InnoDB的辅助索引只会存储主键值而非磁盘地址。
  • 以表t_user_innodb的age列为例,age索引的索引结果如下图。
  • 底层叶子节点的按照(age,id)的顺序排序,先按照age列从小到大排序,age列相同时按照id列从小到大排序。
  • 使用辅助索引需要检索两遍索引:
    1、首先检索辅助索引获得主键
    2、然后使用主键到主索引中检索获得记录。

在这里插入图片描述

  • 等值查询
select * from t_user_innodb where age=22
什么是回表查询?

根据在辅助索引树中获取的主键id,到主键索引树检索数据的过程称为回表查询。

在这里插入图片描述

  • 范围查询
select * from t_user_innodb where age between 30 and 49;
总结
  • innoDB引擎的聚簇查询时必须要有的,且为创建表时指定。
  • inniDB引擎的主键查询和辅助查询的B+树叶子节点的数据不一致。
  • innoDB引擎的辅助查询是根据回表机制进行的。

索引优化

组合查询

组合查询属于辅助查询。

我们在使用索引时,组合索引是我们常用的索引类型。那组合索引是如何构建的,查找的时候又是如何进行查找的呢?
表t_multiple_index,id为主键列,创建了一个联合索引idx_abc(a,b,c),构建的B+树索引结构如图所示。索引树中节点中的索引项按照(a,b,c)的顺序从大到小排列,先按照a列排序,a列相同时按照b列排序,b列相同按照c列排序。在最地城的叶子节点中,如果两个索引项的a,b,c三列都相同,索引项按照主键id排序。
所以组合索引的最底层叶子节点中不存在完全相同的索引项。

CREATE TABLE `t_multiple_index` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`a` int(11) DEFAULT NULL,
`b` int(11) DEFAULT NULL,
`c` varchar(10) DEFAULT NULL,
`d` varchar(10) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_abc` (`a`,`b`,`c`)
) ENGINE=InnoDB;
insert into t_multiple_index (a,b,c,id,d) values(1 ,1 ,4,5,'dll');
insert into t_multiple_index (a,b,c,id,d) values(1 ,5 ,4,2,'doc');
insert into t_multiple_index (a,b,c,id,d) values(5 ,3 ,6,7,'img');
insert into t_multiple_index (a,b,c,id,d) values(13,14,3,4,'xml');
insert into t_multiple_index (a,b,c,id,d) values(13,16,4,1,'txt');
insert into t_multiple_index (a,b,c,id,d) values(13,16,5,3,'pdf');
insert into t_multiple_index (a,b,c,id,d) values(13,16,5,6,'exe');
insert into t_multiple_index (a,b,c,id,d) values(14,14,14,8,'ddd');

在这里插入图片描述
进行查询时:

select * from t_multiple_index where a=13 and b=16 and c=4;
最左匹配原则

组合索引的最左前缀匹配原则:使用组合索引查询时,mysql会一直向右匹配直至遇到范围查询(>、<、between、like)就停止匹配。

创建的idx_abc(a,b,c)索引,相当于创建了(a)、(a,b)(a,b,c)三个索引。
而如果查询条件不包含a列,比如筛选条件只有(b,c)或者c列是无法使用组合索引的。

理解:

优化器优化:

select * from t_multiple_index where b=16 and c=4 and a=13;
#等价于下面的sql,优化器会按照索引的顺序优化
select * from t_multiple_index where a=13 and b=16 and c=4;

explain select a,b from t_multiple_index where b=16;
explain select b from t_multiple_index where b=16 and c=4;
explain select b,c from t_multiple_index where c=4;

索引使用口诀

组合索引的创建原则:

覆盖查询

某一种条件使用组合查询可以使得不用回表也可以得到查询结果,这种条件就是覆盖查询

表t_multiple_index,组合索引idx_abc(a,b,c)的叶子节点中包含(a,b,c,id)四列的值,对于以下查询语句,select中列数据,如果可以直接在辅助索引树上全部获取,也就是说索引树已经“覆盖”了我们的查询需求,这时MySQL就不会白费力气的回表查询,这中现象就是覆盖索引

也就是说select时不要用*。也就是不要有select * from table之类的语句。

select a from t_multiple_index where a=13 and b=16;
select a,b from t_multiple_index where a=13 and b=16;
select a,b,c from t_multiple_index where a=13 and b=16;
select a,b,c,id from t_multiple_index where a=13 and b=16;

此时explain以下去分析这些查询语句的执行,会发现EXTRA= Using index.此时就是覆盖查询,而不是覆盖查询的话,EXTRA= Using where

索引下推 ICP

Index Condition Pushdown,简称ICP。是MySQL5.6对使用索引从表中检索行的
一种优化。ICP可以减少存储引擎必须访问基表的次数以及MySQL服务器必须访问存储引擎的次数。可用于 InnoDB 和 MyISAM 表,对于InnoDB表ICP仅用于辅助索引。
可以通过参数optimizer_switch控制ICP的开始和关闭。

对于语句:

select * from t_multiple_index where a=13 and b>15 and c='5' and d='pdf';
  • 不使用ICP
    由于b>15会中断组合查询,,此时的索引过程如下:
    在这里插入图片描述
  • 使用ICP
    在这里插入图片描述
  • 总结
  • 不使用ICP,不满足最左前缀的组合索引条件的比较是在Server层进行的,非索引条件的比较是在Server层进行的。
  • 使用ICP,所有的组合索引条件的比较是在存储引擎层进行的,非索引条件的比较是在Server层进行的。
  • 对比使用ICP和不使用ICP,可以看到使用ICP可以有效减少回表查询次数和返回给服务层的记录数,从而减少了磁盘IO次数和服务层与存储引擎的交互次数。

索引创建的原则

并不是什么时候的要用索引。

哪些情况需要创建索引

  1. 频繁出现在where 条件字段,order排序,group by分组字段
  2. select 频繁查询的列,考虑是否需要创建联合索引(覆盖索引,不回表)
  3. 多表join关联查询,on字段两边的字段都要创建索引

索引优化建议

  1. 表记录很少不需创建索引 :索引是要有存储的开销
  2. 一个表的索引个数不能过多
    (1)空间:浪费空间。每个索引都是一个索引树,占据大量的磁盘空间。
    (2)时间:更新(插入/Delete/Update)变慢。需要更新所有的索引树。太多的索引也会增加优化器的选择时间。所以索引虽然能够提高查询效率,索引并不是越多越好,应该只为需要的列创建索引。
  3. 频繁更新的字段不建议作为索引:频繁更新的字段引发频繁的页分裂和页合并,性能消耗比较高。
  4. 区分度低的字段,不建议建索引:
    比如性别,男,女;比如状态。区分度太低时,会导致扫描行数过多,再加上回表查询的消耗。
    如果使用索引,比全表扫描的性能还要差。这些字段一般会用在组合索引中。姓名,手机号就非常适合建索引。
  5. 在InnoDB存储引擎中,主键索引建议使用自增的长整型,避免使用很长的字段:主键索引树一个页节点是16K,主键字段越长,一个页可存储的数据量就会越少,比较臃肿,查询时尤其是区间查询时磁盘IO次数会增多。辅助索引树上叶子节点存储的数据是主键值,主键值越长,一个页可存储的数据量就会越少,查询时磁盘IO次数会增多,查询效率会降低。
  6. 不能使用无序的值作为索引:例如身份证、UUID。更新数据时会发生频繁的页分裂,页内数据不紧凑,浪费磁盘空间。
  7. 尽量创建组合索引,而不是单列索引:
    优点:
    (1)1个组合索引等同于多个索引效果,节省空间。
    (2)可以使用覆盖索引
    创建原则:组合索引应该把频繁用到的列、区分度高的值放在前面。频繁
    使用代表索引的利用率高,区分度高代表筛选粒度大,可以尽量缩小筛选
    范围。
举报

相关推荐

0 条评论