0
点赞
收藏
分享

微信扫一扫

深入解读索引在查询中的使用

书呆鱼 2022-03-12 阅读 164

大家好,我是王老狮,索引我们用的非常多,也经常遇到明明已经使用了索引条件为什么没有命中索引,有时候索引为什么会不生效?那么我们今天就来深入的了解一下,索引的具体查找流程和选择机制。

一、索引的作用

首先我们再来回顾下,其实一个索引就是一个 B+树,索引让我们的查询可以快速定位和扫描到我们需要的数据记录上,加快查询的速度。但是需要注意的是一个 select 查询语句在执行过程中一般最多能使用一个二级索引,即 使在 where 条件中用了多个二级索引。

那么我们根据索引的作用和特性,来看下以下几个场景索引是如何判定的。

二、索引在不同查询场景中的使用

我们先定义一张表结构,里面大概有1w多条数据,接下来我们就看看在不同的场景中,索引是如何命中以及选择的吧。

CREATE TABLE `order_exp`  (
  `id` bigint(22) NOT NULL AUTO_INCREMENT COMMENT '订单的主键',
  `order_no` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '订单的编号',
  `order_note` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '订单的说明',
  `insert_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '插入订单的时间',
  `expire_duration` bigint(22) NOT NULL COMMENT '订单的过期时长,单位秒',
  `expire_time` datetime(0) NOT NULL COMMENT '订单的过期时间',
  `order_status` smallint(6) NOT NULL DEFAULT 0 COMMENT '订单的状态,0:未支付;1:已支付;-1:已过期,关闭',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `u_idx_day_status`(`insert_time`, `order_status`, `expire_time`) USING BTREE,
  INDEX `idx_order_no`(`order_no`) USING BTREE,
  INDEX `idx_expire_time`(`expire_time`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 10819 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

2.1.扫描区间

对于某个查询来说,最简单粗暴的执行方案就是扫描表中的所有记录,判断 每一条记录是否符合搜索条件。如果符合,就将其发送到客户端,否则就跳过该 记录。这就是全表扫描

对于使用 InnoDB 存储引擎的表来说,全表扫描意味着从聚簇索引第一个叶 子节点的第一条记录开始,沿着记录所在的单向链表向后扫描,直到最后一个叶 子节点的最后一条记录。虽然全表扫描是一种很笨的执行方案,但却是一种万能的执行方案,所有的查询都可以使用这种方案来执行,只是效率不高。

我们有了索引,利用 B+树查找索引列值等于某个值的记录,这样可以明显 减少需要扫描的记录数量。由于 B+树叶子节点中的记录是按照索引列值由小到 大的顺序排序的,所以即使只扫描某个区间或者某些区间中的记录也可以明显减 少需要扫描的记录数量。因此扫描区间的确定对于整个SQL的查询效率来讲是很重要的。比如下面这个查询语句:

SELECT * FROM order_exp WHERE id >= 3 AND id<= 99;

这个语句其实是想查找 id 值在[3,99]区间中的所有聚簇索引记录。我们可以 通过聚簇索引对应的 B+树快速地定位到id值为 3 的那条聚簇索引记录,然后沿着记录所在的单向链表向后扫描,直到某条聚簇索引记录的 id 值不在[3,99]区间 中为止。

与全表扫描相比,扫描 id 值在[3,99]区间中的记录已经很大程度地减少了需 要扫描的记录数量,所以提升了查询效率。其实所谓的全表扫描,我们可以理解 为扫描的区间是[负无穷,正无穷]或者[第一条记录,最后一条记录]。

再看下面这个查询语句:

SELECT * FROM order_exp WHERE id in(3,9) OR (id>=23 AND id<= 99);

这里有几个扫描区间?

三个,两个单独扫描区间[3,3] 、[9,9] ,一个范围扫描 区间[23,99]。

再看下面这个查询语句:

SELECT * FROM order_exp WHERE order_no <'DD00_10S' AND expire_time> '2021-03-22 18:28:28' AND order_note > '7 排';

这个语句里,order_no 和 expire_time 都有索引,order_note 没有索引,那会有两个扫描区间吗?并不会,请记住,一个 Select 查询语句在执行过程中一般 最多能使用一个二级索引。那么也就是说:如果用 idx_order_no 执行查询,那扫描区间就是[第一条记录,'DD00_10S'], expire_time> '2021-03-22 18:28:28' AND order_note > '7 排'只能成为普通的搜索或 者说判定条件。

如果说用 idx_expire_time 执行查询,那扫描区间就是['2021-03-22 18:28:28', 最后一条记录] ,order_no <'DD00_10S' AND order_note > '7 排'只能成为普通的搜 索或者说判定条件。

无论用哪个索引执行查询,都需要获取到索引中的记录后,进行回表,获取到完整的用户记录后再根据判定条件判断这条记录是否满足 SQL 语句的要求。

2.2.范围区间扫描

其实对于 B+树索引来说,只要索引列和常数使用=、<=>、IN、NOT IN、IS NULL、 IS NOT NULL 、> 、< 、>= 、<= 、BETWEEN 、!=  (不等于也可以写成<>)或者 LIKE操作符连接起来,就可以产生一个区间。

2.2.1.IN操作符

IN 操作符的效果和若干个等值匹配操作符`=`之间用`OR`连接起来是一样 的,也就是说会产生多个单点区间,比如下边这两个语句的效果是一样的:

SELECT * FROM order_exp WHERE insert_time IN (2021-03-22 18:23:42, yyyy);

SELECT * FROM order_exp WHERE insert_time= 2021-03-22 18:23:42 OR insert_time = yyyy;

2.2.2.!=操作符

SELECT * FROM order_exp WHERE order_no != 'DD00_9S'

此时使用 idx_expire_time 执行查询时对应的扫描区间就是[第一条记录 , 'DD00_9S']和['DD00_9S',最后一条记录]。

2.2.3 .LIKE 操作符

LIKE 操作符比较特殊,只有在匹配完整的字符串或者匹配字符串前缀时才产生合适的扫描区间。对于某个索引列来说,字符串前缀相同的记录在由记录组成的单向链表中肯 定是相邻的。比如我们有一个搜索条件是 note LIKE' b%' ,对于二级索引 idx_note 来说,所有字符串前缀为'b'的二级索引记录肯定是相邻的。这也就意味着我们只 要定位到 idx_note 值的字符串前缀为'b'的第一条记录,就可以沿着记录所在的单 向链表向后扫描,直到某条二级索引记录的字符串前缀不为a 为止。

显然,note LIKE' b%'  形成的扫描区间相当于['b', 'c')。

不过在日常的工作中,一个查询的 WHERE 子句可能有很多个小的搜索条件, 这些搜索条件需要使用AND 或者 OR 操作符连接起来,我们来看看怎么从由 AND 或 OR 组成的复杂搜索条件中提取出正确的范围区间。

2.3.所有搜索条件都可以使用某个索引的情况

有时候每个搜索条件都可以使用到某个索引,比如下边这个查询语句:

SELECT * FROM order_exp WHERE order_no > 'DD00_6S' AND order_no > 'DD00_9S';

这个查询中的搜索条件都可以使用到 idx_order_no,也就是说每个搜索条件 都对应着一个idx_order_no 的范围区间。这两个小的搜索条件使用AND 连接起 来,也就是要取两个范围区间的交集,两者交集当然就是 order_no> 'DD00_9S' 了,也就是说上边这个查询使用 idx_order_no 的范围区间就是('DD00_9S',  最后 一条记录)。

再看一下使用OR 将多个搜索条件连接在一起的情况:

SELECT * FROM order_exp WHERE order_no > 'DD00_6S' OR order_no > 'DD00_9S';

OR 意味着需要取各个范围区间的并集,所以上边这个查询使用 idx_expire_time 的范围区间就是( 'DD00_6S' ,最后一条记录)。

2.4.有的搜索条件无法使用索引的情况

我们看下下面这个查询

SELECT * FROM order_exp WHERE expire_time> '2021-03-22 18:35:09' AND order_note = 'abc';

请注意,这个查询语句中能利用的索引只有 idx_expire_time 一个,而idx_expire_time 这个二级索引的记录中又不包含 order_note 这个字段,所以在使用二级索引 idx_expire_time 定位记录的阶段用不到 order_note = 'abc'这个条件,这个条件是在回表获取了完整的用户记录后才使用的,而范围区间是为了到索引 中取记录中提出的概念,所以在确定范围区间的时候不需要考虑order_note = 'abc'这个条件。

我们把上边的查询中用不到 idx_expire_time 的搜索条件化简之后就是这样:

 SELECT * FROM order_exp WHERE expire_time> '2021-03-22 18:35:09';

也就是说最上边那个查询使用 idx_expire_time 的范围区间就是:('2021-03-22 18:35:09',最后一条记录)。

再来看一下使用 OR 的情况:

SELECT * FROM order_exp WHERE expire_time> '2021-03-22 18:35:09' OR order_note = 'abc';

这条语句在搜索时可以化简为:

SELECT * FROM order_exp ;

这也就说如果我们使用 idx_expire_time 执行查询的话,对应的范围区间就是 [第一条记录,最后一条记录] ,也就是需要将全部二级索引的记录进行回表,这个 代价肯定比直接全表扫描都大了。也就是说一个使用到索引的搜索条件和没有使 用该索引的搜索条件使用OR 连接起来后是无法使用该索引的。为什么?道理很 简单,idx_expire_time 这个二级索引的记录中不包含 order_note 这个字段,那就 说,即使二级索引 idx_expire_time 中找到了满足 expire_time> '2021-03-22             18:35:09'的记录,是无法判定 order_note  是否满足 order_note = 'abc'的,又因 为是 OR 条件,所以必须要在主键索引中从第一条记录到最后一条记录逐条判定 order_note  是否等于 'abc'。

2.5.复杂搜索条件下找出范围匹配的区间

有的查询的搜索条件可能特别复杂,比方说下边这个:

SELECT * FROM order_exp WHERE (order_no > 'DD00_9S' AND expire_time = '2021-03-22 18:35:09' ) OR (order_no < 'DD00_6S' AND order_no > 'DD00_9S') OR (order_no LIKE '%0S' AND order_no > 'DD00_12S' AND (expire_time < '2021-03-22 18:28:28' OR order_note = 'abc')) ;

分析一下:

首先查看 WHERE 子句中的搜索条件都涉及到了哪些列,哪些列可能使用到索引。

这个查询的搜索条件涉及到了 order_no、expire_time、order_note 这 3 个列, 然后 order_no 列有二级索引 idx_order_no ,expire_time  列有二级索引idx_expire_time。

对于那些可能用到的索引,分析它们的范围区间。

2.5.1.使用 idx_order_no 执行查询

我们需要把那些用不到该索引的搜索条件暂时移除掉。上边的查询中除了有 关 expire_time 和 order_note 列不能使用到 idx_order_no 索引外,order_no LIKE  '%0S'也使用不到索引。

如果条件太复杂,看着演化怕出错,我们可以把所有用不到的搜索条件视为 True 来进行中间替换,所以把这些搜索条件替换为 True 之后的样子就是这样:

(order_no > 'DD00_9S' AND TRUE ) OR

(order_no < 'DD00_6S' AND order_no > 'DD00_9S') OR

(TRUE AND order_no > 'DD00_12S' AND (TRUE OR TRUE))

再化简:

(order_no > 'DD00_9S') OR

(order_no < 'DD00_6S' AND order_no > 'DD00_9S') OR

(order_no > 'DD00_12S')

接下来替换掉永远为 TRUE 或 FALSE 的条件

因为符合 order_no < 'DD00_6S' AND order_no > 'DD00_9S'永远为 FALSE,所以 上边的搜索条件可以被写成这样:

(order_no > 'DD00_9S') OR (order_no > 'DD00_12S')

很明显,两者使用OR 操作符连接起来的,意味着要取并集,所以最终的结 果化简的到的区间就是:order_no> 'DD00_12S' 。也就是说:上边那个复杂搜索 条件的查询语句如果使用 idx_order_no  索引执行查询的话,需要把满足order_no > 'DD00_12S'的二级索引记录都取出来,然后拿着这些记录的 id 再进行 回表,得到完整的用户记录之后再使用其他的搜索条件进行过滤。记住,我们说 的是如果使用 idx_order_no  索引执行查询,不代表 MySQL 一定会使用,因为 MySQL 需要做整体评估,才能确定是否使用这个索引还是别的索引,或者是干脆全表扫描。

2.5.2.使用 idx_expire_time 执行查询

我们需要把那些用不到该索引的搜索条件暂时使用TRUE 条件替换掉,其中 有关 order_no 和 order_note 的搜索条件都需要被替换掉,替换结果就是:

(TRUE AND expire_time = '2021-03-22 18:35:09' ) OR

(TRUE AND TRUE) OR

(TRUE AND TRUE AND (expire_time < '2021-03-22 18:28:28' OR TRUE))

按照布尔运算的规则,expire_time < '2021-03-22 18:28:28' OR TRUE 的结果肯定是TRUE ,也就是说化简之后的搜索条件成这样了:

expire_time = '2021-03-22 18:35:09' OR TRUE

这个化简之后的结果就更简单了:TRUE

这个结果也就意味着如果我们要使用 idx_expire_time 索引执行查询语句的

话,需要扫描 idx_expire_time 二级索引的所有记录,然后再回表,这种情况下为 啥 MySQL 不直接全表扫描呢?所以一定不会使用 idx_expire_time 索引的。


Tips:上面这个SQL  语句,执行全表扫描的代价大概是2169,用idx_order_no 索引的代价大概是6211 ,所以,实际执行的时候,MySQL会选择全表扫描。至 于代价怎么算来的,后面我们进行详解

2.6.使用联合索引执行查询时对应的扫描区间

联合索引的索引列包含多个列,B+树每一层页面以及每个页面中的记录采用 的排序规则较为复杂,以 order_exp 表的 u_idx_day_status 联合索引为例,它采 用的排序规则如下所示:

  • 先按照 insert_time 列的值进行排序。
  • 在 insert_time 列的值相同的情况下,再按照 order_status 列的值进行排序。
  • 在 insert_time 和 order_status 列的值都相同的情况下,再按照 expire_time 列的值进行排序。

接下来,我们按照按照下面几个案例情况队索引扫描区间进行详解: 

2.6.1.查询一

Q1:SELECT * FROM order_exp WHERE insert_time = '2021-03-22 18:34:55';

由于二级索引记录是先按照 insert_time 列的值进行排序的,所以所有符合 insert_time = '2021-03-22 18:34:55'条件的记录肯定是相邻的,我们可以定位到第 一条符合 insert_time = '2021-03-22 18:34:55'条件的记录,然后沿着记录所在的单 向链表向后扫描,直到某条记录不符合 insert_time = '2021-03-22 18:34:55'条件为 止(当然,对于获取到的每一条二级索引记录都要执行回表操作)。

也就是说,如果我们使用 u_idx_day_status 索引执行查询 Q1 时,对应的扫 描区间就是['2021-03-22 18:34:55', '2021-03-22 18:34:55'],形成这个扫描区间的条 件就是 insert_time = '2021-03-22 18:34:55'。

2.6.2.查询二

Q2:SELECT * FROM order_exp WHERE insert_time = '2021-03-22 18:34:55' AND order_status = 0;

由于二级索引记录是先按照 insert_time 列的值进行排序的;在 insert_time 列的值相等的情况下,再按照 order_status 列进行排序。所以符合 insert_time = '2021-03-22 18:34:55' AND order_status = 0 条件的二级索引记录肯定是相邻的, 我们可以定位到第一条符合insert_time='2021-03-22 18:34:55' AND order_status=0 条件的记录,然后沿着记录所在的链表向后扫描,直到某条记录 不符合 insert_time='2021-03-22 18:34:55'条件或者 order_status=0 条件为止。

也就是说,如果我们使用 u_idx_day_status 索引执行查询 Q2 时,可以形成 扫描区间[('2021-03-22 18:34:55', 0), ('2021-03-22 18:34:55', 0)],形成这个扫描区间 的条件就是 insert_time = '2021-03-22 18:34:55' AND order_status = 0。

2.6.3.查询三

Q3:SELECT * FROM order_exp    WHERE insert_time = '2021-03-22 18:34:55' AND    order_status = 0 AND expire_time = '2021-03-22 18:35: 13';

由于二级索引记录是先按照 insert_time 列的值进行排序的;在 insert_time  列的值相等的情况下,再按照 order_status 列进行排序;在 insert_time 和     order_status 列的值都相等的情况下,再按照 expire_time 列进行排序。所以符合 insert_time = '2021-03-22 18:34:55' AND order_status = 0 AND expire_time =  '2021-03-22 18:35: 13'条件的二级索引记录肯定是相邻的,我们可以定位到第一条 符合 insert_time='2021-03-22 18:34:55' AND order_status=0 AND  expire_time='2021-03-22 18:35: 13'条件的记录,然后沿着记录所在的链表向后扫 描,直到某条记录不符合insert_time='2021-03-22 18:34:55'条件或者order_status=0 条件或者 expire_time='2021-03-22 18:35: 13'条件为止。如果我们使 用 u_idx_day_status 索引执行查询 Q3 时,可以形成扫描区间[('2021-03-2218:34:55', 0, '2021-03-22 18:35:13'), ('2021-03-22 18:34:55', 0, '2021-03-22 18:35: 13')] ,形成这个扫描区间的条件就是 insert_time = '2021-03-22 18:34:55'      AND order_status = 0 AND expire_time = '2021-03-22 18:35:13'。

2.6.4.查询四

Q4:SELECT * FROM order_exp WHERE insert_time < '2021-03-22 18:34:55';

由于二级索引记录是先按照 insert_time 列的值进行排序的,所以所有符合 insert_time < '2021-03-22 18:34:55'条件的记录肯定是相邻的,我们可以定位到第 一条符合 insert_time < '2021-03-22 18:34:55'条件的记录(其实就是 u_idx_day_status 索引第一个叶子节点的第一条记录) ,然后沿着记录所在的链表 向前扫描,直到某条记录不符合 insert_time < '2021-03-22 18:34:55'为止。

也就是说,如果我们使用 u_idx_day_status 索引执行查询 Q4 时,可以形成 扫描区间(第一条记录, '2021-03-22 18:34:55') ,形成这个扫描区间的条件就是  insert_time < '2021-03-22 18:34:55'。

2.6.5.查询五

Q5:SELECT * FROM order_exp       WHERE insert_time = '2021-03-22 18:34:55' AND order_status > =0 ;

由于二级索引记录是先按照 insert_time 列的值进行排序的;在 insert_time 列的值相等的情况下,再按照 order_status 列进行排序。也就是说在符合insert_time = '2021-03-22 18:34:55'条件的二级索引记录中,是按照 order_status   列的值进行排序的,那么此时符合 insert_time = '2021-03-22 18:34:55' AND order_status > =0 ;条件的二级索引记录肯定是相邻的。我们可以定位到第一条符 合 insert_time = '2021-03-22 18:34:55' AND order_status > =0 ;条件的记录,然后沿 着记录所在的链表向后扫描,直到某条记录不符合 insert_time='2021-03-22 18:34:55'条件或者 order_status > =0 条件为止。

也就是说,如果我们使用 u_idx_day_status 索引执行查询 Q5 时,可以形成 扫描区间,条件就是 insert_time = '2021-03-22 18:34:55' AND order_status > =0 ;。

2.6.6.查询六

Q6:SELECT * FROM order_exp    WHERE order_status = 1;

由于二级索引记录不是直接按照 order_status 列的值排序的,所以符合  order_status = 1 的二级索引记录可能并不相邻,也就意味着我们不能通过这个order_status= 1 搜索条件来减少需要扫描的记录数量。在这种情况下,我们是不 会使用 u_idx_day_status 索引执行查询的。

2.6.7.查询七

Q7:SELECT * FROM order_exp WHERE insert_time = '2021-03-22 18:34:55' AND expire_time = '2021-03-22 18:35: 12';

由于二级索引记录是先按照 insert_time 列的值进行排序的,所以符合     insert_time = '2021-03-22 18:34:55'条件的二级索引记录肯定是相邻的,但是对于 符合 insert_time = '2021-03-2218:34:55'条件的二级索引记录来说,并不是直接按 照 expire_time 列进行排序的,也就是说我们不能根据搜索条件 expire_time =      '2021-03-22 18:35: 12'来进一步减少需要扫描的记录数量。那么如果我们使用   u_idx_day_status 索引执行查询的话,可以定位到第一条符合insert_time='2021-03-22 18:34:55'条件的记录,然后沿着记录所在的单向链表向后 扫描,直到某条记录不符合 insert_time = '2021-03-22 18:34:55'条件为止。所以在 使用 u_idx_day_status 索引执行查询 Q7 的过程中,对应的扫描区间其实是    ['2021-03-22 18:34:55', '2021-03-22 18:34:55'] ,形成该扫描区间的搜索条件是   insert_time = '2021-03-22 18:34:55' ,与 expire_time = '2021-03-22 18:35: 12'无关。

2.6.8.查询八

Q8:SELECT * FROM order_exp WHERE insert_time < '2021-03-22 18:34:57' AND order_status = 1;

由于二级索引记录是先按照 insert_time 列的值进行排序的,所以符合 insert_time < '2021-03-22 18:34:57'条件的二级索引记录肯定是相邻的,但是对于 符合 insert_time < '2021-03-2218:34:57'条件的二级索引记录来说,并不是直接按 照 order_status 列进行排序的,也就是说我们不能根据搜索条件 order_status = 0 来进一步减少需要扫描的记录数量。那么如果我们使用 u_idx_day_status 索引执 行查询的话,可以定位到第一条符合 insert_time 的记录,其实就是u_idx_day_status 索引第一个叶子节点的第一条记录,所以在使用u_idx_day_status 索引执行查询 Q8 的过程中,对应的扫描区间其实是[第一条记 录,    '2021-03-22 18:34:57')。

举报

相关推荐

0 条评论