跳转至

MySQL之Explain详解


创建表

CREATE TABLE `table1` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(32) NOT NULL,
  `age` int(11) NOT NULL,
  `gender` bit(1) DEFAULT b'1',
  `birthday` datetime DEFAULT NULL,
  `location` varchar(64) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_name_age_gender` (`name`,`age`,`gender`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

插入数据

INSERT INTO db1.table1(name, age, gender, birthday, location) VALUES
('aa', 20, b'1', '2001-1-1', 'beijing'),
('ab', 21, b'1', '2001-1-2', 'beijing'),
('ac', 22, b'1', '2001-1-3', 'beijing'),
('aa', 10, b'0', '2011-1-1', 'beijing'),
('ba', 30, b'1', '1991-1-1', 'tianjin'),
('ba', 30, b'1', '1991-12-12', 'shanghai'),
('ba', 40, b'0', '1981-6-6', 'chongqing'),
('ca', 20, b'1', '2001-1-1', 'shandong'),
('cb', 20, b'0', '2021-1-1', 'henan'),
('cd', 20, b'1', '2001-1-1', 'shanxi'),
('ce', 20, b'1', '2001-1-1', 'hebei'),
('de', 20, b'0', '1981-1-1', 'guangdong'),
('db', 20, b'0', '2001-1-1', 'taiwan');

测试

测试1 -- 全值匹配

explain select * from table1 where name='x';
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE table1 ref idx_name_age_gender idx_name_age_gender 130 const 1 Using index condition

name是varchar, 长度32, 4*32+2=130, 使用了索引 idx_name_age_gender 中的 name 字段


explain select * from table1 where name='x' and age=18;
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE table1 ref idx_name_age_gender idx_name_age_gender 134 const,const 1 Using index condition

同上, 同时 int 是 4 个字节, 所以130+4=134, 使用了索引 idx_name_age_gender 中的 name 和 age 字段


explain select * from table1 where name='x' and age=18 and gender=b'0';
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE table1 ref idx_name_age_gender idx_name_age_gender 136 const,const,const 1 Using index condition

同上, bit 是 2 个字节, 所以 134+2=136, 使用了索引 idx_name_age_gender 中的 name、 age 和 gender 字段

**注意: **

1、不同版本的MySQL计算key_lan的公式可能不一样, 不同编码的极端公式也可能不一样, 本例中用的是用的编码是utf8mb4(4个字节), 所以是计算公式是 4n*2, 如果用utf8编码就是3n+2

2、上面3个输出中 rows 都是1, 是因为当前表中数据较少, 当数据量很大时, 上方三个输出中的 rows 会依次递减, 即使用联合索引时尽量全值(字段)匹配, 这样效率高


测试2 -- 允许为空

ALTER TABLE db1.table1 MODIFY COLUMN age int(11) NULL;
explain select * from table1 where name='x' and age=18 and gender=b'0';
ALTER TABLE db1.table1 MODIFY COLUMN age int(11) NOT NULL;
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE table1 ref idx_name_age_gender idx_name_age_gender 137 const,const,const 1 Using index condition

当设置 age 允许为空时, key_lan 变成 137, 多出来的一个字节就是用来记录 age 是否为空的


测试3 -- 字符串不加引号

explain select * from table1 where name=0;
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE table1 ALL idx_name_age_gender 13 Using where

name 本身类型是 varchar, where 条件中使用时不添加引号, 就不会走索引

测试4 -- 顺序不一致

explain select * from table1 where age='xx' and name='x';
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE table1 ref idx_name_age_gender idx_name_age_gender 134 const,const 1 Using index condition
explain select * from table1 where age='xx' and name='x' and gender=b'0';
explain select * from table1 where gender=b'0' and age='xx' and name='x';
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE table1 ref idx_name_age_gender idx_name_age_gender 136 const,const,const 1 Using index condition

MySQL内部会优化where条件后面的语句, 使其符合最左前缀原则, 从而可以使用索引

测试5 -- 跳过索引字段

explain select * from table1 where name='xx' and gender=b'0';
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE table1 ref idx_name_age_gender idx_name_age_gender 130 const 1 Using index condition

使用 name 后跳过了 age 直接使用 gender, 未被最左前缀原则, 所以 key_len=130, 即只使用了索引中的 name 字段


explain select * from table1 where age=18 and gender=b'0';
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE table1 ALL 13 Using where

由于跳过了 name, 所以完全没有使用索引

测试5 -- 对索引做其他操作

explain select * from table1 where left(name, 4)='xx' ;
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE table1 ALL 13 Using where

由于对索引使用了函数, 所有没有使用索引


CREATE INDEX idx_birthday USING BTREE ON table1 (birthday);
explain select * from table1 where date(birthday)='2020-01-01';
explain select * from table1 where birthday>='2020-01-01 00:00:00' and birthday<='2020-01-01 23:59:59';
ALTER TABLE db1.table1 DROP INDEX idx_birthday;
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE table1 ALL 13 Using where
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE table1 range idx_birthday idx_birthday 6 1 Using index condition

由于对索引使用了函数, 所有没有使用索引; 但转换成比大小, 就使用了索引, 这就是索引优化

对索引进行计算、函数、手动或自动的类型转换都可能导致无法使用索引

测试5 -- !=、<>、in、not、is null、is not null 条件

explain select * from table1 where name != 'aa';
explain select * from table1 where name <> 'aa';
explain select * from table1 where name not in ('aa', 'ab');
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE table1 ALL idx_name_age_gender 13 Using where
explain select * from table1 where birthday  is null;
explain select * from table1 where birthday is not null;
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE table1 ALL 13 Using where
  • 一般情况下这些都走不了索引
  • 有时候不等于也能走索引
  • in、or 有时候会走索引, 有时候不会走索引, MySQL内部优化器会根据检索比例、表大小等因素判断是否使用索引

其他:

  • 使用 like 通配符, 只要第一个字符不是通配符就能走索引, 例如:

  • 'a%', 可以使用索引

  • '%a', 无法使用所有
  • '%a%', 无法使用所有
  • 'a%b', 可以使用索引
  • 无论表里有多少条数据, like ‘a%’ 都会走所有, in 或者 or 在表大的时候走索引, 表小的时候不走索引

  • name > 'a' and age > 20 不会走索引 (个人猜测使用 > 导致结果集范围比较大, 所以默认不使用索引)

  • name like 'a%' and age > 20 就会走索引
  • 之所以这样, 是因为从MySQL5.6版本开始引入了索引下推原则(适用于like查询): 在遍历索引的过程中优先判断索引包含的字段, 过滤掉不符合要求的记录后再回表, 可减少回表次数, 提高查询效率
  • 简单说就是 like 完之后继续匹配 age 使范围缩小, 而联合索引中第一个字段使用 > 不一定会使用索引(估计和结果集大小有关, like相对来说比较小), 也不会使用索引下推
  • 索引下推会减少回表次数, 仅限于使用innodb的二级索引, 因为主键索引是聚簇索引, 叶子节点包含所有数据, 索引下推不会起到减少查询全行数据的效果
  • 对索引使用范围条件是, 根据条件的大小, 可能会有不同的结果, 例如:

  • where 100 < speed < 100000000 就有可能不走索引

  • where 100 < speed < 200 就有可能走索引

这种情况就好比, 表中的数据量少, 如果使用非主键索引, 还要回表, 不如直接全表扫描来的快