Mysql系列_高级_03_Explain详解

本质

Explain是Mysql的关键字,可以模拟优化器执行SQL语句,返回执行计划信息

explain中的列

id列
id列的编号是 select 的序列号,有几个 select 就有几个id,并且id的顺序是按 select 出现的顺序增长的。 id列越大执行优先级越高,id相同则从上往下执行,id为NULL最后执行。

select_type列
表示对应行是简单还是复杂查询

  • simple:简单查询。查询不包含子查询和union
  • primary:复杂查询中最外层的 select
  • subquery:包含在 select 中的子查询(不在 from 子句中)
  • derived:包含在 from 子句中的子查询。MySQL会将结果存放在一个临时表中,也称为派生表(derived的英文含 义)
  • union:在 union 中的第二个和随后的 select

table列

这一列表示 explain 的一行正在访问哪个表。

  • 当 from 子句中有子查询时,table列是 格式,表示当前查询依赖 id=N 的查询,于是先执行 id=N 的查 询。
  • 当有 union 时,UNION RESULT 的 table 列的值为,1和2表示参与 union 的 select 行id。

type列
这一列表示关联类型或访问类型,即MySQL决定如何查找表中的行,查找数据行记录的大概范围。依次从最优到最差分别为:system > const > eq_ref > ref > range > index > ALL,一般来说,得保证查询达到range级别,最好达到ref

  • NULL:mysql能够在优化阶段分解查询语句,在执行阶段用不着再访问表或索引。例如:在索引列中选取最小值,可 以单独查找索引来完成,不需要在执行时访问表
  • const, system:mysql能对查询的某部分进行优化并将其转化成一个常量(可以看show warnings 的结果)。用于 primary key 或 unique key 的所有列与常数比较时,所以表最多有一个匹配行,读取1次,速度比较快。system是 const的特例,表里只有一条元组匹配时为system
  • eq_ref:primary key 或 unique key 索引的所有部分被连接使用 ,最多只会返回一条符合条件的记录。这可能是在 const 之外最好的联接类型了,简单的 select 查询不会出现这种 type
  • ref:相比 eq_ref,不使用唯一索引,而是使用普通索引或者唯一性索引的部分前缀,索引要和某个值相比较,可能会找到多个符合条件的行
  • range:范围扫描通常出现在 in(), between ,> ,<, >= 等操作中。使用一个索引来检索给定范围的行
  • index:扫描全索引就能拿到结果,一般是扫描某个二级索引,这种扫描不会从索引树根节点开始快速查找,而是直接 对二级索引的叶子节点遍历和扫描,速度还是比较慢的,这种查询一般为使用覆盖索引,二级索引一般比较小,所以这 种通常比ALL快一些
  • ALL:即全表扫描,扫描你的聚簇索引的所有叶子节点。通常情况下这需要增加索引来进行优化了。

possible_keys列
这一列显示查询可能使用哪些索引来查找

key列
这一列显示mysql实际采用哪个索引来优化对该表的访问。explain 时可能出现 possible_keys 有列,而 key 显示 NULL 的情况,这种情况是因为表中数据不多,mysql认为索引 对此查询帮助不大,选择了全表查询。如果想强制mysql使用或忽视possible_keys列中的索引,在查询中使用 force index、ignore index

key_len列
这一列显示了mysql在索引里使用的字节数,通过这个值可以算出具体使用了索引中的哪些列。key_len计算规则如下:

①字符串,char(n)和varchar(n),5.0.3以后版本中,n均代表字符数,而不是字节数,如果是utf-8,一个数字 或字母占1个字节,一个汉字占3个字节

  • char(n):如果存汉字长度就是 3n 字节
  • varchar(n):如果存汉字则长度是 3n + 2 字节,加的2字节用来存储字符串长度,因为 varchar是变长字符串

②数值类型

  • tinyint:1字节
  • smallint:2字节
  • int:4字节
  • bigint:8字节

③时间类型

  • date:3字节
  • timestamp:4字节
  • datetime:8字节

④如果字段允许为 NULL,需要1字节记录是否为 NULL
索引最大长度是768字节,当字符串过长时,mysql会做一个类似左前缀索引的处理,将前半部分的字符提取出来做索引。

ref列
这一列显示了在key列记录的索引中,表查找值所用到的列或常量,常见的有:const(常量),字段名(例:film.id)

** rows列**
这一列是mysql估计要读取并检测的行数,注意这个不是结果集里的行数

** Extra列**
这一列展示的是额外信息。常见的重要值如下:
①Using index:使用覆盖索引
覆盖索引定义:mysql执行计划explain结果里的key有使用索引,如果select后面查询的字段都可以从这个索引的树中获取,这种情况一般可以说是用到了覆盖索引,extra里一般都有using index;覆盖索引一般针对的是辅助索引,整个 查询结果只通过辅助索引就能拿到结果,不需要通过辅助索引树找到主键,再通过主键去主键索引树里获取其它字段值

②Using where:使用 where 语句来处理结果,并且查询的列未被索引覆盖

③Using index condition:查询的列不完全被索引覆盖,where条件中是一个前导列的范围

④Using temporary:mysql需要创建一张临时表来处理查询。出现这种情况一般是要进行优化的,首先是想到用索 引来优化

#actor.name没有索引,此时创建了张临时表来distinct
 explain select distinct name from actor;

⑤Using filesort:将用外部排序而不是索引排序,数据较小时从内存排序,否则需要在磁盘完成排序。这种情况下一 般也是要考虑使用索引来优化的

#actor.name未创建索引,会浏览actor整个表,保存排序关键字name和对应的id,然后排序name并检索行记录
explain select * from actor order by name;

⑥Select tables optimized away:使用某些聚合函数(比如 max、min)来访问存在索引的某个字段

explain select min(id) from film;

Mysql的trace工具

本质
trace是mysql提供的用于查看mysql分析使用哪种查询策略的工具,但是开启trace工具会影响mysql性能,所以只能临时分析sql使用

set session optimizer_trace="enabled=on",end_markers_in_json=on; ‐‐开启trace
select * from employees where name > 'a' order by position;
SELECT * FROM information_schema.OPTIMIZER_TRACE;

{
  "steps": [
    {
      "join_preparation": {‐‐第一阶段:SQL准备阶段,格式化sql
        "select#": 1,
        "steps": [
          {
            "expanded_query": "/* select#1 */ select `employees`.`id` AS `id`,`employees`.`name` AS `name`,`employees`.`age` AS `age`,`employees`.`position` AS `position`,`employees`.`hire_time` AS `hire_time` from `employees` where (`employees`.`name` > 'a') order by `employees`.`position`"
          }
        ] /* steps */
      } /* join_preparation */
    },
    {
      "join_optimization": {‐‐第二阶段:SQL优化阶段
        "select#": 1,
        "steps": [
          {
            "condition_processing": {‐‐条件处理
              "condition": "WHERE",
              "original_condition": "(`employees`.`name` > 'a')",
              "steps": [
                {
                  "transformation": "equality_propagation",
                  "resulting_condition": "(`employees`.`name` > 'a')"
                },
                {
                  "transformation": "constant_propagation",
                  "resulting_condition": "(`employees`.`name` > 'a')"
                },
                {
                  "transformation": "trivial_condition_removal",
                  "resulting_condition": "(`employees`.`name` > 'a')"
                }
              ] /* steps */
            } /* condition_processing */
          },
          {
            "substitute_generated_columns": {
            } /* substitute_generated_columns */
          },
          {
            "table_dependencies": [ ‐‐表依赖详情
              {
                "table": "`employees`",
                "row_may_be_null": false,
                "map_bit": 0,
                "depends_on_map_bits": [
                ] /* depends_on_map_bits */
              }
            ] /* table_dependencies */
          },
          {
            "ref_optimizer_key_uses": [
            ] /* ref_optimizer_key_uses */
          },
          {
            "rows_estimation": [ ‐‐预估表的访问成本
              {
                "table": "`employees`",
                "range_analysis": {
                  "table_scan": { ‐‐全表扫描情况
                    "rows": 100075, ‐‐扫描行数
                    "cost": 10081.9 ‐‐查询成本
                  } /* table_scan */,
                  "potential_range_indexes": [  ‐‐查询可能使用的索引
                    {
                      "index": "PRIMARY", ‐‐主键索引
                      "usable": false,
                      "cause": "not_applicable"
                    },
                    {
                      "index": "idx_name_age_position",  ‐‐辅助索引
                      "usable": true,
                      "key_parts": [
                        "name",
                        "age",
                        "position",
                        "id"
                      ] /* key_parts */
                    }
                  ] /* potential_range_indexes */,
                  "setup_range_conditions": [
                  ] /* setup_range_conditions */,
                  "group_index_range": {
                    "chosen": false,
                    "cause": "not_group_by_or_distinct"
                  } /* group_index_range */,
                  "skip_scan_range": {
                    "potential_skip_scan_indexes": [
                      {
                        "index": "idx_name_age_position",
                        "usable": false,
                        "cause": "query_references_nonkey_column"
                      }
                    ] /* potential_skip_scan_indexes */
                  } /* skip_scan_range */,
                  "analyzing_range_alternatives": {  ‐‐分析各个索引使用成本
                    "range_scan_alternatives": [
                      {
                        "index": "idx_name_age_position",
                        "ranges": [ ‐‐索引使用范围
                          "a < name"
                        ] /* ranges */,
                        "index_dives_for_eq_ranges": true,
                        "rowid_ordered": false, ‐‐使用该索引获取的记录是否按照主键排序
                        "using_mrr": false, 
                        "index_only": false,  ‐‐是否使用覆盖索引
                        "rows": 50037,‐‐索引扫描行数
                        "cost": 17513.2,‐‐索引使用成本
                        "chosen": false, ‐‐是否选择该索引
                        "cause": "cost"
                      }
                    ] /* range_scan_alternatives */,
                    "analyzing_roworder_intersect": {
                      "usable": false,
                      "cause": "too_few_roworder_scans"
                    } /* analyzing_roworder_intersect */
                  } /* analyzing_range_alternatives */
                } /* range_analysis */
              }
            ] /* rows_estimation */
          },
          {
            "considered_execution_plans": [
              {
                "plan_prefix": [
                ] /* plan_prefix */,
                "table": "`employees`",
                "best_access_path": {‐‐最优访问路径
                  "considered_access_paths": [ ‐‐最终选择的访问路径
                    {
                      "rows_to_scan": 100075,
                      "access_type": "scan",‐‐访问类型:为scan,全表扫描
                      "resulting_rows": 100075,
                      "cost": 10079.8,
                      "chosen": true, ‐‐确定选择
                      "use_tmp_table": true
                    }
                  ] /* considered_access_paths */
                } /* best_access_path */,
                "condition_filtering_pct": 100,
                "rows_for_plan": 100075,
                "cost_for_plan": 10079.8,
                "sort_cost": 100075,
                "new_cost_for_plan": 110155,
                "chosen": true
              }
            ] /* considered_execution_plans */
          },
          {
            "attaching_conditions_to_tables": {
              "original_condition": "(`employees`.`name` > 'a')",
              "attached_conditions_computation": [
              ] /* attached_conditions_computation */,
              "attached_conditions_summary": [
                {
                  "table": "`employees`",
                  "attached": "(`employees`.`name` > 'a')"
                }
              ] /* attached_conditions_summary */
            } /* attaching_conditions_to_tables */
          },
          {
            "optimizing_distinct_group_by_order_by": {
              "simplifying_order_by": {
                "original_clause": "`employees`.`position`",
                "items": [
                  {
                    "item": "`employees`.`position`"
                  }
                ] /* items */,
                "resulting_clause_is_simple": true,
                "resulting_clause": "`employees`.`position`"
              } /* simplifying_order_by */
            } /* optimizing_distinct_group_by_order_by */
          },
          {
            "reconsidering_access_paths_for_index_ordering": {
              "clause": "ORDER BY",
              "steps": [
              ] /* steps */,
              "index_order_summary": {
                "table": "`employees`",
                "index_provides_order": false,
                "order_direction": "undefined",
                "index": "unknown",
                "plan_changed": false
              } /* index_order_summary */
            } /* reconsidering_access_paths_for_index_ordering */
          },
          {
            "finalizing_table_conditions": [
              {
                "table": "`employees`",
                "original_table_condition": "(`employees`.`name` > 'a')",
                "final_table_condition   ": "(`employees`.`name` > 'a')"
              }
            ] /* finalizing_table_conditions */
          },
          {
            "refine_plan": [
              {
                "table": "`employees`"
              }
            ] /* refine_plan */
          },
          {
            "considering_tmp_tables": [
              {
                "adding_sort_to_table": "employees"
              } /* filesort */
            ] /* considering_tmp_tables */
          }
        ] /* steps */
      } /* join_optimization */
    },
    {
      "join_execution": {‐‐第三阶段:SQL执行阶段
        "select#": 1,
        "steps": [
          {
            "sorting_table": "employees",
            "filesort_information": [
              {
                "direction": "asc",
                "expression": "`employees`.`position`"
              }
            ] /* filesort_information */,
            "filesort_priority_queue_optimization": {
              "usable": false,
              "cause": "not applicable (no LIMIT)"
            } /* filesort_priority_queue_optimization */,
            "filesort_execution": [
            ] /* filesort_execution */,
            "filesort_summary": {
              "memory_available": 262144,
              "key_size": 40,
              "row_size": 190,
              "max_rows_per_buffer": 1379,
              "num_rows_estimate": 100075,
              "num_rows_found": 100000,
              "num_initial_chunks_spilled_to_disk": 31,
              "peak_memory_used": 269496,
              "sort_algorithm": "std::stable_sort",
              "sort_mode": ""
            } /* filesort_summary */
          }
        ] /* steps */
      } /* join_execution */
    }
  ] /* steps */
}

结论:全表扫描的成本低于索引扫描,所以mysql最终选择全表扫描

set session optimizer_trace="enabled=off"; ‐‐关闭trace

索引最佳实践

CREATE TABLE `employees` (
	`id` int(11) NOT NULL AUTO_INCREMENT,
	`name` varchar(24) NOT NULL DEFAULT '' COMMENT '姓名',
	`age` int(11) NOT NULL DEFAULT '0' COMMENT '年龄',
	`position` varchar(20) NOT NULL DEFAULT '' COMMENT '职位',
	`hire_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入职时间',
	PRIMARY KEY (`id`),
	KEY `idx_name_age_position` USING BTREE (`name`, `age`, `position`)
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARSET = utf8 COMMENT '员工记录表';

drop procedure if exists insert_emp;
 delimiter ;;
  create procedure insert_emp()
	begin
	 declare i int;
	  set i=1;
		while(i<=100000)do
		insert into employees(name,age,position) values(CONCAT('zhuge',i),i,'dev');
		set i=i+1;
		end while;
	end;;
delimiter ;
		
call insert_emp();
where优化

①全值匹配:对于联合索引要尽量将索引包含的字段全部匹配上去执行查询语句

②最左前缀原则:对于联合索引查询要从索引的最左前列开始并且不跳过索引中的列

③不在索引列上做任何操作(计算、函数、(自动or手动)类型转换),会导致索引失效而转向全表扫描

④存储引擎不能使用索引中范围条件右边的列

⑤尽量使用覆盖索引(只访问索引的查询(索引列包含查询列)),减少 select * 语句

⑥mysql在使用不等于(!=或者<>),not in ,not exists 的时候无法使用索引会导致全表扫描 < 小于、 > 大于、 <=、>= 这些,mysql内部优化器会根据检索比例、表大小等多个因素整体评估是否使用索引

⑦is null,is not null 一般情况下也无法使用索引

⑧like以通配符开头(’%abc…’)且无法使用覆盖索引,mysql索引失效会变成全表扫描操作

⑨字符串不加单引号索引失效

⑩范围查询优化:单次数据量查询过大可能导致优化器最终选择不走索引,因此可以将大的范围拆分成多个小范围

order by和group by优化

①MySQL支持两种方式的排序filesort和index,Using index是指MySQL扫描索引本身完成排序。index 效率高,filesort效率低。

②order by使用索引最左前列或者与where子句组合满足最左前缀原则,从而让Mysql使用Using index进行排序

③group by和order by很类似,其实质是先排序后再分组,遵循索引创建顺序的最左前缀法则

④where高于having,能写在where的条件就不要写在having中

⑤不可以在order by中使用group by中不存在的字段,对于不需要排序的查询可以通过使用order by null禁止排序

Using filesort文件排序

①排序方式

  • 单路排序:是一次性取出满足条件行的所有字段,然后在sort buffer中进行排序;用trace工具可以看到sort_mode信息里显示< sort_key, additional_fields >或者< sort_key, packed_additional_fields >
  • 双路排序(又叫回表排序模式):是首先根据相应的条件取出相应的排序字段和可以直接定位行数据的行 ID,然后在 sort buffer 中进行排序,排序完后需要再次取回其它需要的字段;用trace工具 可以看到sort_mode信息里显示< sort_key, rowid >

②选择依据:MySQL 通过比较系统变量 max_length_for_sort_data(默认1024字节) 的大小和需要查询的字段总大小来 判断使用哪种排序模式

  • 如果 字段的总长度小于max_length_for_sort_data ,那么使用 单路排序模式
  • 如果 字段的总长度大于max_length_for_sort_data ,那么使用 双路排序模式
分页查询优化

①根据自增且连续的主键排序的分页查询

select * from employees limit 90000,5;
#优化后
select * from employees where id > 90000 limit 5;

②根据非主键字段排序的分页查询

select * from employees ORDER BY name limit 90000,5;
#优化后
select * from employees e inner join (select id from employees order by name limit 90000,5) ed on e.id = ed.id;
Join关联查询优化
‐‐ 示例表:
CREATE TABLE `t1` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `a` int(11) DEFAULT NULL,
 `b` int(11) DEFAULT NULL,
 PRIMARY KEY (`id`),
 KEY `idx_a` (`a`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
create table t2 like t1;

‐‐ 往t1表插入1万行记录
‐‐ 往t2表插入100行记录

mysql表关联常见算法:

①嵌套循环连接 Nested-Loop Join(NLJ) 算法
一次一行循环地从第一张表(称为驱动表)中读取行,在这行数据中取到关联字段,根据关联字段在另一张表(被驱动 表)里取出满足条件的行,然后取出两张表的结果合集。

EXPLAIN select * from t1 inner join t2 on t1.a= t2.a;

在这里插入图片描述
以上sql的执行流程:

  • 从表 t2 中读取一行数据(如果t2表有查询过滤条件的,会从过滤结果里取出一行数据);
  • 从第 1 步的数据中,取出关联字段 a,到表 t1 中查找;
  • 取出表 t1 中满足条件的行,跟 t2 中获取到的结果合并,作为结果返回给客户端;
  • 重复上面 3 步。

整个过程会读取 t2 表的所有数据(扫描100行),然后遍历这每行数据中字段 a 的值,根据 t2 表中 a 的值索引扫描 t1 表 中的对应行(扫描100次 t1 表的索引,1次扫描可以认为最终只扫描 t1 表一行完整数据,也就是总共 t1 表也扫描了100 行)。因此整个过程扫描了 200 行。

②基于块的嵌套循环连接 Block Nested-Loop Join(BNL)算法
把驱动表的数据读入到 join_buffer 中,然后扫描被驱动表,把被驱动表每一行取出来跟 join_buffer 中的数据做对比。

EXPLAIN select * from t1 inner join t2 on t1.b= t2.b;

在这里插入图片描述以上sql的执行流程:

  • 把 t2 的所有数据放入到 join_buffer 中
  • 把表 t1 中每一行取出来,跟 join_buffer 中的数据做对比
  • 返回满足 join 条件的数据

整个过程对表 t1 和 t2 都做了一次全表扫描,因此扫描的总行数为10000(表 t1 的数据总量) + 100(表 t2 的数据总量) = 10100。并且 join_buffer 里的数据是无序的,因此对表 t1 中的每一行,都要做 100 次判断,所以内存中的判断次数是 100 * 10000= 100 万次。

如果被驱动表的关联字段有索引,那么使用NLJ查询速度会快一点,如果被驱动表的关联没索引,那么使用BNL算法,以上例子中如果t1表的a字段没有索引然后使用NLJ算法,那么mysql将扫描t1表10000*100次,而且是磁盘扫描,而使用BNL算法,仅需要扫描t1表10000次磁盘扫描。

关联sql优化总结

  • 关联字段加索引,让mysql做join操作时尽量选择NLJ算法
  • 小表驱动大表,写多表连接sql时如果明确知道哪张表是小表可以用straight_join写法固定连接驱动方式,省去 mysql优化器自己判断的时间,但是一般情况下不需要使用straight_join,因为mysql优化引擎一般情况下比人靠谱

in和exists优化
原则:小表驱动大表,即小的数据集驱动大的数据集
in:当B表的数据集小于A表的数据集时,in优于exists

select * from A where id in (select id from B)

exists:当A表的数据集小于B表的数据集时,exists优于in
将主查询A的数据,放到子查询B中做条件验证,根据验证结果(true或false)来决定主查询的数据是否保留

select * from A where exists (select 1 from B where B.id = A.id)
count(*)查询优化

四种统计比较

①字段有索引:count(*)≈count(1)>count(字段)>count(主键 id) //字段有索引,count(字段)统计走二级索引,二 级索引存储数据比主键索引少,所以count(字段)>count(主键 id)

②字段无索引:count(*)≈count(1)>count(主键 id)>count(字段) //字段没有索引count(字段)统计走不了索引, count(主键 id)还可以走主键索引,所以count(主键 id)>count(字段)

③原因:count(1)跟count(字段)执行过程类似,不过count(1)不需要取出字段统计,就用常量1做统计,count(字段)还需要取出 字段,所以理论上count(1)比count(字段)会快一点。 count() 是例外,mysql并不会把全部字段取出来,而是专门做了优化,不取值,按行累加,效率很高,所以不需要用 count(列名)或count(常量)来替代 count()。

常见优化方法

①对于myisam存储引擎的表做不带where条件的count查询性能是很高的,因为myisam存储引擎的表的总行数会被 mysql存储在磁盘上,查询不需要计算,对于innodb存储引擎的表mysql不会存储表的总记录行数(因为有MVCC机制,后面会讲),查询count需要实时计算

②如果只需要知道表总行数的估计值可以用show table status查询,性能很高

show table status like 'employees'

③增加数据库计数表
插入或删除表数据行的时候同时维护计数表,让他们在同一个事务里操作

你可能感兴趣的:(Mysql系列,mysql,数据库,sql)