因为创建模拟数据需要用到存储过程,上篇文章已经简单介绍了一下,这里就不重复展开了。
创建表:
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 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表示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。
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把这个查询转为一个常量(高效)
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)
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)
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)
接下来说一下 rows,MySQL 在执行语句的时候,评估预计扫描的行数。
最后就是关键的内容 Extra,别看他是扩展。但是它很重要,因为他更好的辅助你定位 MySQL 到底如何执行的这个语句。我们选择一些重点说一说。
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)
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 每一个索引都会构建一棵树,我们也要做能做心中有“树”。那么我心中的两棵树是这个样子。
那么我们开始分析一下索引的查询原理
select * from test where second_key = 40;
这条语句的查询流程是:
同时我们运行一下 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;
这条语句的查询流程是:
我们继续运行一下 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)
好的,那么进入了本文的高潮阶段,下面的语句走不走索引你知道吗?
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 认为全表扫描比走索引+回表效率高, 那么他会选择全表扫描,如果认为走索引的效率高,那么肯定也是会走索引的。