mysql 学习 - 掌握单表查询优化
本篇已收录在 MySQL 是怎样运行的 学习笔记系列
MySQL Server有一个称为查询优化器的模块,一条查询语句进行语法解析之后就会被交给查询优化器来进行优化,优化的结果就是生成一个所谓的执行计划,这个执行计划表明了应该使用哪些索引进行查询,表之间的连接顺序是啥样的,最后会按照执行计划中的步骤调用存储引擎提供的方法来真正的执行查询,并将查询结果返回给用户。但是在介绍查询优化器之前, 我们需要先对单表查询的优化进行一定程度上的掌握.
建表
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;
访问方法和访问类型
MySQL查询器在执行查询语句的方式称之为
访问方法
或者访问类型
访问方法 const
通过主键列来定位一条记录,比方说这个查询:
SELECT * FROM single_table WHERE id = 1438;
根据唯一二级索引列来定位一条记录的速度也是贼快的,比如下边这个查询:
SELECT * FROM single_table WHERE key2 = 3841;
第一步先从idx_key2对应的B+树索引中根据key2列与常数的等值比较条件定位到一条二级索引记录,然后再根据该记录的id值到聚簇索引中获取到完整的用户记录。
mysql 认为通过主键或者唯一二级索引列与常数的等值比较来定位一条记录是像坐火箭一样快的,所以他们把这种通过主键或者唯一二级索引列来定位一条记录的访问方法定义为:const,意思是常数级别的,代价是可以忽略不计的。
访问方法 ref
对某个普通的二级索引列与常数进行等值比较,比如这样:
SELECT * FROM single_table WHERE key1 = 'abc';
由于普通二级索引并不限制索引列值的唯一性,所以可能找到多条对应的记录,也就是说使用二级索引来执行查询的代价取决于等值匹配到的二级索引记录条数。如果匹配的记录较少,则回表的代价还是比较低的,所以MySQL可能选择使用索引而不是全表扫描的方式来执行查询。mysql 就把这种搜索条件为二级索引列与常数等值比较,采用二级索引来执行查询的访问方法称为:ref.
二级索引列值为NULL的情况
不论是普通的二级索引,还是唯一二级索引,它们的索引列对包含NULL值的数量并不限制,所以我们采用key IS NULL这种形式的搜索条件最多只能使用ref的访问方法,而不是const的访问方法。
二级索引为联合索引, 并且都是等值比较时的情况
对于某个包含多个索引列的二级索引来说,只要是最左边的连续索引列是与常数的等值比较就可能采用ref的访问方法,比方说下边这几个查询:
SELECT * FROM single_table WHERE key_part1 = 'god like';
SELECT * FROM single_table WHERE key_part1 = 'god like' AND key_part2 = 'legendary';
SELECT * FROM single_table WHERE key_part1 = 'god like' AND key_part2 = 'legendary' AND key_part3 = 'penta kill';
但是如果最左边的连续索引列并不全部是等值比较的话,它的访问方法就不能称为ref了,比方说这样:
SELECT * FROM single_table WHERE key_part1 = 'god like' AND key_part2 > 'legendary';
访问方法 ref_or_null
有时候我们不仅想找出某个二级索引列的值等于某个常数的记录,还想把该列的值为NULL的记录也找出来,就像下边这个查询:
SELECT * FROM single_table WHERE key1 = 'abc' OR key1 IS NULL;
当使用二级索引而不是全表扫描的方式执行该查询时,这种类型的查询使用的访问方法就称为ref_or_null,先分别从idx_key1索引对应的B+树中找出key1 IS NULL和key1 = 'abc'的两个连续的记录范围,然后根据这些二级索引记录中的id值再回表查找完整的用户记录。
访问方法 range
有时候我们面对的搜索条件更复杂,比如下边这个查询:
SELECT * FROM single_table WHERE key2 IN (1438, 6328) OR (key2 >= 38 AND key2 <= 79);
如果采用二级索引 + 回表的方式来执行的话,那么此时的搜索条件就不只是要求索引列与常数的等值匹配了,而是索引列需要匹配某个或某些范围的值,在本查询中key2列的值只要匹配下列3个范围中的任何一个就算是匹配成功了:
key2的值是1438
key2的值是6328
key2的值在38和79之间
mysql 把这种利用索引进行范围匹配的访问方法称之为:range.
访问方法 index
看下边这个查询:
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又包含这三个列。
搜索条件中只有key_part2列。这个列也包含在索引idx_key_part中。
也就是说我们可以直接通过遍历idx_key_part索引的叶子节点的记录来比较key_part2 = 'abc'这个条件是否成立,把匹配成功的二级索引记录的key_part1, key_part2, key_part3列的值直接加到结果集中就行了。由于二级索引记录比聚簇索记录小的多(聚簇索引记录要存储所有用户定义的列以及所谓的隐藏列,而二级索引记录只需要存放索引列和主键),而且这个过程也不用进行回表操作,所以直接遍历二级索引比直接遍历聚簇索引的成本要小很多,MySQL 就把这种采用遍历二级索引记录的执行方式称之为:index。
all
最直接的查询执行方式就是我们已经提了无数遍的全表扫描,对于InnoDB表来说也就是直接扫描聚簇索引,MySQL把这种使用全表扫描执行查询的方式称之为:all。
注意事项
尽量少的回表
下边的这个查询:
SELECT * FROM single_table WHERE key1 = 'abc' AND key2 > 1000;
查询优化器会识别到这个查询中的两个搜索条件:
key1 = 'abc'
key2 > 1000
优化器一般会根据single_table表的统计数据来判断到底使用哪个条件到对应的二级索引中查询扫描的行数会更少,选择那个扫描行数较少的条件到对应的二级索引中查询.
一个使用到索引的搜索条件和没有使用该索引的搜索条件使用OR连接起来后是无法使用该索引的
SELECT * FROM single_table WHERE key2 > 100 OR common_field = 'abc';
我们把使用不到idx_key2索引的搜索条件替换为TRUE:
SELECT * FROM single_table WHERE key2 > 100 OR TRUE;
接着化简:
SELECT * FROM single_table WHERE TRUE;
索引合并
Intersection合并
MySQL在某些特定的情况下才可能会使用到Intersection索引合并:
情况一:二级索引列是等值匹配的情况,对于联合索引来说,在联合索引中的每个列都必须等值匹配,不能出现只匹配部分列的情况。
情况二:主键列可以是范围匹配
Union合并
Intersection是交集的意思,这适用于使用不同索引的搜索条件之间使用AND连接起来的情况;Union是并集的意思,适用于使用不同索引的搜索条件之间使用OR连接起来的情况。与Intersection索引合并类似,MySQL在某些特定的情况下才可能会使用到Union索引合并:
情况一:二级索引列是等值匹配的情况,对于联合索引来说,在联合索引中的每个列都必须等值匹配,不能出现只出现匹配部分列的情况。
情况二:主键列可以是范围匹配
情况三:使用Intersection索引合并的搜索条件
查询条件符合了这些情况也不一定就会采用Union索引合并,也得看优化器的心情。优化器只有在单独根据搜索条件从某个二级索引中获取的记录数比较少,通过Union索引合并后进行访问的代价比全表扫描更小时才会使用Union索引合并。
Sort-Union合并
Union索引合并的使用条件太苛刻,必须保证各个二级索引列在进行等值匹配的条件下才可能被用到,比方说下边这个查询就无法使用到Union索引合并:
SELECT * FROM single_table WHERE key1 < 'a' OR key3 > 'z'
这是因为根据key1 < 'a'从idx_key1索引中获取的二级索引记录的主键值不是排好序的,根据key3 > 'z'从idx_key3索引中获取的二级索引记录的主键值也不是排好序的,但是key1 < 'a'和key3 > 'z'这两个条件又特别让我们动心,所以我们可以这样:
先根据key1 < 'a'条件从idx_key1二级索引中获取记录,并按照记录的主键值进行排序
再根据key3 > 'z'条件从idx_key3二级索引中获取记录,并按照记录的主键值进行排序
因为上述的两个二级索引主键值都是排好序的,剩下的操作和Union索引合并方式就一样了。
联合索引替代Intersection索引合并
SELECT * FROM single_table WHERE key1 = 'a' AND key3 = 'b';
这个查询之所以可能使用Intersection索引合并的方式执行,还不是因为idx_key1和idx_key3是两个单独的B+树索引,你要是把这两个列搞一个联合索引,那直接使用这个联合索引就把事情搞定了,何必用啥索引合并呢,就像这样:
ALTER TABLE single_table drop index idx_key1, idx_key3, add index idx_key1_key3(key1, key3);
这样我们把没用的idx_key1、idx_key3都干掉,再添加一个联合索引idx_key1_key3,使用这个联合索引进行查询简直是又快又好,既不用多读一棵B+树,也不用合并结果. 不过小心有单独对key3列进行查询的业务场景,这样子不得不再把key3列的单独索引给加上。