Mysql

关注公众号 jb51net

关闭
首页 > 数据库 > Mysql > MySQL之B+Tree、索引

MySQL之B+Tree、索引及使用时优化方式

作者:祁仙森

MySQL索引优化关键点包括BTree、Hash、R-tree及Full-text索引,并详述BTree结构与操作,强调索引使用场景与失效情形,如优化SQL需考虑索引选择、覆盖索引及分页优化策略

索引结构

MySQL目前提供了以下四种索引

InnoDB引擎MyISAM引擎Memory引擎
BTREE索引支持支持
HASH索引不支持不支持
R-tree索引不支持支持
Full-text5.6版本之后支持支持

通常所说的索引,若为特别指出,基本值B+树(多路搜索树,并不一定是二叉树)结构组织的索引。

其中聚集索引、复合索引、前缀索引、唯一索引默认都是使用B+tree树索引,统称为索引

BTREE结构

Btree又叫多路平衡树,一颗m叉的btree特性如下:

以5叉BTREE为例

插入步骤

key的数量:[ceil(m/2) - 1]<=n<=m-1。所以2<=n<=4。当n>4时,中间节点分 裂到父节点,两边节点分 裂

以插入C N G A H E K Q M F W L T Z D P R X Y S数据为例

演变过程:

首先,结点空间足够,4个字母插入相同的结点中

再插入H,n>4,空间不够,中间元素G字母向上分 裂到新的节点(当H插入的时候ACGHN,其中中间节点为G,所以是G向上分 裂)

插入E,K,Q不需要分 裂,各节点空间足够

插入M,在HKNQ块插入M(HKMNQ中间元素是M),元素M向上分 裂到父节点G,HK与NQ分 裂

插入F、W、L、T不需要分 裂

插入Z,Z大于M,走最右边指针,指向NQTW的节点,在这个节点中NQTW插入Z,Z排序后是最大,中间元素为T,但是此时n最大为4,T需要向上分 裂,同时NQ和WZ分 裂

插入D,与上述插入Z同样的步骤,D是中间元素,向上分 裂,AC和EF分 裂,而D插入到父节点中还能保持平衡;然后继续插入P、R、X、Y都满足Btree特性,不需要分 裂

最后插入S,S大于M小于T走第四个指针,向NPQR中插入,插入后事NPQRS,key个数超过4了,所以中间元素Q向上分 裂,同时NP、RS分 裂;Q插入到父节点中DGMT,变成DGMQT,超过4,中间元素M向上分 裂,同时DG和QT分 裂

删除步骤

例如依次删除上述BTREE中的H、T、R、E

首先删除元素H,当然首先查找H,H在一个叶子结点中,且该叶子结点元素数目3大于最小元素数目ceil(m/2)-1=2,则操作很简单,咱们只需要移动K至原来H的位置,移动L至K的位置(也就是结点中删除元素后面的元素向前移动)

删除T,因为T没有在叶子结点中,而是在中间结点中找到,发现它的继承者W(字母升序的下个元素),将W上移到T的位置,然后将原包含W的孩子结点中的W进行删除,删除W后,该孩子结点中元素个数大于2,满足平衡条件,无需进行合并操作。

删除R元素,R在叶子结点中,但是该结点中元素数目为2,删除导致只有1个元素,已经小于最小元素数目ceil(5/2)-1=2,而由前面我们已经知道:如果其某个相邻兄弟结点中比较丰满(元素个数大于ceil(5/2)-1=2),则可以向父结点借一个元素,然后将最丰满的相邻兄弟结点中上移最后或最前一个元素到父节点中(有没有看到红黑树中左旋操作的影子?),在这个实例中,右相邻兄弟结点中比较丰满(3个元素大于2),所以先向父节点借一个元素W下移到该叶子结点中,代替原来S的位置,S前移;然后X在相邻右兄弟结点中上移到父结点中,最后在相邻右兄弟结点中删除X,后面元素前移。

删除元素E,因为E所在的结点数目刚好满足最小平衡所需条件元素个数,而相邻的兄弟结点也是同样的情况,删除一个元素都不能满足条件,所以需要该节点与某相邻兄弟结点进行合并操作;首先移动父结点中的元素(该元素在两个需要合并的两个结点元素之间)下移到其子结点中,然后将这两个结点进行合并成一个结点。所以在该实例中,咱们首先将父节点中的元素D下移到已经删除E而只有F的结点中,然后将含有D和F的结点和含有A,C的相邻兄弟结点进行合并成一个结点。

但是虽然D元素满足ACDF的平衡条件,而G节点不满足平衡条件,元素个数小于ceil(5/2)-1,而所有相邻节点中元素个数都刚刚满足平衡,不能像第二步骤中那样借,只能像第三步骤那样合并,与兄弟结点进行合并成一个结点,而根结点中的唯一元素M下移到子结点,这样,树的高度减少一层

B+TREE结构

B+Tree为Btree的变种,区别为:

由于B+Tree只有叶子节点保存key信息,查询任何key都要从root走到叶子。所以B+Tree的查询效率更加稳定

MySQL中的B+TREE

MySQL索引数据结构对经典的B+TREE进行了优化。在原有的B+Tree基础上,增加一个指向相邻叶子节点的链表指针,就形成了带有顺序指针的B+Tree,提高区间访问的性能

索引分类

索引语法

创建索引

-- 创建索引的时候不指定索引的类型的时候默认使用的是B+TREE索引

-- 方式一:使用create的方式创建索引
create [UNIQUE|FULL TEXT|SPATIAL] INDEX index_name [USING index_type] on table_name(index_col_name,...);
-- 方式二:使用alter方式创建索引
-- 普通的索引
alter table table_name add index index_name(columnName_list);
-- 唯一索引(索引的列里面的值除了null可以多次出现,其余的值必须唯一)
alter table table_name add unique index index_name(columnName_list);
-- 全文索引
alter table table_name add fulltext index_name(columnName_list);

查看索引

show index from table_name;

删除索引

drop index index_name on table_name;

索引失效情形

定义了索引而索引失效的分析

情形一:对使用索引的字段在条件查询的时候使用了函数

情形二:

例如一张表中字段name,status,address构成了复合索引,顺序是create index name_status_address_index on table_name(name,status,address),其中name、status、address都是varchar类型的

情形三:

上述案例中索引情形

等值条件查询时where name = 'aa' and status = 1只有name走索引,status涉及了隐式转换,不走索引

情形四:

使用or关联条件的时候,如果or前面的字段使用了索引,而后面的字段没有索引,那么是不走索引的

例如

select name,status,address where name = 'aa' or remark = 'bb';
-- 使用explain分析的时候name也没有走索引,索引失效了

select name,status,address where name = 'aa' and remark = 'bb';
-- 使用explain分析的时候name是走索引的,与or相反
-- 还是上述的索引案例,这里id字段是表table_name的主键,所以是有id字段索引的;
-- 此处即使后面模糊匹配使用的是%开头了,但是explain执行下面的情形的时候我们发现还是走name_status_address_index名字索引
select id,name,status,address where name like '%aa%';

使用in和not in的时候,使用in走索引,not in不走索引(8版本之前是这样,8版本之后都走索引)

有表信息,里面sellerid为主键,name,status,address为上述描述的联合索引

5.7版本分别执行in和not in的案例

8版本执行案例,in和not in都走索引

情形五:

如果MySQL评估使用索引比全表更慢,则不使用索引,如上面情形四种in和not in案例的数据,里面sellerid为主键,name,status,address为上述描述的联合索引,现在再设置一个address的单独的索引create index idx_seller_address on seller(address);

当执行select * from seller where name = '北京市';我们发现即使使用了定义了索引,但是使用expalin的时候却没有像我们预期的那样,而是走的全表索引,'北京市’的太多,估算不走索引,走全表扫描;

但是执行select * from seller where name = '西安市';这个时候,我们发现依旧走idx_seller_address索引。

也就是说索引中的某个字段的值占据的比例特别大,基本占据了全表的记录,那么就不走索引,直接全表扫描

情形六

is null和is not null有时走索引,有时不走索引

就像上面所说的那样,字段占据全表的记录多为null的话执行is null就不走索引,走is not null就会走索引;相反索引字段中值为null少的话,执行字段is null的时候走索引,走is not null就不走索引

例如数据

name字段有索引

当执行elplain select * from t_user where name is null;的时候

当执行elplain select * from t_user where name is not null;的时候

SQL优化的细节

例如上述的复合情形

select * from table_name where name= 'aa';
-- 此sql语句使用explain分析的时候可以看到Extra信息为Using index Condition,就是说查询的结果还需回表查询所有信息

select name,status,address from table_name where name= 'aa';
-- 此sql语句使用explain分析的时候可以看到Extra信息为Using where;Using index,就是说,查询直接用到了索引中的数据,不用再回表查询

select name,status,remark from table_name where name = 'aa';
-- 此sql语句使用explain分析的时候可以看到Extra信息为Using index Condition,就是说查询的结果中remark字段不再符合索引中,还需回表查询信息,所以extra为Using index Condition

大批量导入数据

大批量导入数据使用load指令导入大批量数据的时候

-- load指令,其中filepath,table_name为具体文件路径,和表名,','表示字段值之间以逗号分隔,\n表示换行符
load data local infile 'filepath' into table `table_name` fields terminated ',' line terminated '\n';
-- 会话级关闭
set UNIQUE_CHECKS=0;
-- 导入完成后,再开启
set UNIQUE_CHECKS=1;

insert时候优化的细节

多数据插入的时候尽量将数据拼接在一起,开启手动提交事务,被 插入的数据记录对应的主键有序

order by排序的时候

有两种情形在explain的extra中的信息,一种执行时使用到索引using index,一种执行时没有使用到索引Using filesort

例如有emp表,表中的age和salary是复合索引

执行下面的情形始终在explain的extra是filesort,即使order by字段建立了索引,但是没有使用到

但是在查询的使用覆盖索引的时候,即查询的字段只有索引内的字段,但是没有索引外的字段,下面是只有索引内的字段展示;一旦select后面的字段有除索引定义的字段外的字段,如name的时候就不走索引了;当然也会出现一种情形,即使使用了覆盖索引,而在排序的条件,

一个字段升序,一个字段降序,又会出现Using filesort的情形了;两个字段同时升序或同时降序也会用到索引,但是字段必须按照索引定义的时候的字段先后顺序,不然即便是同样的排序方式,字段先后顺序变了,也会出现Using filesort的情形;一个字段增降序同样用到索引

对出现filesort的优化

通过创建合适的索引,能够减少Filesort 的出现,但是在某些情况下,条件限制不能让Filesort消失,那就需要加快Filesort的排序操作。对于Filesort,MySQL 有两种排序算法:

1)两次扫描算法: MySQL4.1之前,使用该方式排序。首先根据条件取出排序字段和行指针信息,然后在排序区sort buffer中排序,如果sort buffer不够,则在临时表temporary table中存储排序结果。完成排序之后,再根据行指针回表读取记录,该操作可能会导致大量随机I/O操作。

2.)一次扫描算法: 一次性取出满足条件的所有字段,然后在排序区sort buffer中排序后直接输出结果集。排序时内存开销较大,但是排序效率比两次扫描算法要高。MySQL通过比较系统变量max_length_for_sort_data的大小和Query语句取出的字段总大小,来判定是否那种排序算法,如果max_length_for_sort_data更大,那么使用第二种优化之后的算法;否则使用第一种。

可以适当提高sort_buffer_sizemax_length_for_sort_data系统变量,来增大排序区的大小,提高排序的效率。

group by语句的优化

由于**GROUP BY实际上也同样会进行排序操作**,且与ORDER BY相比,GROUP BY主要只是多了排序之后的分组操作。当然,如果在分组的时候还使用了其他的一些聚合函数,那么还需要一些聚合函数的计算。所以,在GROUP BY的实现过程中,与ORDER BY一样也可以利用到索引。

如果查询包含group by但是用户想要避免排序结果的消耗,则可以执行order by null禁止排序。如下:

而上面中的Using temporary是使用到了临时表的含义,因为age没有索引

子查询优化

MySQL4.1版本之后,开始支持SQL的子查询。这个技术可以使用SELECT语句来创建一个单列的查询结果 ,然后把这个结果作为过滤条件用在另一个查询中。使用子查询可以一次性的完成很多逻辑上需要多个步骤才能完成的SQL操作,同时也可以避免事务或者表锁死,并且写起来也很容易。但是,有些情况下,查询是可以被更高效的连接(JOIN )替代。

or优化

上面说到or关键字的时候,若or前面的字段有索引,后面的字段没有索引,查询时整个是不走索引的;而若有复合索引的时候,例如有一个复合索引create index idx_age_salary on table_name (age,salary);在使用查询的时候select * from table_name where age = 20 or salary = 3000;这条语句即便使用到了age和salary,而age和salary是复合索引,但实际查询的时候不会走索引建议使用union替换or

情形一:

情形二:

此处使用id和age分别走不通索引名的索引,但是type是index_merge

再看看使用union的情形,此处的type分别是常量const和ref,效率还是高于上面的index_merge

limit分页查询优化

分页查询时,通过创建覆盖索引能够比较好地提高性能。一个常见又非常头疼的问题就是limit 2000000,10 , 此时需要MySQL排序前2000010记录,仅仅返回2000000-2000010的记录,其他记录丢弃,查询排序的代价非常大

优化思路一:在主键上完成排序分页操作,最后根据主键关联回原表查询所需要的其他列内容

优化思路二:此方法有一定局限性,只适用于id自增的表,且主键不能断层,(若中间出现某个记录被删除,id出现断层,结果就不准确),可以把limit查询转化成某个位置的查询

索引提示

use index建议MySQL去使用哪个

使用use index人为干预数据库走具体的索引,使用use index仅仅是提供参考,MySQL不一定使用

在执行select * from tb_seller where name = ‘aa’;的时候,可能走两个索引,虽然最终只走一个索引,看下explain具体情形,在使用use index(index_name)的时候就会指定了

ignore index忽略索引

有点类似于上面的use index,不过是反着来的感觉

froce index强制使用某个索引

例如某个字段有索引,但是执行的时候遇到了索引值的记录过多的情形,MySQL会认为不走索引更高效的情形,就不走索引,而是全表扫描了

使用force index后的情况

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文