MySQL Server 有一个被称为查询优化器的模块,
一条查询语句进行语法解析之后,就会被交给查询优化器来进行优化,
优化的结果是生成一个执行计划,
这个执行计划表明了应该使用哪些索引进行查询,表之间的连接顺序是什么,
最后会按照执行计划中的步骤调用存储引擎提供的方法来真正的执行查询,并将查询结果返回给用户。
本章介绍 MySQL 是怎么执行单表查询的。
create table single_table (
id INT NOT NULL AUTO_INCREMENT,
key1 VARCHAR(100),
key2 INT,
key3 VARCHAR(100),
key_part1 VARCHAR(100),
key_part2 VARCHAR(100),
key_part3 VARCHAR(100),
common_field VARCHAR(100),
PRIMARY KEY (id),
KEY idx_key1 (key1),
UNIQUE KEY idx_key2 (key2),
KEY idx_key3 (key3),
KEY idx_key_part(key_part1, key_part2, key_part3)
) Engine=InnoDB CHARSET=utf8;
为 single_table 表建立了 1 个聚簇索引和 4 个二级索引,分别是:
- 为 id 列建立的聚簇索引。
- 为 key1 列建立的 idx_key1 二级索引。
- 为 key2 列建立的 idx_key2 二级索引,而且该索引是唯一二级索引。
- 为 key3 列建立的 idx_key3 二级索引。
- 为 key_part1、key_part2、key_part3 列建立的 idx_key_part 联合索引。
访问方法的概念
我们写的 select 查询语句本质上只是一种声明式的语法,
只是告诉 MySQL 我们要获取的数据符合哪些规则,
至于 MySQL 背后是怎么把查询结果搞出来的那是 MySQL 自己的事。
对于单个表的查询来说,设计 MySQL 的人把查询的执行方式大致分为两种:
- 使用全表扫描进行查询:把表的每一行记录都扫一遍,把符合搜索条件的记录加入到结果集。
- 使用索引进行查询:如果 select 查询语句中的搜索条件可以使用到某个索引,那直接使用 B+树索引来执行查询。
把 MySQL 执行查询语句的方式称为访问方法或者访问类型。
同一个查询语句可能可以使用多种不同的访问方法来执行,
虽然最后的查询结果都是一样的,但是执行花费的时间可能差别很大。
下边具体介绍各种访问方法。
Explain 详解
const
const 访问方法:根据主键 或者 唯一二级索引与常数的等值匹配查询。
意思是常数级别的,代价是可以忽略不计的。
常数,即固定的数或者字符。
const 访问方法只能在主键列 或者 唯一二级索引列和一个常数进行等值比较时才有效。
如果是联合索引,即主键 或者 唯一二级索引是由多个列构成的话,
索引中的每一个列都需要与常数进行等值比较时,const 访问方法才有效。
这是因为:只有该索引中全部列都采用等值比较才可以定位唯一的一条记录。
对于唯一二级索引来说,查询该列为 null 值的情况比较特殊,比如这样:
select * from single_table where key2 is null;
因为唯一二级索引列并不限制 null 值的数量,所以上述语句可能访问到多条记录,
也就是说上边这个语句不使用 const 访问方法来执行。
ref
ref 访问方法:根据普通二级索引(非唯一二级索引)与常数的等值匹配查询。
由于普通二级索引并不限制索引列值的唯一性,所以可能找到多条对应的记录,
也就是说,使用普通二级索引来执行查询的代价取决于:与常数的等值比较 匹配到的记录条数。
如果使用二级索引列与常数的等值比较 匹配到的记录条数较少,则回表的代价比较低,
所以 MySQL 可能选择使用该索引而不是全表扫描的方式来执行查询。
二级索引列值为 null 的情况:不论是普通的二级索引,还是唯一二级索引,
它们的索引列对包含 null 值的数量并不限制,
所以采用 where key is null 这种形式的搜索条件最多只能使用 ref 的访问方法,达不到 const 访问方法。
对于是联合索引的二级索引来说,最左边的连续索引列与常数的等值比较就可能采用 ref 的访问方法,
比如下边这几个查询:
select key_part1 from single_table where key_part1 = 'god like';
select key_part1 from single_table where key_part1 = 'god like' and key_part2 = 'legendary';
select key_part1 from single_table where key_part1 = 'god like';
但是,如果最左边的连续索引列并不全部是等值比较的话,它的访问方法就不能称为 ref 了,比如:
select key_part1 from single_table where key_part1 = 'god like' and key_part2 > 'legendary';
ref_or_null
有时候我们不仅想找出某个二级索引列的值等于某个常数的记录,还想把该列的值为 null 的记录也找出来,就像下边这个查询:
select key1 from single_table where key1 = 'abc' or key1 is null;
当使用二级索引,而不是全表扫描的方式执行上面的查询时,它使用的访问方法就称为 ref_or_null。
上边的查询相当于:从 idx_key1 索引对应的 B+树中找出条件为 key1 is null 和 key1 = ‘abc’ 的两个连续的记录范围。
range
range 访问方法:根据聚簇索引 或者 二级索列进行范围查询。
比如下面这个查询:
select * from single_table where key2 >= 38 and key2 <= 79;
index
index 访问方法:只扫描某个二级索引的B+树的叶子节点全部记录进行查询,不进行回表操作。
比如下面这个查询:
select key_part1, key_part2, key_part3 from single_table where key_part2 = 'abc';
由于 key_part2 并不是联合索引 idx_key_part 最左索引列,
所以无法使用 ref 或者 range 访问方法来执行这个语句。
但是上面的查询语句符合这两个条件:
- 查询列表中只有 3 个列:key_part1, key_part2, key_part3,并且索引 idx_key_part 又包含这 3 个列。
- 搜索条件中只有 key_part2 列,并且这个列也包含在索引 idx_key_part 中。
可以直接通过遍历 idx_key_part 索引的叶子节点的记录来比较 key_part2 = ‘abc’ 这个条件是否成立,
把匹配成功的二级索引记录的 key_part1, key_part2, key_part3 列的值直接加到结果集中就行了。
由于二级索引记录比聚簇索记录小的多,而且这个过程也不用进行回表操作,
所以直接遍历二级索引比直接遍历聚簇索引的成本要小很多。
all
all 访问方式:扫描聚簇索引的B+树的叶子节点的全部记录。
索引合并
MySQL 在一般的情况下执行一个查询时最多只会用到单个二级索引,
在特殊情况下,也可能在一个查询中使用到多个二级索引,
设计 MySQL 的人把这种使用多个索引来完成一次查询的执行方法称之为:索引合并 (index merge),
具体的索引合并算法有下边三种。
- Intersection 合并
- Union 合并
- Sort-Union 合并
具体介绍看原文章。