准备工作

SQL脚本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
--Create Database
CREATE DATABASE test

--Create Table
USE test;

CREATE TABLE `single_table` (    
`id` INT(11) NOT NULL AUTO_INCREMENT,    
`key1` VARCHAR(100),    
`key2` INT(11) ,    
`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 uq_key2 (`key2`),    
KEY idx_key3 (`key3`),    
KEY idx_key_part(`key_part1`, `key_part2`, `key_part3`)
) Engine=InnoDB;

--Create Procedure
DELIMITER ;;
CREATE PROCEDURE cost_data()
    BEGIN    
    DECLARE i INT;    
    SET i=0;    
    WHILE i<10000 DO        
        INSERT INTO single_table(key1,key2,key3,key_part1,key_part2,key_part3,common_field)
		VALUES(SUBSTRING(MD5(RAND()),1,2),i+1,SUBSTRING(MD5(RAND()),1,3),SUBSTRING(MD5(RAND()),1,4),
		SUBSTRING(MD5(RAND()),1,5),SUBSTRING(MD5(RAND()
),1,6),SUBSTRING(MD5(RAND()),1,7));        
        SET i=i+1;    
    END WHILE;
END;;
DELIMITER ;

--Call Procedure
CALL cost_data();

--Query Data
SELECT COUNT(*) FROM test.single_table;
SELECT * FROM test.single_table LIMIT 20;

RBO优化

一些预制的经典RBO规则

  • 移除不必要的括号
  • 常量传递
  • 移除没用的表达式
  • 表达式计算
  • HAVING 子句和 WHERE 子句的合并
  • 常量表检测

左外连接、右外连接的位置不能互换
但是 内连接可以,选择驱动表、被驱动表就很关键
被驱动表 WHERE 子句合并空值拒绝条件时了,外连接 和 内连接可以相互转换

子查询根据维度的不同,可以分为:

  • 标量子查询,单列单值
  • 行子查询,只有一行
  • 列子查询,一列但有多个值
  • 表子查询

子查询在语句中的位置

  • SELECT 子句中
  • FROM 子句中
  • WHERE 或者 ON 子句中
  • ORDER BY 子句中
  • GROUP BY 子句中

一些 boolean 语句

  • IN、NOT IN,判断某个操作数是否存在于子查询结果集合中
  • ANY(SOME),只要结果中存在一个值就符合条件
  • ALL,结果集中必须所有条件都符合
  • [NOT] EXIST,仅仅判断子查询的结果集中是否有记录,而不在乎其记录具体是什么

ANY的一些优化

1
2
3
4
5
6
7
xxx  < ANY(SELECT inner_expr...)
-- 可以转化为
xxx < (SELECT max(inner_expr)...)

xxx > ANY(SELECT inner_expr...)
-- 可以转换为
xxx > (SELECT min(inner_expr)...)

ALL的一些优化

1
2
3
4
5
6
7
xxx < ALL(SELECT inner_expr...)
-- 可以转换为
xxx < (SELECT min(inner_expr)...)

xxx > ALL(SELECT inner_expr...)
-- 可以转换为
xxx > (SELECT max(inner_expr)...)

按照与外层查询关系分类

  • 不相关子查询
  • 相关子查询

IN 子查询优化,先尝试转为半连接,如果可以则继续用下面 5种规则优化

  • Talbe pullout,表上提,必须是主键
  • Duplicate Weedout,用临时表去重
  • LooseScan
  • Semi-join Materialization
  • FirshMatch

talbe pullout优化

1
2
3
4
SELECT * FROM s1 WHERE key2 IN (SELECT key2 FROM s2 WHERE key3 = 'a';
-- s2的key2是唯一索引,肯定不重复
-- 相当于 s1.key2 = s2.key2,直接将子查询上提
SELECT s1.* FROM s1 INNER JOIN s2 ON s1.key2 = s2.key2 WHERE s2.key3 = 'a'

loose scan优化

1
2
3
4
5
6
SELECT * FROM s1 WHERE key3 IN (SELECT key1 FROM s2 WHERE key1 > 'a' AND key1 < 'b')
-- 相当于 s1.key3 = s2.key1
-- 子查询本身带了 WHERE,对 s2的key1做了范围扫描 (a, b)
-- 对于满足条件的如 s2.key1:aa、aa、aa、ab、ab、ac、ac
-- 只需要取重复记录的第一条即可
-- 所以叫松散扫描

如果不能转为 半连接,则:

  • 先将子查询物化,再执行查询
  • 执行 IN 到 EXIST 的转换

派生表,也就是 FROM 后面跟着的查询

  • 优先尝试跟外层查询进行合并
  • 如果不能合并,则物化处理

CBO 优化

查看表状态

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
SHOW TABLE STATUS LIKE 'single_table'\G
*************************** 1. row ***************************
           Name: single_table
         Engine: InnoDB
        Version: 10
     Row_format: Dynamic
           Rows: 9748
 Avg_row_length: 163
    Data_length: 1589248
Max_data_length: 0
   Index_length: 1212416
      Data_free: 4194304
 Auto_increment: 10001
    Create_time: 2022-12-14 19:49:32
    Update_time: NULL
     Check_time: NULL
      Collation: latin1_swedish_ci
       Checksum: NULL
 Create_options:
        Comment:

这里有两个很重要的字段:

  • rows,表中记录的行数,InnoDB并不精准
  • Data_length,InnoDB聚集索引占用的存储空间大小

默认page 是16K,所以反推出 page数量:
聚集索引页面数量 = 1589248 / 16 / 1024.0 = 97个page

索引扫描成本分析

假设查询语句如下:

1
2
3
4
5
6
SELECT * FROM single_table WHERE
  key1 IN ('a', 'b', 'c') AND 
  key2 > 10 AND key2 < 1000 AND 
  key3 > key2 AND
  key_part1 LIKE '%hello%' AND
  common_field = '123'

下面要分析各种情况的成本

  • 全表扫描
  • 使用key1的 idx_key1 索引成本
  • 使用key2的 uk_key2 索引成本
  • 其他如 key3 不是常数比较用不到索引;key_part1也用不到;common_field没有索引

全表扫描成本

IO成本:
97 * 1.0 + 1.1 = 98.1

这里的97是 page 数量, 1.0是加载一个page的常数成本,后面的1.1是微调值

CPU成本:
9748 * 0.2 + 1.0 = 1950.6

这里的 9748 是总记录数,InnoDB中是一个估算值, 0.2是访问一条记录所需的常数成本,后面的.1.0是微调值

总成本 = 98.1 + 1950.6 = 2048.7

所以 扫描全表的成本是 2048.7

唯一索引 uk_key2的成本
对于需要回表的情况要依赖两方面因素:
1、扫描区间数量
2、需要回表的记录数

MySQL粗暴的认为,扫描一个区间的I/O成本跟 一个方页面的I/O成本一样
这里只扫描了一个区间所以:

扫描区间的成本:
1 * 1.0 = 1.0

需要回表的数量
计算 key2 > 10 AND key2 < 1000 这个索引页面的记录数
非叶节点的记录数就对应 叶子节点的page数
递归B+ 数到最左边10这个非叶结点,再递归到最右边1000这个非叶节点
如果 10 和 1000正好在一个页面就直接统计,否则再递归往下遍历
回表的CPU: 95条记录 * 02. + 0.01 = 19.01 0.01 是微调值

根据这些记录到 聚集索引中的成本

InnoDB中,每次回表相当于访问一个page
I/O成本: 95 * 1.0 = 95.0

CPU读取记录的成本
95 * 0.2 = 19.0

使用 uk_key2的总成本

I/O 成本: 二级索引扫描区间成本 + 预估回表扫描成本 = 1.0 + 95 * 1.0 = 96.0
CPU成本: 读取二级索引记录数 + 回表读取聚集索引记录成本 = 95 * 0.2 + 0.01 +95 * 0.2 = 38.01
uk_key2 总成本 = 96.0 + 38.01 = 134.01

普通索引 idx_key1的成本
查询条件为: key1 IN (‘a’, ‘b’, ‘c’)

索引扫描区间成本

3个 单点扫描 = 3 * 1.0 = 3.0
需要回表的记录数,要分别统计’a’,‘b’,‘c’ 三个区间的
方法还是遍历最左非叶节点,最右非叶节点,再统计数量
假设分别为 35、44、39,总回表记录数为: (35 + 34 + 39) * 0.2 + 0.01 = 23.61

回表去聚集索引的成本

I/O 成本 = 118 * 1.0 = 118.0
CPU成本 = 118 * 0.2 = 23.6

idx_key1 的总成本

I/O成本:3.0 + 118 * 1.0 = 121 扫描二级索引区间数量 + 预计二级索引记录数量
CPU成本:118 * 0.2 + 0.01 + 118 * 0.2 = 47.21 读取二级索引记录的成本 + 回表检查的成本
总成本 : 121 + 47.2 = 168.21

其他情况,预估可能使用到的 索引合并
对比各种情况:

  • 全表扫描成本 2048.7
  • 使用uk_key2的成本: 134.01
  • 使用idx_key1的成本: 168.21

所以会选择uk_key2来执行查询

基于统计信息的成本计算

扫描区间大于某个阈值,就使用基于统计信息的计算:

1
SHOW VARIABLES LIKE '%dive%'
Variable_name Value
eq_range_index_dive_limit 200

2

字段含义:

  • Table:索引所在的表名
  • Non_unique:0表示该索引是唯一索引,1表示该索引不是唯一索引
  • Key_name:索引的名称
  • Seq_in_index:索引列在索引中的位置,从1开始。对于组合索引来说,这个字段很重要。
  • Column_name:索引列的名称
  • Collation:索引列的值以什么方式存储在索引中。在MySQL中,A 表示有排序,B+树索引使用该方式;NULL 表示无序的,Heap索引使用该方式;
  • Cardinality:索引中唯一值的数目的估计值。通过运行ANALYZE TABLE或myisamchk -a可以更新。基数根据被存储为整数的统计数据来计数,所以即使对于小型表,该值也没有必要是精确的。基数越大,当进行联合时,MySQL使用该索引的机会就越大。索引列所有值去重后的数量。该值除以该表的行数越接近1越好,如果非常小,则需要考虑是否可以删除该索引!
  • Sub_part:数值 N 表示只对该列的前 N 个字符进行索引;NULL 表示索引整个列的值
  • Packed:指示关键字是否被压缩,NULL 表示没有压缩
  • Null:索引列是否可以为空
  • Index_type:索引类型,BTREE 表示B+树索引。一共有四种(BTREE, FULLTEXT, HASH, RTREE)。
  • Comment:注释
  • Index_comment:注释

这里主要使用到两个值:

  • SHOW TABLE STATUS中的 rows
  • SHOW INDEX中的 cardinalit

一个值的重复次数 = row / cardinality

假设单点扫描区间有 1W个,如

1
SELECT * FROM s1 WHERE key1 IN (.....)   

rows 为9748,key1的cardinality为256
所以key1的每个值平均重复次数为: 9748 / 256 = 38.0
扫描区间1W 个,乘以 38(平均重复值),所以回表是 38W次

统计数据的收集

基于磁盘的永久统计信息存放在两个表中

  • mysql.innodb_table_stats
  • mysql.innodb_index_stats

innodb_table_stats 信息如下:
3 主要字段介绍

  • n_rows表中记录数量
  • clustered_index_size 聚集索引占用的page数量
  • sum_of_other_sizes 其他索引占用的page数量

n_rows统计信息收集
通过系统变量:

1
SHOW VARIABLES LIKE 'innodb_stats_persistent_sample_pages'

来控制的
默认值是20,然后计算出 这 20个page中平均每个page包含的记录数量 avg_page
之后,统计全部叶子节点数量 N
总记录数 = N * avg_page

clustered_index_size、sum_of_other_sizes 统计信息
通过系统表空间的 SYS_INDEXES 找到各个索引对应的 根page
每个根page 都有这两个信息:

  • PAGE_BTR_SEG_LEAF,B+树叶子段的头部信息,仅在B+树的Root页定义
  • PAGE_BTR_SEG_TOP,B+树非叶子段的头部信息,仅在B+树的Root页定义

通过这个信息就可以找到 INode etnry
mysql文件存储结构-18
这里包含了三个 list base node 结构体
mysql文件存储结构-19
每个 list base node都包含了 List Length,也就是链表长度
每个节点对应一个区,一个区 64个page,那么链表节点 * 64 就是占用的page数量
因为一个区的page可能有些是空闲的,所以这里统计出来的会比实际的要大
如果想完全精准统计,需要继续遍历每个链表指向的 XDES entry
每个 XDES entry包含了一个 bit map,统计了当前区中64个page使用情况
mysql文件存储结构-17

以上统计的是 clustered_index_size,sum_of_other_sizes也是同样的思路

innodb_index_stats 统计信息如下:
4 主要字段介绍

  • stat_name,统计项的名称
  • stat_value,对应的统计项的值
  • sample_size,为生成统计数据而采样的page数量,统计索引中包含多少不重复的值的采样
  • stat_description,统计项描述信息

stat_name信息

  • n_leaf_pages,该索引的叶子节点实际占用的page数量
  • size,该索引占用多少page,包括分配给叶子和非叶节点 但还没使用的page
  • n_diff_pfxNN,索引不重复的值有多少

n_diff_pfxNN

  • 对于主键、唯一索引,这一项只有一个
  • idx_key1是普通索引,n_diff_pfx01 表示key1不重复的个数,n_diff_pfx02表示key1+id 不重复的个数
  • idx_key_part有4个,n_diff_pfx01是key1不重复的,n_diff_pfx02是 key1 + key2不重复的,n_diff_pfx03 是key1 + key2 + key3 不重复的
  • n_diff_pfx04是 key1 + key2 + key3 + id 不重复的

联合索引的采样 = innodb_stats_persistent_sample_pages * 索引中包含的字段个数

统计信息的更新
开启更新

1
SHOW VARIABLES LIKE 'innodb_stats_auto_recalc';

如果表发生的数据变更超过了 10%,就会自动更新
也可以单独为表设置更新方式

手动更新

1
ANALYZE TABLE sinle_table   

innodb_table_stats、innodb_index_stats 这两个表跟普通表一样,也是可以直接通过SQL更新的
更新完后要刷新

1
FLUSH TABLE single_table;   

对待NULL的方式

1
SHOW VARIABLES LIKE 'innodb_stats_method'

三种方式

  • nulls_equal,认为所有NULL都是相等的,默认值,如果某个列NULL很多,导致列平均重复次数很多,优化器不倾向于用索引
  • nulls_unequal,认为所有NULL值都不等,倾向于使用所以
  • nulls_ignored,忽略NULL值

调节成本常数

mysql.server_cost 表信息:
5
server层的操作成本常数

  • disk_temptable_create_cost (default 40.0) 内部myisam或innodb临时表的创建代价
  • disk_temptable_row_cost (default 1.0) 向基于磁盘的临时表写入、读取一条记录的成本,增大这个值会让优化器尽可能减少创建磁盘临时表
  • key_compare_cost (default 0.1) 两个记录比较的代价,多用在排序中,增大此值会提升filesort成本,让优化器更倾向于使用索引
  • memory_temptable_create_cost (default 2.0) 创建基于内存的临时表成本,增大会让优化器尽可能减少创建基于内存的临时表
  • memory_temptable_row_cost (default 0.2) 向基于内存的临时表读、写一条记录的成本,增大此值会让优化器尽可能减少创建内存临时表
  • row_evaluate_cost (default 0.2) 读取并检查一条记录是否符号搜索条件的成本,增大这个值会让优化器倾向于使用索引,而不是全表扫描

手动修改

1
2
UPDATE `mysql`.`server_cost` SET `cost_value` = '0.00001' WHERE `cost_name` = 'disk_temptable_create_cost'; 
FLUSH OPTIMIZER_COSTS;

这样查询的时候,就更倾向于使用基于磁盘的临时表

mysql.engine_cost 表信息:
6
这里增加一个了 device_type,用来区分机械硬盘、固态硬盘的,只是没用到
engine层操作成本常数

  • io_block_read_cost (default 1.0) 从磁盘读数据的代价,对innodb来说,表示从磁盘读一个page的代价
  • memory_block_read_cost (default 1.0) 从内存读数据的代价,对innodb来说,表示从buffer pool读一个page的代价

从磁盘和从内存中读取的成本都是 1.0,这是因为MySQL不能很好的预测,当前的数据page,到底是在硬盘上,还是在内存中
所以粗暴的认为都是从硬盘读,所以都是 1.0

JOIN优化

MySQL的JOIN实现方式

  • 循环嵌套连接
  • 使用索引加速
  • 基于块的循环嵌套

查看 join_buffer_size,默认256K:

1
SHOW VARIABLES LIKE 'join_buffer_size'

条件过滤 conditionfilering
两个表连接的成本

  • 单词查询驱动表的成本
  • 多次查询被驱动表的成本

这里把查询 驱动表后,得到的就称为:扇出,fanout,这个值越小越好
如果驱动表是全表扫描,那么扇出的值就是 表的记录数
如果有索引,就需要预估,比如主键、二级索引的 范围,单点区间扫描的成本预估等
如果查询条件中还有非索引的列,就只能用启发式搜索,预测了

查询:

1
2
3
4
SELECT * FROM s1 INNER JOIN s2
  ON s1.key1 = s2.common_field
  WHERE s1.key2 > 10 AND s1.key2 < 1000 AND
        s2.key2 > 1000 AND s2.key2 < 2000

要分区 s1还是s2 作为驱动表

  • 假设uk_key2作为s1中索引成本最低的
  • 使用uk_key2访问s1的成本 + s1的扇出值 * 使用uk_key2访问 s2的成本
  • 使用uk_key2访问s2的成本 + s2的扇出值 * 使用Idx_key1访问s1的成本

优化重点:

  • 进来减少驱动表的扇出
  • 访问去被动表的成本尽可能低,也就是使用索引,最好是主键或者唯一索引

N表连接问题,$O(N!)$ 时间复杂度

  • 提前结束某种连接顺序的成本评估
  • 系统变量 optimizer_search_depth
  • 启发式规则,即忽略某些连接规则
1
2
3
4
5
-- 搜索深度默认 62   
SHOW VARIABLES LIKE 'optimizer_search_depth'

--- 是否启动这些启发式规则,帮助减枝
SHOW VARIABLES LIKE 'optimizer_prune_level'

EXPLAIN

各个字段

explain中的各个字段

列名 描述
id 在一个大的查询语句中,每个SELECT关键字都对应一个唯一的ID
select_type SELECT关键字对应的查询类型
table 表名
partitions 匹配的分区信息
type 针对单表的访问方法
possible_keys 可能用到的索引
key 实际使用的索引
key_len 实际使用的索引长度
ref 当使用索引列等值查询时,与索引列进行等值匹配的对象信息
rows 预估需要读取的记录数量
filtered 针对预估的需要读取的记录,经过搜索条件过滤后剩余记录的百分比
Extra 一些额外的信息

select_type

含义如下:

select_type Value JSON Name Meaning
SIMPLE None Simple SELECT (not using UNION or subqueries)
PRIMARY None Outermost SELECT
UNION None Second or later SELECT statement in a UNION
DEPENDENT UNION dependent (true) Second or later SELECT statement in a UNION, dependent on outer query
UNION RESULT union_result Result of a UNION.
SUBQUERY None First SELECT in subquery
DEPENDENT SUBQUERY dependent (true) First SELECT in subquery, dependent on outer query
DERIVED None Derived table
DEPENDENT DERIVED dependent (true) Derived table dependent on another table
MATERIALIZED materialized_from_subquery Materialized subquery
UNCACHEABLE SUBQUERY cacheable (false) A subquery for which the result cannot be cached and must be re-evaluated for each row of the outer query
UNCACHEABLE UNION cacheable (false) The second or later select in a UNION that belongs to an uncacheable subquery (see UNCACHEABLE SUBQUERY)

SIMPLE
不包含 UNION或者子查询的都是 SIMPLE

1
EXPLAIN SELECT * FROM s1;
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE s1 NULL ALL NULL NULL NULL NULL 9958 100.0 NULL

PRIMARY
包含UNION、UNION ALL、或者子查询的大查询,其由好几个部分组成,最左边的select_tupe就是PRIMARY

1
EXPLAIN SELECT * FROM s1 UNION SELECT * FROM s2;
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 PRIMARY s1 NULL ALL NULL NULL NULL NULL 9958 100.0 NULL
2 UNION s2 NULL ALL NULL NULL NULL NULL 9958 100.0 NULL
NULL UNION RESULT <union1,2> NULL ALL NULL NULL NULL NULL 9958 100.0 NULL

UNION
UNION RESULT
这两个在上面已经展示了,其中 UNION RESULT用作去重的,如果是 UNION ALL则没有这一步

SUBQUERY
如果子查询不能转为半连接查询,并且该子查询是 不相关子查询,则将子查询物化执行
如下外层的是PRIMARY,内层的子查询会被物化,只执行一次

1
EXPLAIN SELECT * FROM s1 WHERE key1 IN (SELECT key1 FROM s2) OR key3 = 'a';
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 PRIMARY s1 NULL ALL NULL NULL NULL NULL 9958 100.0 Using where
2 SUBQUERY s2 NULL ALL NULL NULL NULL NULL 9958 100.0 NULL

DEPENDENT SUBQUERY
如果包子查询不能转为半连接,该子查询能被转为相关子查询
这种子查询意味着可能会被执行多次

1
EXPLAIN SELECT * FROM s1 WHERE key1 IN (SELECT key1 FROM s2 WHERE s1.key2 = s2.key2) OR key3 = 'a';
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 PRIMARY s1 NULL ALL idx_key3 NULL NULL NULL 9991 100.0 Using where
2 DEPENDENT SUBQUERY s2 NULL ref uq_key2,idx_key1 uq_key2 5 test.s1.key2 1 100.0 Using where

DEPENDENT UNION
其子查询可能会被执行多次
大查询包含子查询,子查询又包含UNION 的两个小查询,其select_type就是 DEPENDENT SUBQUERY
SELECT key1 FROM s1 WHERE key1 = 'b' 这个小查询的 select_type 就是 DEPENDENT UNION

1
EXPLAIN SELECT * FROM s1 WHERE key1 IN (SELECT key1 FROM s2 WHERE key1 = 'a' UNION SELECT key1 FROM s1 WHERE key1 = 'b')
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 PRIMARY s1 NULL ALL NULL NULL NULL NULL 9991 100.0 Using where
2 DEPENDENT SUBQUERY s2 NULL ref idx_key1` idx_key1 103 const 1 100.0 Using where
3 DEPENDENT UNION s1 NULL ref idx_key1 idx_key1 103 const 1 100.0 Using where
NULL DEPENDENT RESULT <union2,3> NULL ALL NULL NULL NULL NULL NULL NULL Using temporary

再看这个SQL

1
2
3
EXPLAIN SELECT * FROM s1 WHERE key1 IN (SELECT key1 FROM s2 WHERE key1 = 'a' UNION SELECT key1 FROM s1 WHERE key1 = 'b' UNION 
	SELECT key1 FROM s2 WHERE key1 = 'b' UNION SELECT key1 FROM single_table WHERE key1 = 'c'
)

1

DERIVED

1
explain select * from (select key1, count(*) as c from s1 group by key1) as derived_s1 where c > 1;
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 PRIMARY NULL ALL NULL NULL NULL NULL 9958 100.0 Using where
2 DERIVED s1 NULL ALL NULL NULL NULL NULL 9958 100.0 Using temporary; Using filesort

MATERIALIZED
将子查询物化之后,与外层查询进行连接查询
如下第3 条记录就是 MATERIALIZED,优化器将子查询转为物化表,前两条记录的id 都是1,说明对应的表是连接查询
第二条记录的table是,说明对物化之后的表做的查询,然后再将s1和该物化表做连接查询

1
EXPLAIN SELECT * FROM s1 WHERE key1 IN (SELECT key1 FROM s2);
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE s1 NULL ALL NULL NULL NULL NULL 9958 100.0 Using where
1 SIMPLE <subquery2> NULL eq_ref <auto_key> <auto_key> 103 test.s1.key1 1 100.0 NULL
2 MATERIALIZED s2 NULL ALL NULL NULL NULL NULL 9958 100.0 Using temporary; Using filesort

partitions

准备数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
CREATE TABLE `tr` (
     `id` INT, 
     `name` VARCHAR(50), 
     `purchased` DATE
) ENGINE=INNODB DEFAULT CHARSET=utf8
PARTITION BY RANGE( YEAR(purchased) ) (
     PARTITION p0 VALUES LESS THAN (1990),
     PARTITION p1 VALUES LESS THAN (1995),
     PARTITION p2 VALUES LESS THAN (2000),
     PARTITION p3 VALUES LESS THAN (2005),
     PARTITION p4 VALUES LESS THAN (2010),
     PARTITION p5 VALUES LESS THAN (2015)
);


INSERT INTO `tr` VALUES
 (1, 'desk organiser', '2003-10-15'),
 (2, 'alarm clock', '1997-11-05'),
 (3, 'chair', '2009-03-10'),
 (4, 'bookcase', '1989-01-10'),
 (5, 'exercise bike', '2014-05-09'),
 (6, 'sofa', '1987-06-05'),
 (7, 'espresso maker', '2011-11-22'),
 (8, 'aquarium', '1992-08-04'),
 (9, 'study desk', '2006-09-16'),
 (10, 'lava lamp', '1998-12-25');

查询

1
EXPLAIN SELECT * FROM tr WHERE purchased = '2014-05-09'
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE tr p5 ALL NULL NULL NULL NULL 9958 100.0 Using where

type

system
只有一条数据,并且统计是精准时

1
2
3
CREATE TABLE t(i INT) ENGINE=MYISAM;
INSERT INTO t VALUES(1);
EXPLAIN SELECT * FROM t; 
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE t NULL system NULL NULL NULL NULL 1 100.0 NULL

const
当使用主键 或者唯一二级索引 进行常数匹配

1
EXPLAIN SELECT * FROM s1 WHERE id = 5;
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE t NULL const PRIMARY PRIMARY 4 CONST 1 100.0 NULL

eq_ref
被驱动表通过 主键、唯一二级索引列进行等值匹配时

1
EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.id = s2.id
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE s1 NULL ALL idx_key1 NULL NULL NULL 9991 100.0 Using where
1 SIMPLE s2 NULL ref idx_key1 idx_key1 103 test.s1.id 39 100.0 NULL

ref
对普通二级索引与常量等值匹配时

1
EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.key1 = s2.key1;
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE s1 NULL ALL NULL NULL NULL NULL 1 100.0 NULL
1 SIMPLE s2 NULL eq_ref PRIMARY PRIMARY 4 test.s1.id 1 100.0 NULL

单表查询

1
EXPLAIN SELECT * FROM single_table WHERE key1 = 'abc'
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE single_table NULL ref idx_key1 idx_key1 103 const 1 100.0 NULL

fulltext
全文索引

ref_or_null
对普通二级索引与常量等值匹配时,并且该索引列的值也可以为NULL

1
EXPLAIN SELECT * FROM s1 WHERE key1 = 'a' OR key1 IS NULL
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE s1 NULL ref_or_null idx_key1 idx_key1 103 const 2 100.0 Using index condition

index_merge
一般只会为单个索引生成扫描区间,但某些情况下会生成索引何彪

  • Intersection
  • Union
  • Sort-Union
  • 注意,MySQL 没有Sort-Intersection,不过 mariadb 有这个功能
1
EXPLAIN SELECT * FROM s1 WHERE key1 = 'a' OR key3 = 'a';
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE s1 NULL index_merge idx_key1,idx_key3 idx_key1,idx_key3 103,103 NULL 2 100.0 Using union(idx_key1,idx_key3);Using where

unique_subquery
类似于eq_ref,针对一些包含IN子句的查询语句
如果可以将IN转为EXIST,并使用主键,或者唯一二级索引时

1
EXPLAIN SELECT * FROM s1 WHERE common_field IN (SELECT id FROM s2 WHERE s1.common_field = s2.common_field) OR key3 = 'a';
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 PRIMARY s1 NULL ALL idx_key3 idx_key1 NULL NULL 9991 100.0 Using where
2 DEPENDENT SUBQUERY s2 NULL unique_subquery PRIMARY PRIMARY 4 func 1 10.0 Using where

index_subquery
unique_subquery类似,使用的是普通索引

1
EXPLAIN SELECT * FROM s1 WHERE common_field IN (SELECT key3 FROM s2 WHERE s1.common_field = s2.common_field) OR key3 = 'a';
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 PRIMARY s1 NULL ALL idx_key3 NULL NULL NULL 9991 100.0 Using where
2 DEPENDENT SUBQUERY s2 NULL index_subquery idx_key3 idx_key3 103 func 2 10.0 Using where

range
范围查询

1
EXPLAIN SELECT * FROM s1 WHERE KEY1 IN ('a', 'b', 'c');
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE s1 NULL range PRIMARY PRIMARY 4 NULL 4 100.00 Using where

index
使用覆盖索引

1
EXPLAIN SELECT key_part2 FROM s1 WHERE key_part3 = 'a';
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE s1 NULL index NULL id_key_part 309 NULL 9991 10.00 Using where; Using index

下面这种情况,对id做排序,也会变成index类型

1
EXPLAIN SELECT * FROM s1 ORDER BY id;
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE s1 NULL index NULL id_key_part 4 NULL 9991 10.00 NULL

ALL
全表扫描

1
EXPLAIN SELECT * FROM s1 ORDER BY id;
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE s1 NULL ALL NULL NULL NULL NULL 9991 100.00 NULL

EXPLAIN其他字段

key_len:实际使用的索引长度
比如某次查询 key_len为 103,100是varchar长度,ascii类型的
因为是变长,所以+2,又因为允许为NULL,又+1, 所以是 103
如果扫描区间为 SELECT key_par1 = ‘a’ AND key_part2 > ‘b’
则使用key_len 为 103 * 2

ref
当访问方式是 const、eq_ref、ref、ref_or_null、unique_subquery、index_subquery中的一个时
ref列展示的就是与索引等值匹配的东西是什么
比如一个常数,一个具体的列名字: test.s1.id、或者是一个函数(显示为func)

filter
主要用在多表连接时

  • 使用全表扫描的方式执行单表查询,驱动表扇出就是全部搜索记录是多少条
  • 使用索引时,计算驱动表扇出时要预估满足形成索引扫描区间的搜索条件外,还满足其他搜索条件的记录有多少条
1
EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.key1 = s2.key1 WHERE s1.common_field = 'a';
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE s1 NULL ALL idx_key1 NULL NULL NULL 9991 10.0 Using where
2 SIMPLE s2 NULL ref idx_key1 idx_key1 103 test.s1.key1 39 100.0 Using where

id = 1的是驱动表,rows为:9991
filter的10.0表示:对s1 的扇出值为: 9991 * 10% = 999条

Extra

官方解释 -> 这里

No tables used
查询语句中没有FROM 子句时

1
SELECT 1

Impossible WHERE
查询语句的WHERE 为FALSE时

1
SELECT * FROM s1 WHERE 1 != 1

No matching min/max row
当查询列表处有 MIN、MAX时,但并没有记录符合WHERE 子句中的搜索条件

1
EXPLAIN SELECT MIN(key1) FROM s1 WHERE key1 = 'abcdefg'

Using index
使用覆盖索引

1
SELECT key1 FROM s1 WHERE key1 = 'a'

Using index condition
使用索引下推,只适合二级索引,下面SQL会查找出key1 > ‘z’,但是不回表,继续判断是否满足 ‘%a’,提高效率

1
SELECT * FROM s1 WHERE key1 > 'z' AND key1 LIKE '%a';

Using WHERE
在 server层做判断时

1
SELECT * FROM s1 WHERE common_field = 'a'

Using join buffer(Block Nested Loop)
被驱动表不能有效利用索引时,分配了一块连接缓冲区

1
SELECT * FROM s1 INNER JOIN s2 ON s1.common_field = s2.common_field

Using intersect
还包括Using unionUsing sort_union
说明使用了索引合并的方式

1
2
-- Using union(PRIMARY,uq_key2); Using where
SELECT * FROM s1 WHERE id = 1 OR key2 = 2

Zero limit 当limit子句的参数为0时

1
SELECT * FROM s1 LIMIT 0

Using filesort
使用排序时

1
SELECT * FROM s1 ORDER BY common_field LIMIT 10

Using temporary
使用临时表时

1
SELECT DISTINCT common_field FROM s1

下面这种语句就会使用临时表 + 排序,虽然没有排序,但是Mysql 默认对于使用 GROUP BY 加了 ORDER BY

1
2
3
4
SELECT common_field, COUNT(*) AS c FROM s1 GROUP BY common_field

-- 改成这样就没有 filesort了   
SELECT common_field, COUNT(*) AS c FROM s1 GROUP BY common_field ORDER BY NULL

Start temporary, End tmporary
优化器会尝试将 IN子查询 转为半连接,当执行策略为 Duplicate Weedout时,是通过建立临时表来做去重
驱动表的执行计划Extra会显示 Start temporary提示,被驱动表的Extra会提示 End temporary

LooseScan
当 IN子查询转为半连接时,采用松散扫描时

1
2
-- 驱动该表显示: Using where; Using index; LooseScan
EXPLAIN SELECT * FROM s1 WHERE key3 IN (SELECT key1 FROM s2 WHERE key1 >= 'z')

FirstMatch
将 IN子查询转为半连接时,采用 first-match方式,被驱动表的Extra则会有提示

1
2
-- 被驱动表提示: Using where; FirstMatch(s1)
SELECT * FROM s1 WHERE common_field IN (SELECT key1 FROM s2 WHERE s1.key3 = s2.key3)

JSON格式的执行计划

查询:

1
2
EXPLAIN FORMAT=JSON
SELECT * FROM s1 INNER JOIN s2 ON s1.key1 = s2.key2 WHERE s1.common_field= 'a'

执行结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
{
	"query_block": {
		"select_id": 1,
		"cost_info": {
			"query_cost": "3294.12"
		},
		"nested_loop": [{
				"table": {
					"table_name": "s1",
					"access_type": "ALL",
					"possible_keys": [
						"idx_key1"
					],
					"rows_examined_per_scan": 9991,
					"rows_produced_per_join": 999,
					"filtered": "10.00",
					"cost_info": {
						"read_cost": "1895.38",
						"eval_cost": "199.82",
						"prefix_cost": "2095.20",
						"data_read_per_join": "601K"
					},
					"used_columns": [
						"id",
						"key1",
						"key2",
						"key3",
						"key_part1",
						"key_part2",
						"key_part3",
						"common_field"
					],
					"attached_condition": "((`test`.`s1`.`common_field` = 'a') and (`test`.`s1`.`key1` is not null))"
				}
			},
			{
				"table": {
					"table_name": "s2",
					"access_type": "ref",
					"possible_keys": [
						"uq_key2"
					],
					"key": "uq_key2",
					"used_key_parts": [
						"key2"
					],
					"key_length": "5",
					"ref": [
						"test.s1.key1"
					],
					"rows_examined_per_scan": 1,
					"rows_produced_per_join": 999,
					"filtered": "100.00",
					"index_condition": "(`test`.`s1`.`key1` = `test`.`s2`.`key2`)",
					"cost_info": {
						"read_cost": "999.10",
						"eval_cost": "199.82",
						"prefix_cost": "3294.12",
						"data_read_per_join": "601K"
					},
					"used_columns": [
						"id",
						"key1",
						"key2",
						"key3",
						"key_part1",
						"key_part2",
						"key_part3",
						"common_field"
					]
				}
			}
		]
	}
}

optimizer trace

查看状态

1
SHOW VARIABLES LIKE 'optimizer_trace'

开启:

1
SET optimizer_trace="enabled=on"
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE s1 NULL range uq_key2,idx_key1,idx_key3 idx_key1 103 NULL 1 5.0 Using index condition; Using where

查询:

1
2
3
4
5
6
EXPLAIN 
SELECT * FROM s1 WHERE 
key1 > 'z' AND 
key2 < 100000 AND 
key3 IN ('a', 'b', 'c') AND
common_field = 'abc'

查询结果

1
SELECT *FROM information_schema.`OPTIMIZER_TRACE`

优化过程:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
{
  "steps": [
    {
      "join_preparation": {
        "select#": 1,
        "steps": [
          {
            "IN_uses_bisection": true
          },
          {
            "expanded_query": "/* select#1 */ select `s1`.`id` AS `id`,`s1`.`key1` AS `key1`,`s1`.`key2` AS `key2`,
			`s1`.`key3` AS `key3`,`s1`.`key_part1` AS `key_part1`,`s1`.`key_part2` AS `key_part2`,`s1`.`key_part3` AS
			`key_part3`,`s1`.`common_field` AS `common_field` from `s1` where ((`s1`.`key1` > 'z') 
			and (`s1`.`key2` < 100000) and (`s1`.`key3` in ('a','b','c')) and (`s1`.`common_field` = 'abc'))
			limit 0,1000"
          }
        ]
      }
    },
    {
      "join_optimization": {
        "select#": 1,
        "steps": [
          {
            "condition_processing": {
              "condition": "WHERE",
              "original_condition": "((`s1`.`key1` > 'z') and (`s1`.`key2` < 100000) and (`s1`.`key3` in ('a','b','c')) and (`s1`.`common_field` = 'abc'))",
              "steps": [
                {
                  "transformation": "equality_propagation",
                  "resulting_condition": "((`s1`.`key1` > 'z') and (`s1`.`key2` < 100000) and (`s1`.`key3` in ('a','b','c')) and multiple equal('abc', `s1`.`common_field`))"
                },
                {
                  "transformation": "constant_propagation",
                  "resulting_condition": "((`s1`.`key1` > 'z') and (`s1`.`key2` < 100000) and (`s1`.`key3` in ('a','b','c')) and multiple equal('abc', `s1`.`common_field`))"
                },
                {
                  "transformation": "trivial_condition_removal",
                  "resulting_condition": "((`s1`.`key1` > 'z') and (`s1`.`key2` < 100000) and (`s1`.`key3` in ('a','b','c')) and multiple equal('abc', `s1`.`common_field`))"
                }
              ]
            }
          },
          {
            "substitute_generated_columns": {
            }
          },
          {
            "table_dependencies": [
              {
                "table": "`s1`",
                "row_may_be_null": false,
                "map_bit": 0,
                "depends_on_map_bits": [
                ]
              }
            ]
          },
          {
            "ref_optimizer_key_uses": [
            ]
          },
          {
            "rows_estimation": [
              {
                "table": "`s1`",
                "range_analysis": {
                  "table_scan": {
                    "rows": 9991,
                    "cost": 11991
                  },
                  "potential_range_indexes": [
                    {
                      "index": "PRIMARY",
                      "usable": false,
                      "cause": "not_applicable"
                    },
                    {
                      "index": "uq_key2",
                      "usable": true,
                      "key_parts": [
                        "key2"
                      ]
                    },
                    {
                      "index": "idx_key1",
                      "usable": true,
                      "key_parts": [
                        "key1",
                        "id"
                      ]
                    },
                    {
                      "index": "idx_key3",
                      "usable": true,
                      "key_parts": [
                        "key3",
                        "id"
                      ]
                    },
                    {
                      "index": "idx_key_part",
                      "usable": false,
                      "cause": "not_applicable"
                    }
                  ],
                  "setup_range_conditions": [
                  ],
                  "group_index_range": {
                    "chosen": false,
                    "cause": "not_group_by_or_distinct"
                  },
                  "analyzing_range_alternatives": {
                    "range_scan_alternatives": [
                      {
                        "index": "uq_key2",
                        "ranges": [
                          "NULL < key2 < 100000"
                        ],
                        "index_dives_for_eq_ranges": true,
                        "rowid_ordered": false,
                        "using_mrr": false,
                        "index_only": false,
                        "rows": 10000,
                        "cost": 12001,
                        "chosen": false,
                        "cause": "cost"
                      },
                      {
                        "index": "idx_key1",
                        "ranges": [
                          "z < key1"
                        ],
                        "index_dives_for_eq_ranges": true,
                        "rowid_ordered": false,
                        "using_mrr": false,
                        "index_only": false,
                        "rows": 1,
                        "cost": 2.21,
                        "chosen": true
                      },
                      {
                        "index": "idx_key3",
                        "ranges": [
                          "a <= key3 <= a",
                          "b <= key3 <= b",
                          "c <= key3 <= c"
                        ],
                        "index_dives_for_eq_ranges": true,
                        "rowid_ordered": false,
                        "using_mrr": false,
                        "index_only": false,
                        "rows": 3,
                        "cost": 6.61,
                        "chosen": false,
                        "cause": "cost"
                      }
                    ],
                    "analyzing_roworder_intersect": {
                      "usable": false,
                      "cause": "too_few_roworder_scans"
                    }
                  },
                  "chosen_range_access_summary": {
                    "range_access_plan": {
                      "type": "range_scan",
                      "index": "idx_key1",
                      "rows": 1,
                      "ranges": [
                        "z < key1"
                      ]
                    },
                    "rows_for_plan": 1,
                    "cost_for_plan": 2.21,
                    "chosen": true
                  }
                }
              }
            ]
          },
          {
            "considered_execution_plans": [
              {
                "plan_prefix": [
                ],
                "table": "`s1`",
                "best_access_path": {
                  "considered_access_paths": [
                    {
                      "rows_to_scan": 1,
                      "access_type": "range",
                      "range_details": {
                        "used_index": "idx_key1"
                      },
                      "resulting_rows": 1,
                      "cost": 2.41,
                      "chosen": true
                    }
                  ]
                },
                "condition_filtering_pct": 100,
                "rows_for_plan": 1,
                "cost_for_plan": 2.41,
                "chosen": true
              }
            ]
          },
          {
            "attaching_conditions_to_tables": {
              "original_condition": "((`s1`.`common_field` = 'abc') and (`s1`.`key1` > 'z') and (`s1`.`key2` < 100000) and (`s1`.`key3` in ('a','b','c')))",
              "attached_conditions_computation": [
              ],
              "attached_conditions_summary": [
                {
                  "table": "`s1`",
                  "attached": "((`s1`.`common_field` = 'abc') and (`s1`.`key1` > 'z') and (`s1`.`key2` < 100000) and (`s1`.`key3` in ('a','b','c')))"
                }
              ]
            }
          },
          {
            "refine_plan": [
              {
                "table": "`s1`",
                "pushed_index_condition": "(`s1`.`key1` > 'z')",
                "table_condition_attached": "((`s1`.`common_field` = 'abc') and (`s1`.`key2` < 100000) and (`s1`.`key3` in ('a','b','c')))"
              }
            ]
          }
        ]
      }
    },
    {
      "join_execution": {
        "select#": 1,
        "steps": [
        ]
      }
    }
  ]
}

参考