ICode9

精准搜索请尝试: 精确搜索
首页 > 数据库> 文章详细

MySQL(InnoDB剖析):---B+树索引的使用(联合索引、覆盖索引、优化器不使用索引的情况、索引提示、MRR优化、ICP优化)

2021-02-14 10:01:32  阅读:195  来源: 互联网

标签:where MRR 查询 --- 索引 优化 select


前言

  • 在了解了B+树索引的本质和实现后,下一个需要考虑的问题是怎样正确地使用B+树索引,这不是一个简单的问题。这里所总结的可能并不适用于所有的应用场合。我所能做的只是概括一个大概的方向。在实际的生产环境使用中,每个DBA和开发人员,还是需要根据自己的具体生产环境来使用索引,并观察索引使用的情况,判断是否需要添加索引。不要盲从任何人给你的经验意见
  • 根据前面的介绍,用户已经知道数据库中存在两种类型的应用,OLTP和OLAP应用
    • 在OLTP应用中,查询操作只从数据库中取得一小部分数据,一般可能都在10条记录以下,甚至在很多时候只取1条记录,如根据主键值来取得用户信息,根据订单号取得订单的详细信息,这都是典型OLTP应用的查询语句。在这种情况下,B+树索引建立后,对该索引的使用应该只是通过该索引取得表中少部分的数据。这时建立B+树索引才是有意义的,否则即使建立了,优化器也可能选择不使用索引
    • 对于OLAP应用,情况可能就稍显复杂了。不过概括来说,在OLAP应用中,都需要访问表中大量的数据,根据这些数据来产生查询的结果,这些查询多是面向分析的查询,目的是为决策者提供支持。如这个月每个用户的消费情况,销售额同比、环比增长的情况。因此在OLAP中索引的添加根据的应该是宏观的信息,而不是微观,因为最终要得到的结果是提供给决策者的。例如不需要在OLAP中对姓名字段进行索引,因为很少需要对单个用户进行查询。但是对于OLAP中的复杂查询,要涉及多张表之间的联接操作,因此索引的添加依然是有意义的。但是,如果联接操作使用的是 Hash Join,那么索引可能又变得不是非常重要了,所以这需要DBA或开发人员认真并仔细地研究自己的应用。不过在OLAP应用中,通常会需要对时间字段进行索引,这是因为大多数统计需要根据时间维度来进行数据的筛选

 

一、联合索引

  • 概念:联合索引是指多表上的多个列进行索引

图示说明

  • 下面创建一个表t,所以你idx_a_b是联合索引,联合的列为(a,b)
create table t(
    a int,
    b int,
    primary key(a),
    key idx_a_b(a,b)
)engine=innodb;
  • 从本质上说,联合索引也是一棵树,不同的是联合索引的键值的数量不是1,而是大于等于2
  • 下面我们以键值的数量为2来介绍,假定两个键值的名称分别为a,b,如下图所示

  • 从图中可以看到多个键值的B+树情况,其实和前面讨论的单个键值的B+数并没有区别,键值都是排序的
  • 重点:如果是联合索引,索引排序是看第一个字段的值, 因此在此处键值的排序时根据a进行排序的,即(1,1)、(1,2)、(2,1)、(2,4)、(3,1)、(3,2)
  • 对于下面的两种查询语句,可以使用(a,b)这个联合索引来查询,因为其实按照a进行查询的
select * from table where a=xxx and b=xxx;
 
select * from table where a=xxx;
  • 但是对于下面的查询语句就不能使用上面那颗B+索引树了,因为其是按照b字段进行查询的
select * from table where b=xxx;

演示案例

  • 联合索引的第二个好处是已经对第二个键值进行了排序处理。例如,在很多情况下应用程序都需要查询某个用户的购物情况,并按照时间进行排序,最后取出最近三次的购买记录,这时使用联合索引可以避免多一次的排序操作,因为索引本身在叶子节点已经排序了
  • 例如,下面创建一个测试表bug_log
create table bug_log(
    userid int unsigned not null,
    buy_date date
)engine=innodb;

  • 插入一些字段
insert into bug_log values(1,'2020-01-01');
insert into bug_log values(2,'2020-01-01');
insert into bug_log values(3,'2020-01-01');
insert into bug_log values(1,'2020-02-01');
insert into bug_log values(3,'2020-02-01');
insert into bug_log values(1,'2020-03-01');
insert into bug_log values(1,'2020-04-01');

  • 对userid字段设置一个单列索引,对userid和buy_date字段同时设置一个联合字段
alter table bug_log add key(userid);
alter table bug_log add key(userid,buy_date);

  • 此时我们查看一下只对userid执行查询时使用到的索引。可以看到可以使用的索引有两个,但是这个查询语句只用了userid索引(因为该索引的叶子节点包含单个键值,所以理论上一个页能存放的记录应该更多)
explain select * from buy_log where userid=2;

  • 现在取出userid为1的最近3次购买记录,然后查看执行计划。可以看到可以使用的索引有两个,但是这个查询语句使用了联合索引(因为在这个查询中buy_date字段已经排序好了,使用联合索引取出数据,无序再对buy_date字段再进行一次排序操作)
explain select * from bug_log where userid=1 order by buy_date desc limit 3;

  • 若查询时强制使用userid索引,那么执行计划如下,可以看到在“Extra”信息中显示“Using filesort”代表需要额外的一次排序操作才能完成查询,而这次显示需要对列buy_date进行排序,因为索引userid中but_date是未排序的:
explain select * from bug_log use index for join(userid) where userid=1 order by buy_date desc limit 3;

 

索引排序

  • 因此联合索引(a,b)是根据列a,b进行排序,因此下列语句可以直接使用联合索引得到结果
select ... from table where a=xxx order by b;
  • 然而对于联合索引(a,b,c)来说,下列语句同样可以直接通过联合索引得到结果:
select ... from table where a=xxx order by b;
select ... from table where a=xxx and b=xxx order by c;
  • 但是对于下面的语句,联合索引不能直接得到结果,其还需要执行一次filesort排序操作,因为索引(a,c)并未排序
select ... from table where a=xxx order by c;

 

二、覆盖索引

  • InnoDB支持覆盖索引(或索引覆盖),即从辅助索引中就可以得到查询的记录,而不需要查询聚集索引中的记录
  • 使用覆盖索引的一个好处是:不包含整行记录的所有信息,故其大小要远小于聚集索引,因此可以减少大量的IO操作

  • 对于InnoDB的辅助索引来说,由于其包含了主键信息,因此其叶子节点存放的数据为(primary key1,primary key2,...,key1,key2,...)。例如,下列语句都可仅使用一次辅助联合索引来完成查询:

演示案例

  • 覆盖索引的另一个好处是对某些统计问题而言的
  • 还是对上面创建的表bug_log进行分析,分析下面的查询
explain select count(*) from bug_log;

 

  • InnoDB并不会选择通过查询聚集索引来进行统计。由于表上还有辅助索引,而辅助索引远小于聚集索引,选择辅助索引可以减少IO操作,故优化器的选择如下图所示:
    • possible_keys为NULL,但是Extra却显示了优化器进行了覆盖索引操作

此外,在通常情况下,诸如(a,b)的联合索引,一般是不可以选择列b中所谓的查询条件。但是如果是统计操作,并且是覆盖索引的,则优化器会进行选择,如下述语句:

  • 从图中可以看到possible_keys依然为NULL,但是key却显示userid_2,即表示(userid,buy_date)的联合索引。Extra同样可以发现Using index提示,表示为覆盖索引
explain select count(*) from bug_log
where buy_date>='2021-01-01' and buy_date<'2021-02-01';

三、优化器选择不使用索引的情况

  • 在某些情况下,当执行explain进行SQL语句的分析时,会发现优化器并没有选择索引去查找数据,而是通过扫描聚集索引,也就是直接进行全表的扫描来得到数据。这样情况多发生于范围查找、JOIN链接操作等情况下
  • 例如:
explain select * from ordertails where orderid>10000 and orderid<102000;
  • 上面的语句查找订单号大于10000的订单详情,通过命令show index from ordertails可以观察到如下所示的情况:
    • 表中有(orderid,productid)的联合主键
    • 还有对于列orderid的单个索引

SQL语句显示的结果如下:

  • 优化器没有按照orderid上的索引来查找数据
  • possible_keys上可以看到查询使用primary、orderid、ordersorder_details3个索引
  • 但是最后的索引使用中,优化器选择了primary聚集索引(也就是表扫描,而非orderid辅助索引扫描)

为什么选择聚集索引

  • 因为用户要选择的数据是整行信息,而orderid索引不能覆盖到我们要查询的信息,因此在对orderid索引查询到指定数据后,还需要一次书签访问来查找整行数据的信息
  • 虽然orderid索引中数据时顺序存放的,但是再一次进行书签查找的数据则是无序的,因此变为了磁盘上的离散操作
  • 如果要求访问的数据量很小,则优化器还是会选择辅助索引,但是当访问的数据占整个表中数据的大部分时(一般是20%左右),优化器会选择通过聚集索引来查找数据
  • 因此之前已经提到过,熟悉怒读要远远快于离散度
  • 因此对于不能进行索引覆盖的情况,优化器选择辅助索引的情况:通过辅助索引查找的数据时少量的。这是由当前传统机械硬盘特性所决定的,即利用顺序读来替换随机读的查找
  • 若用户使用的磁盘时固态硬盘,随机读操作非常快,同时有足够的自信来确认使用辅助索引可以带来更好的性能,那么可以使用关键字force index来强制使用某个索引,例如:
explain select * from ordertails force index(orderid)
where orderid>10000 and orderid<102000;

 

四、索引提示

  • MySQL数据库支持索引提示(index hint),显式地告诉优化器使用哪个索引
  • 个人总结以下两种情况可能需要用到索引提示:
    • MySQL数据库的优化器错误地选择了某个索引,导致SQL语句运行的很慢。这种情况在最新的MySQL数据库版本中很少见。优化器在绝大部分情况下工作得都非常有效和正确。这时有经验的DBA或开发人员可以强制优化器使用某个索引,以此提高SQL运行的速度
    • 某SQL语句可以选择的索引非常多,这时优化器选择执行计划时间的开销可能会大于SQL语句本身。例如,优化器分析Range查询本身就是比较耗时的操作。这时DBA或开发人员分析最优的索引选择,通过索引提示来强制使优化器不进行各个执行路径的成本分析,直接选择指定的索引来完成查询
  • 语法格式:
    • USE INDEX:提示操作,提示优化器可以使用某个索引,但是实际上使用的索引还是根据优化器自己选择
    • FORCE INDEX:强制操作,让优化器强制根据自己提供的索引来进行查询工作
  • 使用语法如下:
explain select * from ordertails force index(orderid)
where orderid>10000 and orderid<102000;

演示案例

  • 创建一个表t,并填入相应的数据
create table t(
    a int,
    b int,
    key (a),
    key (b)
)engine=innodb;
insert into t select 1,1;
insert into t select 1,2;
insert into t select 2,3;
insert into t select 2,4;
insert into t select 1,2;

分析下面的查询语句:

  • possible_keys显示了SQL语句可使用的索引为a,b
  • key显示了实际使用的索引,同样也为a和b,也就是所这个查询语句使用a和b两个索引来完成这一查询
  • Extra提示的Using intersect(b,a)表示根据两个索引得到的结果进行求交的数学运行,最后得到结果
explain select * from t where a=1 and b=2;

 

现在我们使用“USE INDEX”的索引提示来使用a这个索引,结果如下: 

  • 虽然我们制定了使用a索引,但是优化器实际选择的是通过表扫描的方式
  • 因此,USE INDEX只是高度优化器可以选择该索引,实际上优化器还是会再根据自己的判断进行选择
explain select * from t use index(a) where a=1 and b=2;

 

现在使用“FORCE INDEX”的索引提示,结果如下: 

  • 我们使用FORCE INDEX的索引提示之后,优化器的最终选择和用户指定的索引是已知的
explain select * from t force index(a) where a=1 and b=2;

 

 

 

五、Multi-Range Read(MRR)优化

  • MySQL 5.6版本开始支持Multi-Range Read(MRR)优化。MRR优化的目的就是为了减少磁盘的随机访问,并且将随机访问转换为较为顺序的数据访问,这对于IO-bound类型的SQL查询语句可带来性能极大的提升
  • MRR优化的好处:

    • MRR使数据访问变得较为顺序。在查询辅助索引时,首先根据得到的查询结果按照主键进行排序,并按照主键排序的顺序进行书签查找

    • 减少缓冲池中页被替换的次数

    • 批量处理对键值的查询操作

  • 对于InnoDB和MyISAM存储引擎的范围查询和JOIN查询操作,MRR工作方式如下:
    • 将查询得到的辅助索引键值存放在一个缓存中,这是缓存中的数据是根据辅助索引键值排序的
    • 将缓存中的键值根据RowID进行排序
    • 根据RowID的排序顺序来访问实际的数据文件
  • 此外,若InnoDB存储引擎或者MyISAM存储引擎的缓冲池不足够大,即不能存放下一张表中的所有数据,此时频繁的离散读操作还会导致缓存中的页被替换出缓冲池,然后有不断地被读入缓冲池。若按照主键顺序访问,则可以将重复行为降为最低

范围查询演示案例

  • salary表上有一个辅助索引idx_s,因此除了通过辅助索引查询键值外,还需要通过书签查找来进行对整行数据的查询
  • 有下面的SQL语句:
explain select * from slaries where salary>10000 and salary<40000;
  • 当不启用MRR特性时,看到的执行如下图所示:

 

  • 若启动MRR特性,则除了会在列Extra看到Using index condition外,还会看见Using MRR选项,如下图所示:

  • 而在实际的执行中两个执行时间差距非常大,见下图

  • 可见MRR将访问数据转换为顺序后查询性能得到了很大的提高

 

将范围查询拆分为键值对演示案例

  • 此外,MRR还可以将某些范围查询,拆分为键值对,以此来进行批量的数据查询。这样做的好处是可以在拆分过程中,直接过滤一些不符合查询条件的数据
  • 例如:
select * from t where
key_part1>=1000 and key_part1<2000 and let_part2=10000;
  • 表t中有(key_part1,key_part2)的联合索引,因此索引根据key_part1,key_part2的位置关系进行排序
  • 若没有启动MRR,此时查询类型为范围查询,SQL优化器会先将key_part1大于1000且小于2000的数据都取出,即使key_part2不等于1000。待取出行数据后再根据key_part2的条件进行过滤。这会导致无用数据被取出。如果有大量的数据且其key_part2不等于1000,则启用MRR优化会使性能有巨大的提升
  • 如果启用了MRR,优化器会先将查询条件进行拆分,然后再进行数据查询。就上述查询而言,优化器会将查询条件拆分为(1000,1000),(1001,1000),(1002,1000),......,(1999,1000),最后再根据这些拆分出的条件进行数据的查询
  • 现在来看一个实际的例子。例如:
select * from salaeies
where (from_date between '1986-01-01' and '1995-01-01')
and (salary between 38000 and 40000);
  • 若启用MRR优化,则执行计划如下图所示:

 

optimizer_switch选项

  • 是否启用MRR优化可以通过这个参数中的标记(flag)来控制
    • mrr标志:表示是否启用MRR优化。当为on时,表示启用MRR优化
    • mrr_cost_bases标记:表示是否通过cost bases的方式来选择是否启用mrr
  • 若mrr为on,mrr_cost_bases设为off,则总是启用MRR优化。
  • 例如,下述语句将MRR优化总是设为开启状态

read_rnd_buffer_size参数

  • 用来控制键值的缓冲区大小
  • 当大于该值时,则执行期对已经缓存的数据根据RowID进行排序,并通过RowID来取得行数据
  • 该值默认为256K

六、Index Condition Pushdown(ICP)优化

  • 和MRR优化一样,Index Condition Pushdown同样是MySQL 5.6开始支持的一种根据索引进行查询的优化方式
  • 之前的MySQL不支持ICP,当进行索引查询时,首先根据索引来查找记录,然后再根据WHERE条件来过滤条件。支持了ICP之后,MySQL会在取出索引的同时,判断是否可以进行WHERE条件的过滤,也就是讲WHERE的部分过滤操作放在了存储引擎层
  • 在某些查询下,可以大大减少上层SQL层对记录的索引(fetch),从而提高数据库的整体性能
  • ICP优化支持range、ref、eq_ref、ref_or_null类型的查询,当前支持MyISAM和InnoDB存储引擎
  • 当优化器选择ICP优化时,可在执行计划的列Extra看到Using index condition提示

演示案例

  • 假设下面的表有联合索引(zip_code,last_name,firset_name)
  • 查询语句如下:
select * from people where zipcode='95054'
and lastname like '%etrunia%'
and address like '%Main Street%';
  • 对于上述语句,MySQL可以通过索引来定位zipcode等于95054的记录,但是索引对WHERE条件的lastname和address条件没有任何帮助
  • 若不支持ICP优化,则数据库需要先通过索引取出所有zipcode等于95054的记录,然后再过滤WHERE之后的两个条件
  • 若支持ICP优化,则在索引取出时,就会进行WHERE条件的过滤,然后再去获取记录。这极大提高查询的效率。当然,WHERE可以过滤的条件是要该索引可以覆盖到的范围
  • 现在看下面的SQL语句:
select * from salaries where (from_date between '1986-01-01' and '1995-01-01')
and (salary between 38000 and 40000);
  • 如果不启用MRR优化,则执行计划如下

  • 可以看到Extra有Using index condition的提示。但是为什么这里的idx_s索引会使用Index Condition Pushdown优化呢?因为这张表的主键是(emp_no,from_date)的联合索引,所以idx_s索引中包含了from_date的数据,故可以使用此优化方式
  • 下表对比了在MySQL 5.5和MySQL 5.6中上述SQL语句的执行时间,并且同时比较开启MRR后的执行时间

  • 上述的执行时间的比较同样是不对缓冲池做任何的预热操作。可见Index Condition Pushdown优化可以将查询效率在原有MySQL 5.5版本的技术上提高23%。而再同时启用MRR优化后,性能还能有400%的提升

 

 

标签:where,MRR,查询,---,索引,优化,select
来源: https://blog.csdn.net/m0_46405589/article/details/113805792

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有