[关闭]
@adamhand 2019-03-08T09:24:10.000000Z 字数 12638 阅读 997

05 | 深入浅出索引(下)


首先来看一个问题:

在下面这个表 T 中,如果执行 select * from T where k between 3 and 5,需要执行几次树的搜索操作,会扫描多少行?

  1. mysql> create table T (
  2. ID int primary key,
  3. k int NOT NULL DEFAULT 0,
  4. s varchar(16) NOT NULL DEFAULT '',
  5. index k(k))
  6. engine=InnoDB;
  7. insert into T values(100,1, 'aa'),(200,2,'bb'),(300,3,'cc'),(500,5,'ee'),(600,6,'ff'),(700,7,'gg');

这个表的InnoDB存储结构如下图所示:



下面是这条SQL语句的执行流程:

在这个过程中,回到主键索引树搜索的过程,我们称为回表。可以看到,这个查询过程读了 k 索引树的 3 条记录(步骤 1、3 和 5),回表了两次(步骤 2 和 4)。

在这个例子中,由于查询结果所需要的数据只在主键索引上有,所以不得不回表。那么,有没有可能经过索引优化,避免回表过程呢?

覆盖索引

如果执行的语句是 select ID from T where k between 3 and 5,这时只需要查 ID 的值,而 ID 的值已经在 k 索引树上了,因此可以直接提供查询结果,不需要回表。也就是说,在这个查询里面,索引 k 已经“覆盖了”我们的查询需求,我们称为覆盖索引。也就是说,索引已经包含所有需要查询的字段的值,只读取索引就能够查询到需要的值

由于覆盖索引可以减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段。

联合索引

联合索引有叫多列索引,是使用多个列作为条件进行查询。

先来看一个问题,假如有一个市民表定义如下:

  1. CREATE TABLE `tuser` (
  2. `id` int(11) NOT NULL,
  3. `id_card` varchar(32) DEFAULT NULL,
  4. `name` varchar(32) DEFAULT NULL,
  5. `age` int(11) DEFAULT NULL,
  6. `ismale` tinyint(1) DEFAULT NULL,
  7. PRIMARY KEY (`id`),
  8. KEY `id_card` (`id_card`),
  9. KEY `name_age` (`name`,`age`)
  10. ) ENGINE=InnoDB;

在上表中,是否有必要将身份证号和名字建立联合索引?

众所周知,身份证号是市民的唯一标识。也就是说,如果有根据身份证号查询市民信息的需求,只要在身份证号字段上建立索引就够了,没有必要使用(身份证号,姓名)两列联合查询。那么再建立一个(身份证号、姓名)的联合索引,是不是浪费空间?

如果现在有一个高频请求,要根据市民的身份证号查询他的姓名,这个联合索引就有意义了。它可以在这个高频请求上用到覆盖索引,不再需要回表查整行记录,减少语句的执行时间。

所以,可以看到在以下两种情况下可以使用联合索引:

最左前缀原则

在使用联合索引时,需要满足最左前缀原则。最左前缀:查询条件必须包含联合索引最左边的列。比如联合索引为(name,age,gender),查询条件必须包含name才行,但是name和查询条件中的其他条件字段顺序可以任意。

下面,使用(name,age)这个联合索引来分析。这个索引的示意图如下:



可以看到,索引项是按照索引定义里面出现的字段顺序排序的。即先按照name进行排序,再按照age进行排序。

最左前缀可以是联合索引的最左 N 个字段,也可以是字符串索引的最左 M 个字符。

索引列的顺序

在建立联合索引的时候,如何安排索引内的字段顺序。有两个原则。

比如,有ab两列,现在需要建立a的索引和a、b两列的联合索引,不需要b列的索引,那建立(a,b)联合查询就会比(b,a)好,因为前者建立之后就不再需要单独建立a的索引。

索引的选择性是指:不重复的索引值和记录总数的比值。最大值为 1,此时每个记录都有唯一的索引与其对应。选择性越高,查询效率也越高。

例如下面显示的结果中 customer_id 的选择性比 staff_id 更高,因此最好把 customer_id 列放在多列索引的前面。

  1. SELECT COUNT(DISTINCT staff_id)/COUNT(*) AS staff_id_selectivity,
  2. COUNT(DISTINCT customer_id)/COUNT(*) AS customer_id_selectivity,
  3. COUNT(*)
  4. FROM payment;
  1. staff_id_selectivity: 0.0001
  2. customer_id_selectivity: 0.0373
  3. COUNT(*): 16049

索引下推

索引下推主要解决的是不符合最左前缀的查询。

在上面的表中,有一个需求:检索出表中“名字第一个字是张,而且年龄是 10 岁的所有男孩”。那么,SQL 语句是这么写的:

  1. mysql> select * from tuser where name like '张 %' and age=10 and ismale=1;

因为存在like语句,所以不能使用联合索引。所以这个语句在搜索索引树的时候,只能用 “张”,找到第一个满足条件的记录 ID3。

接下来,如果在 MySQL 5.6 之前,只能从 ID3 开始一个个回表。到主键索引上找出数据行,再对比字段值。

MySQL 5.6 引入的索引下推优化index condition pushdown), 可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。

两种情况的执行过程分别如下图所示:



不使用索引下推



使用索引下推

上述图中虚线表示回表。可以看到,不使用索引下推时,InnoDB在(name,age)索引中看不到age的值,需要回表4次。而使用索引下推,InnoDB 在 (name,age) 索引内部就判断了 age 是否等于 10,对于不等于 10 的记录,直接判断并跳过。只需要回表2次。

默认启动。可以通过optimizer_switch系统变量去控制它是否开启:

  1. SET optimizer_switch = 'index_condition_pushdown=off';
  2. SET optimizer_switch = 'index_condition_pushdown=on';

补丁

最左前缀原则

最左前缀原则需要满足的条件:

比如联合索引(a,b,c),满足最左前缀可以使用联合索引的情况有:

需要注意的是,最左前缀原则是只有用到了最左边的那个索引就会执行,与顺序无关,所以形如c and b and a下面的也会使用联合索引。

不符合的情况有:

下面用一个具体的例子来验证,使用mqsql8.0

首先建表:

  1. mysql> create table T(
  2. id int(20) primary key not null auto_increment,
  3. name varchar(255) default null,
  4. age int(20) default null,
  5. gender varchar(255) default null,
  6. address varchar(255) default null,
  7. KEY `unique_index` (`name`,`age`,`gender`))ENGINE=InnoDB;

上面的表中对name,age,gender字段加上了联合索引。

然后插入几条数据:

  1. insert into T values(1, '张三',20,'male','beijing');
  2. insert into T values(2, '王五',21,'feale','shanghai');
  3. insert into T values(3, '张三',23,'male','shandong');
  4. insert into T values(4, '张无',24,'male','chognqing');
  5. insert into T values(5, '李九',20,'male','guangzhou');

查询条件为name

  1. explain select * from t where name='张三'\G;

结果为:

  1. *************************** 1. row ***************************
  2. id: 1
  3. select_type: SIMPLE
  4. table: t
  5. partitions: NULL
  6. type: ref
  7. possible_keys: unique_index
  8. key: unique_index
  9. key_len: 767
  10. ref: const
  11. rows: 2
  12. filtered: 100.00
  13. Extra: Using index
  14. 1 row in set, 1 warning (0.00 sec)

可以看到,key: Using index使用了联合索引。(关于explain关键字各个字段的含义,可以参考[MySQL高级](一) EXPLAIN用法和结果分析)

查询条件为age

  1. explain select * from t where age=20\G;

结果为:

  1. *************************** 1. row ***************************
  2. id: 1
  3. select_type: SIMPLE
  4. table: t
  5. partitions: NULL
  6. type: ALL
  7. possible_keys: NULL
  8. key: NULL
  9. key_len: NULL
  10. ref: NULL
  11. rows: 1
  12. filtered: 100.00
  13. Extra: Using where
  14. 1 row in set, 1 warning (0.00 sec)

可以看到,key: null没有使用索引。

查询条件为gender

  1. explain select * from t where gender='male'\G;
  1. *************************** 1. row ***************************
  2. id: 1
  3. select_type: SIMPLE
  4. table: t
  5. partitions: NULL
  6. type: ALL
  7. possible_keys: NULL
  8. key: NULL
  9. key_len: NULL
  10. ref: NULL
  11. rows: 1
  12. filtered: 100.00
  13. Extra: Using where
  14. 1 row in set, 1 warning (0.00 sec)

没有使用索引。

查询条件为name and age

  1. explain select * from t where name='张三' and age=20\G;
  1. *************************** 1. row ***************************
  2. id: 1
  3. select_type: SIMPLE
  4. table: t
  5. partitions: NULL
  6. type: ref
  7. possible_keys: unique_index
  8. key: unique_index
  9. key_len: 773
  10. ref: const,const
  11. rows: 1
  12. filtered: 100.00
  13. Extra: NULL
  14. 1 row in set, 1 warning (0.00 sec)

使用了联合索引。

查询条件为age and name

依旧使用name,但是不在最左边,也不影响使用联合索引。

  1. explain select * from t where age=10 and name='张三'\G;
  1. *************************** 1. row ***************************
  2. id: 1
  3. select_type: SIMPLE
  4. table: t
  5. partitions: NULL
  6. type: ref
  7. possible_keys: unique_index
  8. key: unique_index
  9. key_len: 773
  10. ref: const,const
  11. rows: 1
  12. filtered: 100.00
  13. Extra: NULL
  14. 1 row in set, 1 warning (0.00 sec)

查询条件为name and gender

  1. explain select * from t where name='张三' and gender='male'\G;
  1. *************************** 1. row ***************************
  2. id: 1
  3. select_type: SIMPLE
  4. table: t
  5. partitions: NULL
  6. type: ref
  7. possible_keys: unique_index
  8. key: unique_index
  9. key_len: 768
  10. ref: const
  11. rows: 1
  12. filtered: 100.00
  13. Extra: Using index condition
  14. 1 row in set, 1 warning (0.00 sec)

这两个条件分别位于联合索引位置的第一和第三,测试联合索引依旧有效

查询条件为name or age

  1. explain select * from t where name='fdf' or age=10\G;
  1. *************************** 1. row ***************************
  2. id: 1
  3. select_type: SIMPLE
  4. table: t
  5. partitions: NULL
  6. type: ALL
  7. possible_keys: unique_index
  8. key: NULL
  9. key_len: NULL
  10. ref: NULL
  11. rows: 1
  12. filtered: 100.00
  13. Extra: Using where
  14. 1 row in set, 1 warning (0.00 sec)

把 and 换成 or,联合所索引无效

查询条件为age and gender

  1. explain select * from t where age=10 and gender='male'\G;
  1. *************************** 1. row ***************************
  2. id: 1
  3. select_type: SIMPLE
  4. table: t
  5. partitions: NULL
  6. type: ALL
  7. possible_keys: NULL
  8. key: NULL
  9. key_len: NULL
  10. ref: NULL
  11. rows: 1
  12. filtered: 100.00
  13. Extra: Using where
  14. 1 row in set, 1 warning (0.00 sec)

这两个条件分别位于联合索引位置的第二和第三,发现联合索引无效

三个索引一起用

  1. explain select * from t where name='zhagnsan' and age=10 and gender='male'\G;
  1. *************************** 1. row ***************************
  2. id: 1
  3. select_type: SIMPLE
  4. table: t
  5. partitions: NULL
  6. type: ref
  7. possible_keys: unique_index
  8. key: unique_index
  9. key_len: 1541
  10. ref: const,const,const
  11. rows: 1
  12. filtered: 100.00
  13. Extra: NULL
  14. 1 row in set, 1 warning (0.00 sec)

三个索引一起用,有效

包含like的语句

  1. explain select * from t where name like '%z%' and age=10 and gender='male'\G;
  1. *************************** 1. row ***************************
  2. id: 1
  3. select_type: SIMPLE
  4. table: t
  5. partitions: NULL
  6. type: ALL
  7. possible_keys: NULL
  8. key: NULL
  9. key_len: NULL
  10. ref: NULL
  11. rows: 1
  12. filtered: 100.00
  13. Extra: Using where
  14. 1 row in set, 1 warning (0.00 sec)

包含like的语句,索引无效

语句包含<=等含有范围的关键字

  1. explain select * from t where name='zhagnsan' and age<=10 and gender='male'\G;
  1. *************************** 1. row ***************************
  2. id: 1
  3. select_type: SIMPLE
  4. table: t
  5. partitions: NULL
  6. type: range
  7. possible_keys: unique_index
  8. key: unique_index
  9. key_len: 1541
  10. ref: NULL
  11. rows: 1
  12. filtered: 100.00
  13. Extra: Using index condition
  14. 1 row in set, 1 warning (0.00 sec)

可以看到,索引有效

小补丁

前面通过实际情况分析了最左前缀规则,但是分析的还比较粗略,知识分析了前缀索引能不能用,没有具体分析select语句中的具体哪个条件可以用索引,哪个条件不可以用索引。下面就这个问题分析一下。

还是创建一个表,为了分析简单起见,创建一个只有tinyint类型的表,该类型占1个字节。

  1. mysql> create table t3(
  2. id1 tinyint not null primary key auto_increment,
  3. id2 tinyint not null,
  4. id3 tinyint not null,
  5. id4 tinyint not null,
  6. id5 tinyint not null
  7. )engine=InnoDB;
  8. insert into t3 values(1,2,3,4,5);

下面来分析一下语句中的哪个条件可以使用索引而哪个条件不可以使用。

语句1

  1. explain select * from t3 where id2=2 and id3=3 and id4=4\G;

结果如下:

  1. *************************** 1. row ***************************
  2. id: 1
  3. select_type: SIMPLE
  4. table: t3
  5. partitions: NULL
  6. type: ref
  7. possible_keys: union_index
  8. key: union_index
  9. key_len: 3
  10. ref: const,const,const
  11. rows: 2
  12. filtered: 100.00
  13. Extra: NULL
  14. 1 row in set, 1 warning (0.00 sec)

可以看到,key字段不为空,使用了索引。更进一步,key_len的值为3,表示使用的索引长度为3个字节。而tinyint类型的长度为1个字节,所以说明,id2id3id4都使用了索引。

语句2

  1. explain select * from t3 where id2=2 and id3>3 and id4=4\G;

执行结果为:

  1. *************************** 1. row ***************************
  2. id: 1
  3. select_type: SIMPLE
  4. table: t3
  5. partitions: NULL
  6. type: range
  7. possible_keys: union_index
  8. key: union_index
  9. key_len: 2
  10. ref: NULL
  11. rows: 1
  12. filtered: 50.00
  13. Extra: Using index condition
  14. 1 row in set, 1 warning (0.00 sec)

可以看到,key_len的值为2,也就是id2id3id4中只用到了两个索引。为了验证哪两个字段使用了索引,执行下列语句:

  1. explain select * from t3 where id2=2 and id3>3\G;

结果为:

  1. *************************** 1. row ***************************
  2. id: 1
  3. select_type: SIMPLE
  4. table: t3
  5. partitions: NULL
  6. type: range
  7. possible_keys: union_index
  8. key: union_index
  9. key_len: 2
  10. ref: NULL
  11. rows: 1
  12. filtered: 100.00
  13. Extra: Using index condition
  14. 1 row in set, 1 warning (0.00 sec)

可以看到,将id4去掉之后还是使用两个索引,说明id4没有用到索引,而id2id3用到了索引

语句3

  1. explain select * from t3 where id2=2 and id3=3 and id4>4\G;

执行结果为:

  1. *************************** 1. row ***************************
  2. id: 1
  3. select_type: SIMPLE
  4. table: t3
  5. partitions: NULL
  6. type: range
  7. possible_keys: union_index
  8. key: union_index
  9. key_len: 3
  10. ref: NULL
  11. rows: 1
  12. filtered: 100.00
  13. Extra: Using index condition
  14. 1 row in set, 1 warning (0.00 sec)

可以看到,三列都用了索引。

语句4

  1. explain select * from t3 where id2>2 and id3=3 and id4=4\G;

执行结果为:

  1. *************************** 1. row ***************************
  2. id: 1
  3. select_type: SIMPLE
  4. table: t3
  5. partitions: NULL
  6. type: range
  7. possible_keys: union_index
  8. key: union_index
  9. key_len: 1
  10. ref: NULL
  11. rows: 1
  12. filtered: 50.00
  13. Extra: Using index condition
  14. 1 row in set, 1 warning (0.00 sec)

可以看到,只有一列用了索引。为了验证是哪一列用了索引,执行下列语句:

  1. explain select * from t3 where id2>2\G;

结果为为:

  1. *************************** 1. row ***************************
  2. id: 1
  3. select_type: SIMPLE
  4. table: t3
  5. partitions: NULL
  6. type: range
  7. possible_keys: union_index
  8. key: union_index
  9. key_len: 1
  10. ref: NULL
  11. rows: 1
  12. filtered: 100.00
  13. Extra: Using index condition
  14. 1 row in set, 1 warning (0.00 sec)

可以看到,仍然用了一个索引,说明id3id4没用到索引,只有id2用了

语句5

  1. explain select * from t3 where id2=2 and id3=3 order by id4\G;

结果如下

  1. *************************** 1. row ***************************
  2. id: 1
  3. select_type: SIMPLE
  4. table: t3
  5. partitions: NULL
  6. type: ref
  7. possible_keys: union_index
  8. key: union_index
  9. key_len: 2
  10. ref: const,const
  11. rows: 1
  12. filtered: 100.00
  13. Extra: Using index condition
  14. 1 row in set, 1 warning (0.00 sec)

可以看到,key_ken2,可想而知,id2id3用了索引而id4没用到。

语句6

  1. explain select * from t3 where id2=2 and id4=4 order by id3\G;

结果如下,

  1. *************************** 1. row ***************************
  2. id: 1
  3. select_type: SIMPLE
  4. table: t3
  5. partitions: NULL
  6. type: ref
  7. possible_keys: union_index
  8. key: union_index
  9. key_len: 1
  10. ref: const
  11. rows: 1
  12. filtered: 50.00
  13. Extra: Using index condition
  14. 1 row in set, 1 warning (0.00 sec)

可想而知,只有id2用了索引。

语句7

  1. explain select * from t3 where id3=3 and id4=4 order by id2\G;

结果如下,

  1. *************************** 1. row ***************************
  2. id: 1
  3. select_type: SIMPLE
  4. table: t3
  5. partitions: NULL
  6. type: ALL
  7. possible_keys: NULL
  8. key: NULL
  9. key_len: NULL
  10. ref: NULL
  11. rows: 2
  12. filtered: 50.00
  13. Extra: Using where; Using filesort
  14. 1 row in set, 1 warning (0.00 sec)

可以看到,所有列都没用到索引。

语句8

  1. explain select * from t3 where id2 like 2 and id3=3 and id4=4\G;

结果如下:

  1. *************************** 1. row ***************************
  2. id: 1
  3. select_type: SIMPLE
  4. table: t3
  5. partitions: NULL
  6. type: ALL
  7. possible_keys: union_index
  8. key: NULL
  9. key_len: NULL
  10. ref: NULL
  11. rows: 2
  12. filtered: 50.00
  13. Extra: Using where
  14. 1 row in set, 1 warning (0.00 sec)

可以看到,没有用到索引。

语句9

  1. explain select * from t3 where id2=2 and id3 like 3 and id4=4\G;
  2. *************************** 1. row ***************************
  3. id: 1
  4. select_type: SIMPLE
  5. table: t3
  6. partitions: NULL
  7. type: ref
  8. possible_keys: union_index
  9. key: union_index
  10. key_len: 1
  11. ref: const
  12. rows: 2
  13. filtered: 50.00
  14. Extra: Using index condition
  15. 1 row in set, 1 warning (0.00 sec)

可以看到,只有id2用了索引。

语句10

  1. explain select * from t3 where id2=2 and id3=3 and id4 like 4\G;
  2. *************************** 1. row ***************************
  3. id: 1
  4. select_type: SIMPLE
  5. table: t3
  6. partitions: NULL
  7. type: ref
  8. possible_keys: union_index
  9. key: union_index
  10. key_len: 2
  11. ref: const,const
  12. rows: 2
  13. filtered: 50.00
  14. Extra: Using index condition
  15. 1 row in set, 1 warning (0.00 sec)

可以看到,只有id2id3用了索引。

小结:

这种情况的原因是联合索引是按照索引的顺序进行排序的,而使用范围符号和order by时就破坏了有序性,所以导致联合索引不能用。

单引号和双引号的区别

如果双引号的sql执行报错,可以执行:

  1. SET SESSION SQL_MODE=ANSI_QUOTES;

一个问题

如果将上面验证最左前缀的表中的address字段去掉,变为下面的表:

  1. create table T2(
  2. id int(20) primary key not null auto_increment,
  3. name varchar(255) default null,
  4. age int(20) default null,
  5. gender varchar(255) default null,
  6. KEY `unique_index` (`name`,`age`,`gender`))ENGINE=InnoDB;

也就是说,表中所有字段都是索引(主键id默认为聚簇索引),这时,即使查询条件不满足最左前缀规则,也可以使用联合索引了。比如执行下面的查询语句:

  1. explain select * from t2 where age=10 and gender='male'\G;

结果为:

  1. *************************** 1. row ***************************
  2. id: 1
  3. select_type: SIMPLE
  4. table: t2
  5. partitions: NULL
  6. type: index
  7. possible_keys: unique_index
  8. key: unique_index
  9. key_len: 1541
  10. ref: NULL
  11. rows: 1
  12. filtered: 100.00
  13. Extra: Using where; Using index
  14. 1 row in set, 1 warning (0.00 sec)

参考

MYSQL创建表,字段用双引号时报错
mysql的单引号和双引号的区别??(基本没有)
理解Mysql的多列索引
mysql-覆盖索引
mysql高效索引之覆盖索引
mysql覆盖索引详解
mysql联合索引详解
多个单列索引和联合索引的区别详解
[MySQL高级](一) EXPLAIN用法和结果分析

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注