探索MySQL not in到底走索引吗?

一、创建模拟数据

因为创建模拟数据需要用到存储过程,上篇文章已经简单介绍了一下,这里就不重复展开了。

创建表:

CREATE TABLE test (
id INT NOT NULL AUTO_INCREMENT,
second_key INT,
text VARCHAR(20),
field_4 VARCHAR(20),
status VARCHAR(10),
create_date date,
PRIMARY KEY (id),
KEY idx_second_key (second_key)
) Engine=InnoDB CHARSET=utf8;

插入100万条数据

call test_insert(1000000);

部分数据如下所示:

mysql> select * from test.test limit 10;
+----+------------+------+------------+--------+-------------+
| id | second_key | text | field_4    | status | create_date |
+----+------------+------+------------+--------+-------------+
|  1 |          0 | t0   | 367a170042 | good   | 1974-04-02  |
|  2 |         10 | t1   | 14fcc361da | good   | 1981-02-06  |
|  3 |         20 | t2   | ad27ff39dd | good   | 1987-12-14  |
|  4 |         30 | t3   | cc25aba017 | good   | 1994-10-20  |
|  5 |         40 | t4   | ce6e4bacb1 | good   | 1974-04-10  |
|  6 |         50 | t5   | b0eb6d3801 | good   | 1981-02-13  |
|  7 |         60 | t6   | bb005167b1 | good   | 1987-12-21  |
|  8 |         70 | t7   | 37ea9bb71f | good   | 1994-10-27  |
|  9 |         80 | t8   | 300393e7e5 | good   | 1974-04-17  |
| 10 |         90 | t9   | 89e861ceb6 | good   | 1981-02-21  |
+----+------------+------+------------+--------+-------------+
10 rows in set (0.00 sec)

二、explain详解

运行explain select * from test\G 命令我们得到如下内容

mysql> explain select * from test\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: test
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 996473
     filtered: 100.00
        Extra: NULL
1 row in set, 1 warning (0.00 sec)

我们需要重点关注这几个字段

  • type
  • rows
  • Extra

下面我们来逐一讲解下:
type表示MySQL在执行当前语句时候执行的类型,有这几个值system,const,eq_ref,ref,fulltext,ref_or_null,index_merge,unique_subquery,index_subquery,range,index,all。

结果值从好到坏依次是:
system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL ,一般来说,得保证查询至少达到range级别,最好能达到ref。

  1. system 比较少见,当引擎是 MyISAM 或者 Memory 的时候并且只有一条记录,就是 system,表示可以系统级别的精准访问,这个不常见可以忽略。
  2. const 查询命中的是主键或者唯一二级索引等值匹配的时候。比如 where id = 1
mysql> explain select * from test where id=1\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: test
   partitions: NULL
         type: const
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 4
          ref: const
         rows: 1
     filtered: 100.00
        Extra: NULL
1 row in set, 1 warning (0.00 sec)
PS:const 当确定最多只会有一行匹配的时候,MySQL优化器会在查询前读取它而且只读取一次,因此非常快。当主键放入where子句时,mysql把这个查询转为一个常量(高效)
  1. eq_ref 连表时候可以使用主键或者唯一索引进行等值匹配的时候。
mysql> explain select a.second_key from test a left join test1 b on a.id=b.id where a.second_key = 10\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: a
   partitions: NULL
         type: ref
possible_keys: idx_second_key
          key: idx_second_key
      key_len: 5
          ref: const
         rows: 1
     filtered: 100.00
        Extra: Using index
*************************** 2. row ***************************
           id: 1
  select_type: SIMPLE
        table: b
   partitions: NULL
         type: eq_ref
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 4
          ref: test.a.id
         rows: 1
     filtered: 100.00
        Extra: Using index
2 rows in set, 1 warning (0.00 sec)
  1. ref 和 ref_or_null, 当非唯一索引和常量进行等值匹配的时候。只是 ref_or_null 表示查询条件是 where second_key is null
  2. fulltext, index_merge不常见跳过。
  3. unique_subquery 和 index_subquery 表示联合语句使用 in 语句的时候命中了唯一索引或者普通索引的等值查询。
  4. range 表示使用索引的范围查询,比如 where second_key > 10 and second_key < 90
mysql> explain select second_key from test  where second_key >10 and second_key < 90\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: test
   partitions: NULL
         type: range
possible_keys: idx_second_key
          key: idx_second_key
      key_len: 5
          ref: NULL
         rows: 7
     filtered: 100.00
        Extra: Using where; Using index
1 row in set, 1 warning (0.00 sec)
  1. index 我们命中了索引,但是需要全部扫描索引。
mysql> explain select second_key from test\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: test
   partitions: NULL
         type: index
possible_keys: NULL
          key: idx_second_key
      key_len: 5
          ref: NULL
         rows: 996473
     filtered: 100.00
        Extra: Using index
1 row in set, 1 warning (0.00 sec)
  1. All,这个太直观了,就是说没有使用索引,走的是全表扫描。

接下来说一下 rows,MySQL 在执行语句的时候,评估预计扫描的行数。
最后就是关键的内容 Extra,别看他是扩展。但是它很重要,因为他更好的辅助你定位 MySQL 到底如何执行的这个语句。我们选择一些重点说一说。

  1. Using index,当我们查询条件和返回内容都存在索引里面,就可以走覆盖索引,不需要回表,比如 select second_key from test where second_key = 10
  2. Using index condition,经典的索引下推,虽然命中了索引,但是并不是严格匹配,需要使用索引进行扫描对比,最后再进行回表,比如 explain select * from test where second_key > 9000000 and second_key like ‘%0’\G
mysql> explain select * from test where second_key > 9000000  and second_key like '%0'\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: test
   partitions: NULL
         type: range
possible_keys: idx_second_key
          key: idx_second_key
      key_len: 5
          ref: NULL
         rows: 202716
     filtered: 100.00
        Extra: Using index condition
1 row in set, 1 warning (0.00 sec)
  1. Using where,当我们使用全表扫描时,并且 Where 中有引发全表扫描的条件时,会命中。比如 select * from test where text = ‘t’
  2. Using filesort,查询没有命中任何索引,需要在内存或者硬盘中排序的,比如 select * from test where text = ‘t’ order by text desc limit 10
mysql> explain select * from test where text = 't' order by text desc\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: test
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 996473
     filtered: 10.00
        Extra: Using where
1 row in set, 1 warning (0.00 sec)

你也可以发现,无论是 type 还是 Extra,他们都是从前往后性能越来越差的,所以我们在优化 SQL 的时候,要尽量往前面的优化。好了到这里我们就简单介绍了完了关键词了,但是到我们可以分析 not in 是否命中索引还差点内容。我们需要了解一下 MySQL 的索引原理。下面是一个 B+ Tree 的索引图,也是 MySQL 索引的原理。

三、索引原理

MySQL 每一个索引都会构建一棵树,我们也要做能做心中有“树”。那么我心中的两棵树是这个样子。

  1. 第一棵树是主键索引,每一个 Page 就是 B+树中最重要的概念——页,这里我们也叫它节点。非叶子节点不存储数据,只存储指向子节点的指针,叶子节点存储主键和其他所有列值。其中每个节点通过双向指针链接左右节点组成了双向链表,页内部每个块可以理解为一条记录,页内多条记录通过单向指针链接,组成单链表,所有的页和页内的记录都是根据主键从左到右递增的。
  2. 第二棵树是二级索引,非叶子节点不存储数据,只存储指向子节点的指针,叶子节点存储二级索引和主键,所有的页和页内的记录都是根据二级索引从左到右递增的,这些是和主键索引最大的不同,其余的一样。
    探索MySQL not in到底走索引吗?_第1张图片
    探索MySQL not in到底走索引吗?_第2张图片

那么我们开始分析一下索引的查询原理

select * from test where second_key = 40;

这条语句的查询流程是:

  1. 因为 second_key 有索引,所以走的是 idx_second_key 二级索引生成的树。
  2. 通过检查 Page 1 发现我们需要查询的记录在 Page 12 所属的叶子节点内。
  3. 通过查询 Page 12 发现我们需要查询的记录在 Page 27 节点内。
  4. 从 Page 27 的节点内从左向右遍历,得到 40 节点。
  5. 获取到 40 节点里面存储的主键 ID 4。
  6. 因为二级索引里面没有数据,所以需要回表,回表的时候重新通过 ID 4 查找 primary_key 主键索引树。
  7. 依照刚才的顺序,最终找到内容在 Page 27 里面的节点,返回。

同时我们运行一下 explain 验证一下,type 是 ref,走的是非唯一索引的等值匹配。

mysql> explain select * from test where second_key = 40 \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: test
   partitions: NULL
         type: ref
possible_keys: idx_second_key
          key: idx_second_key
      key_len: 5
          ref: const
         rows: 1
     filtered: 100.00
        Extra: NULL
1 row in set, 1 warning (0.00 sec)

上面是一个非常简单的查询,那么我们看一下稍微复杂的。

select * from test where second_key > 10 and second_key < 50;

这条语句的查询流程是:

  1. 因为 second_key 有索引,所以走的是 idx_second_key 二级索引生成的树。
  2. 因为索引是从左到右递增的,所以我们先找 second_key > 10,通过前面的讲解,我们会定位到 Page 23 的第 2 个节点。
  3. 因为叶子节点是双向链表,所以我们不需要重新从根节点找其他内容,我们直接从左向右遍历比较,直到内容 >= 50 停止,这样我们会定位到 Page 16 的第 1 个节点停止。
  4. 那么我们拿到的结果就是 Page 23 和 Page 27 的 20,30,40 节点。
  5. 然后回表,分别找到 20,30,40 对应的主键 2,3,4 的内容,返回数据。

我们继续运行一下 explain,type 是 range 表示使用索引的范围查询, Extra 里面有了内容。Using index condition 表示 range 查询的时候使用了索引进行比较以后才进行的回表。

mysql> explain select * from test where second_key > 10 and second_key < 50 \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: test
   partitions: NULL
         type: range
possible_keys: idx_second_key
          key: idx_second_key
      key_len: 5
          ref: NULL
         rows: 3
     filtered: 100.00
        Extra: Using index condition
1 row in set, 1 warning (0.00 sec)

四、not in 原理

好的,那么进入了本文的高潮阶段,下面的语句走不走索引你知道吗?

select * from test where second_key not in(10,30,50);
mysql> explain select * from test where second_key not in(10,30,50)\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: test
   partitions: NULL
         type: ALL
possible_keys: idx_second_key
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 996473
     filtered: 50.00
        Extra: Using where
1 row in set, 1 warning (0.00 sec)

可以看出来,是没有走索引的,我们换个语句试试呢

select second_key from test where second_key not in(10,30,50);

再运行一次试试,这一次就走了索引了。

mysql> explain select second_key from test where second_key not in(10,30,50)\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: test
   partitions: NULL
         type: range
possible_keys: idx_second_key
          key: idx_second_key
      key_len: 5
          ref: NULL
         rows: 498239
     filtered: 100.00
        Extra: Using where; Using index
1 row in set, 1 warning (0.00 sec)

那么为什么第一次没有走索引呢?
MySQL 会在选择索引的时候进行优化,如果 MySQL 认为全表扫描比走索引+回表效率高, 那么他会选择全表扫描。回到我们这个例子,全表扫描 rows 是 996473,不需要回表;但是如果走索引的话,不仅仅需要扫描 498239 次,还需要回表 498239 次,那么 MySQL 认为反复的回表的性能消耗还不如直接全表扫描呢,所以 MySQL 默认的优化导致直接走的全表扫描。

那么我就是想 select * 还走索引怎么办呢?

第一种方式:

select * from test where second_key not in(10,30,50) limit 5;

执行explain如下:

mysql> explain select * from test  where second_key not in(10,30,50) limit 5\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: test
   partitions: NULL
         type: range
possible_keys: idx_second_key
          key: idx_second_key
      key_len: 5
          ref: NULL
         rows: 498239
     filtered: 100.00
        Extra: Using index condition
1 row in set, 1 warning (0.00 sec)

因为 limit 的增加,让 MySQL 优化的时候发现,索引 + 回表的性能更高一些。所以 not in 只要使用合理,一定会是走索引的,并且真实环境中,我们的记录很多的,MySQL一般不会评估出 ALL 性能更高。

第二种方式(强制使用索引,force index ):

 select * from test force index(idx_second_key) where second_key not in(10,30,50);

执行explain如下:

mysql> explain select * from test force index(idx_second_key) where second_key not in(10,30,50)\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: test
   partitions: NULL
         type: range
possible_keys: idx_second_key
          key: idx_second_key
      key_len: 5
          ref: NULL
         rows: 498239
     filtered: 100.00
        Extra: Using index condition
1 row in set, 1 warning (0.00 sec)

那么最后还是说一下 not in 走索引的原理吧,这样你就可以更放心大胆的用 not in 了

select * from test where second_key not in(10,30,50) limit 5;

这个语句在真正执行的时候其实被拆解了

select * from test where 
(second_key < 10) 
or 
(second_key > 10 and second_key < 30) 
or 
(second_key > 30 and second_key < 50) 
or 
(second_key > 50);

这个语句分解完成以后就相当于,4 个开区间,分别的寻找一次开始节点,然后依照索引查找就可以了。

五、总结

MySQL 会在选择索引的时候进行优化,如果 MySQL 认为全表扫描比走索引+回表效率高, 那么他会选择全表扫描,如果认为走索引的效率高,那么肯定也是会走索引的。

你可能感兴趣的:(mysql,mysql,数据库,database)