SQL运行内幕:从执行原理看调优的本质

相信你们看过无数的MySQL调优经验贴了,会告诉你各类调优手段,如:html

  • 避免 select *;
  • join字段走索引;
  • 慎用in和not in,用exists取代in;
  • 避免在where子句中对字段进行函数操做;
  • 尽可能避免更新汇集索引;
  • group by若是不须要排序,手动加上 order by null;
  • join选择小表做为驱动表;
  • order by字段尽可能走索引...

其中有些手段也许跟随者MySQL版本的升级过期了。咱们真的须要背这些调优手段吗?我以为是没有必要的,在掌握MySQL存储架构SQL执行原理的状况下,咱们就很天然的明白,为何要提议这么优化了,甚至可以发现别人提的不太合理的优化手段。java

洞悉MySQL底层架构:游走在缓冲与磁盘之间 这篇文章中,咱们已经介绍了MySQL的存储架构,详细对你在MySQL存储索引缓冲IO相关的调优经验中有了必定的其实。mysql

本文,咱们重点讲解经常使用的SQL的执行原理,从执行原理,以及MySQL内部对SQL的优化机制,来分析SQL要如何调优,理解为何要这样...那样...那样...调优。算法

image-20200626112814627

若是没有特别说明,本文以MySQL5.7版本做为讲解和演示。sql

阅读完本文,您将了解到:数据库

  • COUNT: MyISAM和InnoDB存储引擎处理count的区别是什么?
  • COUNT: count为什么性能差?
  • COUNT: count有哪些书写方式,怎么count统计会快点?
  • ORDER BY: order by语句有哪些排序模式,以及每种排序模式的优缺点?
  • ORDER BY: order by语句会用到哪些排序算法,在什么场景下会选择哪一种排序算法
  • ORDER BY: 如何查看和分析sql的order by优化手段(执行计划 + OPTIMIZER_TRACE日志)
  • ORDER BY: 如何优化order by语句的执行效率?(思想:减少行查询大小,尽可能走索引,可以走覆盖索引最佳,可适当增长sort buffer内存大小)
  • JOIN: join走索引的状况下是如何执行的?
  • JOIN: join不走索引的状况下是如何执行的?
  • JOIN: MySQL对Index Nested-Loop Join作了什么优化?(MMR,BKA)
  • JOIN: BNL算法对缓存会产生什么影响?有什么优化策略?
  • JOIN: 有哪些经常使用的join语句?
  • JOIN: 针对join语句,有哪些优化手段?
  • UNION: union语句执行原理是怎样的?
  • UNION: union是如何去重的?
  • GROUP BY: group by彻底走索引的状况下执行计划如何?
  • GROUP BY: 什么状况下group by会用到临时表?什么状况下会用到临时表+排序?
  • GROUP BY: 对group by有什么优化建议?
  • DISTINCT: distinct关键词执行原理是什么?
  • 子查询: 有哪些常见的子查询使用方式?
  • 子查询: 常见的子查询优化有哪些?
  • 子查询: 真的要尽可能使用关联查询取代子查询吗?
  • **子查询:**in 的效率真的这么慢吗?
  • 子查询: MySQL 5.6以后对子查询作了哪些优化?(SEMIJOIN,Materializatioin,Exists优化策略)
  • 子查询: Semijoin有哪些优化策略,其中Materializatioin策略有什么执行方式,为什么要有这两种执行方式?
  • 子查询: 除了in转Exists这种优化优化,MariaDB中的exists转in优化措施有什么做用?

image-20200626122041744

一、count

存储引擎的区别编程

  • MyISAM引擎每张表中存放了一个meta信息,里面包含了row_count属性,内存和文件中各有一份,内存的count变量值经过读取文件中的count值来进行初始化。[1]可是若是带有where条件,仍是必须得进行表扫描json

  • InnoDB引擎执行count()的时候,须要把数据一行行从引擎里面取出来进行统计。后端

下面咱们介绍InnoDB中的count()。数组

count中的一致性视图

InnoDB中为什么不像MyISAM那样维护一个row_count变量呢?

前面 洞悉MySQL底层架构:游走在缓冲与磁盘之间 一文咱们了解到,InnoDB为了实现事务,是须要MVCC支持的。MVCC的关键是一致性视图。一个事务开启瞬间,全部活跃的事务(未提交)构成了一个视图数组,InnoDB就是经过这个视图数组来判断行数据是否须要undo到指定的版本。

以下图,假设执行count的时候,一致性视图获得当前事务可以取到的最大事务ID DATA_TRX_ID=1002,那么行记录中事务ID超过1002都都要经过undo log进行版本回退,最终才能得出最终哪些行记录是当前事务须要统计的:

image-20200607161751139

row1是其余事务新插入的记录,当前事务不该该算进去。因此最终得出,当前事务应该统计row2,row3。

执行count会影响其余页面buffer pool的命中率吗?

咱们知道buffer pool中的LRU算法是是通过改进的,默认状况下,旧子列表(old区)占3/8,count加载的页面一直往旧子列表中插入,在旧子列表中淘汰,不会晋升到新子列表中。因此不会影响其余页面buffer pool的命中率。

count(主键)

count(主键)执行流程以下:

  • 执行器请求存储引擎获取数据;
  • 为了保证扫描数据量更少,存储引擎找到最小的那颗索引树获取全部记录,返回记录的id给到server。返回记录以前会进行MVCC及其可见性的判断,只返回当前事务可见的数据;
  • server获取到记录以后,判断id若是不为空,则累加到结果记录中。

image-20200607165008486

count(1)

count(1)与count(主键)执行流程基本一致,区别在于,针对查询出的每一条记录,不会取记录中的值,而是**直接返回一个"1"**用于统计累加。统计了全部的行。

count(字段)

与count(主键)相似,会筛选非空的字段进行统计。若是字段没有添加索引,那么会扫描汇集索引树,致使扫描的数据页会比较多,效率相对慢点

count(*)

count(*)不会取记录的值,与count(1)相似。

执行效率对比:count(字段) < count(主键) < count(1)

二、order by

如下是咱们本节做为演示例子的表,假设咱们有以下表:

image-20200613115722738

索引以下:

image-20200610222405107

对应的idx_d索引结构以下(这里咱们作了一些夸张的手法,让一个页数据变小,为了展示在索引树中的查找流程):

image-20200614201329128

2.一、如何跟踪执行优化

为了方便分析sql的执行流程,咱们能够在当前session中开启 optimizer_trace:

SET optimizer_trace='enabled=on';

而后执行sql,执行完以后,就能够经过如下堆栈信息查看执行详情了:

SELECT * FROM information_schema.OPTIMIZER_TRACE\G;

如下是

select a, b, c, d from t20 force index(idx_abc)  where a=3 order by d limit 100,2;
复制代码

的执行结果,其中符合a=3的有8457条记录,针对order by重点关注如下属性

"filesort_priority_queue_optimization": {  // 是否启用优先级队列
  "limit": 102,           // 排序后须要取的行数,这里为 limit 100,2,也就是100+2=102
  "rows_estimate": 24576, // 估计参与排序的行数
  "row_size": 123,        // 行大小
  "memory_available": 32768,    // 可用内存大小,即设置的sort buffer大小
  "chosen": true          // 是否启用优先级队列
},
...
"filesort_summary": {
  "rows": 103,                // 排序过程当中会持有的行数
  "examined_rows": 8457,      // 参与排序的行数,InnoDB层返回的行数
  "number_of_tmp_files": 0,   // 外部排序时,使用的临时文件数量
  "sort_buffer_size": 13496,  // 内存排序使用的内存大小
  "sort_mode": "sort_key, additional_fields"  // 排序模式
}
复制代码

2.1.一、排序模式

其中 sort_mode有以下几种形式:

  • sort_key, rowid:代表排序缓冲区元组包含排序键值和原始表行的行id,排序后须要使用行id进行回表,这种算法也称为original filesort algorithm(回表排序算法);
  • sort_key, additional_fields:代表排序缓冲区元组包含排序键值和查询所须要的列,排序后直接从缓冲区元组取数据,无需回表,这种算法也称为modified filesort algorithm(不回表排序);
  • sort_key, packed_additional_fields:相似上一种形式,可是附加的列(如varchar类型)紧密地打包在一块儿,而不是使用固定长度的编码。

如何选择排序模式

选择哪一种排序模式,与max_length_for_sort_data这个属性有关,这个属性默认值大小为1024字节:

  • 若是查询列和排序列占用的大小超过这个值,那么会转而使用sort_key, rowid模式;
  • 若是不超过,那么全部列都会放入sort buffer中,使用sort_key, additional_fields或者sort_key, packed_additional_fields模式;
  • 若是查询的记录太多,那么会使用sort_key, packed_additional_fields对可变列进行压缩。

2.1.二、排序算法

基于参与排序的数据量的不一样,能够选择不一样的排序算法:

  • 若是排序取的结果很小,小于内存,那么会使用优先级队列进行堆排序;

    • 例如,如下只取了前面10条记录,会经过优先级队列进行排序:

    • select a, b, c, d from t20 force index(idx_abc)  where a=3 order by d limit 10;
      复制代码
  • 若是排序limit n, m,n太大了,也就是说须要取排序很后面的数据,那么会使用sort buffer进行快速排序

    • 以下,表中a=1的数据又三条,可是因为须要limit到很后面的记录,MySQL会对比优先级队列排序和快速排序的开销,选择一个比较合适的排序算法,这里最终放弃了优先级队列,转而使用sort buffer进行快速排序:

    • select a, b, c, d from t20 force index(idx_abc)  where a=1 order by d limit 300,2;
      复制代码
  • 若是参与排序的数据sort buffer装不下了,那么咱们会一批一批的给sort buffer进行内存快速排序,结果放入排序临时文件,最终使对全部排好序的临时文件进行归并排序,获得最终的结果;

    • 以下,a=3的记录超过了sort buffer,咱们要查找的数据是排序后1000行起,sort buffer装不下1000行数据了,最终MySQL选择使用sort buffer进行分批快排,把最终结果进行归并排序:

    • select a, b, c, d from t20 force index(idx_abc)  where a=3 order by d limit 1000,10;
      复制代码

2.二、order by走索引避免排序

执行以下sql:

select a, b, c, d from t20 force index(idx_d) where d like 't%' order by d limit 2;
复制代码

咱们看一下执行计划:

image-20200609222820565

发现Extra列为:Using index condition,也就是这里只走了索引。

执行流程以下图所示:

经过idx_d索引进行range_scan查找,扫描到4条记录,而后order by继续走索引,已经排好序,直接取前面两条,而后去汇集索引查询完整记录,返回最终须要的字段做为查询结果。这个过程只须要借助索引。

image-20200610225145415

如何查看和修改sort buffer大小?

咱们看一下当前的sort buffer大小:

image-20200610225943761

能够发现,这里默认配置了sort buffer大小为512k。

咱们能够设置这个属性的大小:

SET GLOBAL sort_buffer_size = 32*1024;

或者

SET sort_buffer_size = 32*1024;

下面咱们统一把sort buffer设置为32k

SET sort_buffer_size = 32*1024; 
复制代码

2.三、排序算法案例

2.3.一、使用优先级队列进行堆排序

若是排序取的结果很小,而且小于sort buffer,那么会使用优先级队列进行堆排序;

例如,如下只取了前面10条记录:

select a, b, c, d from t20 force index(idx_abc) where a=3 order by d limit 10;
复制代码

a=3的总记录数:8520。查看执行计划:

image-20200614155911344

发现这里where条件用到了索引,order by limit用到了排序。咱们进一步看看执行的optimizer_trace日志:

"filesort_priority_queue_optimization": {
  "limit": 10,
  "rows_estimate": 27033,
  "row_size": 123,
  "memory_available": 32768,
  "chosen": true  // 使用优先级队列进行排序
},
"filesort_execution": [
],
"filesort_summary": {
  "rows": 11,
  "examined_rows": 8520,
  "number_of_tmp_files": 0,
  "sort_buffer_size": 1448,
  "sort_mode": "sort_key, additional_fields"
}
复制代码

发现这里是用到了优先级队列进行排序。排序模式是:sort_key, additional_fields,即先回表查询完整记录,把排序须要查找的全部字段都放入sort buffer进行排序。

因此这个执行流程以下图所示:

  1. 经过where条件a=3扫描到8520条记录;
  2. 回表查找记录;
  3. 把8520条记录中须要的字段放入sort buffer中;
  4. 在sort buffer中进行堆排序;
  5. 在排序好的结果中取limit 10前10条,写入net buffer,准备发送给客户端。

image-20200614164934844

2.3.二、内部快速排序

若是排序limit n, m,n太大了,也就是说须要取排序很后面的数据,那么会使用sort buffer进行快速排序。MySQL会对比优先级队列排序和归并排序的开销,选择一个比较合适的排序算法。

如何衡量到底是使用优先级队列仍是内存快速排序? 通常来讲,快速排序算法效率高于堆排序,可是堆排序实现的优先级队列,无需排序完全部的元素,就能够获得order by limit的结果。 MySQL源码中声明了快速排序速度是堆排序的3倍,在实际排序的时候,会根据待排序数量大小进行切换算法。若是数据量太大的时候,会转而使用快速排序。

有以下SQL:

select a, b, c, d from t20 force index(idx_abc)  where a=1 order by d limit 300,2;
复制代码

咱们把sort buffer设置为32k:

SET sort_buffer_size = 32*1024; 
复制代码

其中a=1的记录有3条。查看执行计划:

image-20200614165900455

能够发现,这里where条件用到了索引,order by limit 用到了排序。咱们进一步看看执行的optimizer_trace日志:

"filesort_priority_queue_optimization": {
  "limit": 302,
  "rows_estimate": 27033,
  "row_size": 123,
  "memory_available": 32768,
  "strip_additional_fields": {
    "row_size": 57,
    "sort_merge_cost": 33783,
    "priority_queue_cost": 61158,
    "chosen": false  // 对比发现快速排序开销成本比优先级队列更低,这里不适用优先级队列
  }
},
"filesort_execution": [
],
"filesort_summary": {
  "rows": 3,
  "examined_rows": 3,
  "number_of_tmp_files": 0,
  "sort_buffer_size": 32720,
  "sort_mode": "<sort_key, packed_additional_fields>"
}
复制代码

能够发现这里最终放弃了优先级队列,转而使用sort buffer进行快速排序。

因此这个执行流程以下图所示:

  1. 经过where条件a=1扫描到3条记录;
  2. 回表查找记录;
  3. 把3条记录中须要的字段放入sort buffer中;
  4. 在sort buffer中进行快速排序
  5. 在排序好的结果中取limit 300, 2第300、301条记录,写入net buffer,准备发送给客户端。

image-20200614170720664

2.3.三、外部归并排序

当参与排序的数据太多,一次性放不进去sort buffer的时候,那么咱们会一批一批的给sort buffer进行内存排序,结果放入排序临时文件,最终使对全部排好序的临时文件进行归并排序,获得最终的结果。

有以下sql:

select a, b, c, d from t20 force index(idx_abc) where a=3 order by d limit 1000,10;
复制代码

其中a=3的记录有8520条。执行计划以下:

image-20200614171147989

能够发现,这里where用到了索引,order by limit用到了排序。进一步查看执行的optimizer_trace日志:

"filesort_priority_queue_optimization": {
  "limit": 1010,
  "rows_estimate": 27033,
  "row_size": 123,
  "memory_available": 32768,
  "strip_additional_fields": {
    "row_size": 57,
    "chosen": false,
    "cause": "not_enough_space"  // sort buffer空间不够,没法使用优先级队列进行排序了
  }
},
"filesort_execution": [
],
"filesort_summary": {
  "rows": 8520,
  "examined_rows": 8520,
  "number_of_tmp_files": 24,  // 用到了24个外部文件进行排序
  "sort_buffer_size": 32720,
  "sort_mode": "<sort_key, packed_additional_fields>"
}
复制代码

咱们能够看到,因为limit 1000,要返回排序后1000行之后的记录,显然sort buffer已经不能支撑这么大的优先级队列了,因此转而使用sort buffer内存排序,而这里须要在sort buffer中分批执行快速排序,获得多个排序好的外部临时文件,最终执行归并排序。(外部临时文件的位置由tmpdir参数指定)

其流程以下图所示:

image-20200614174511131

2.四、排序模式案例

2.4.一、sort_key, additional_fields模式

sort_key, additional_fields,排序缓冲区元组包含排序键值和查询所须要的列(先回表取须要的数据,存入排序缓冲区中),排序后直接从缓冲区元组取数据,无需再次回表。

上面 2.3.一、2.3.2节的例子都是这种排序模式,就不继续举例了。

2.4.二、<sort_key, packed_additional_fields>模式

sort_key, packed_additional_fields:相似上一种形式,可是附加的列(如varchar类型)紧密地打包在一块儿,而不是使用固定长度的编码。

上面2.3.3节的例子就是这种排序模式,因为参与排序的总记录大小太大了,所以须要对附加列进行紧密地打包操做,以节省内存。

2.4.三、<sort_key, rowid>模式

前面咱们提到,选择哪一种排序模式,与max_length_for_sort_data[2]这个属性有关,max_length_for_sort_data规定了排序行的最大大小,这个属性默认值大小为1024字节:

image-20200614184450403

也就是说若是查询列和排序列占用的大小小于这个值,这个时候会走sort_key, additional_fields或者sort_key, packed_additional_fields算法,不然,那么会转而使用sort_key, rowid模式。

如今咱们特地把这个值设置小一点,模拟sort_key, rowid模式:

SET max_length_for_sort_data = 100;
复制代码

这个时候执行sql:

select a, b, c, d from t20 force index(idx_abc) where a=3 order by d limit 10;
复制代码

这个时候再查看sql执行的optimizer_trace日志:

"filesort_priority_queue_optimization": {
  "limit": 10,
  "rows_estimate": 27033,
  "row_size": 49,
  "memory_available": 32768,
  "chosen": true
},
"filesort_execution": [
],
"filesort_summary": {
  "rows": 11,
  "examined_rows": 8520,
  "number_of_tmp_files": 0,
  "sort_buffer_size": 632,
  "sort_mode": "<sort_key, rowid>"
}
复制代码

能够发现这个时候切换到了sort_key, rowid模式,在这个模式下,执行流程以下:

  1. where条件a=3扫描到8520条记录;
  2. 回表查找记录;
  3. 找到这8520条记录的idd字段,放入sort buffer中进行堆排序;
  4. 排序完成后,取前面10条;
  5. 取这10条的id回表查询须要的a,b,c,d字段值;
  6. 依次返回结果给到客户端。

image-20200614191000922

能够发现,正由于行记录太大了,因此sort buffer中只存了须要排序的字段和主键id,以时间换取空间,最终排序完成,再次从汇集索引中查找到全部须要的字段返回给客户端,很明显,这里多了一次回表操做的磁盘读,总体效率上是稍微低一点的。

2.五、order by优化总结

根据以上的介绍,咱们能够总结出如下的order by语句的相关优化手段:

  • order by字段尽可能使用固定长度的字段类型,由于排序字段不支持压缩;
  • order by字段若是须要用可变长度,应尽可能控制长度,道理同上;
  • 查询中尽可能不用用select *,避免查询过多,致使order by的时候sort buffer内存不够致使外部排序,或者行大小超过了max_length_for_sort_data致使走了sort_key, rowid排序模式,使得产生了更多的磁盘读,影响性能;
  • 尝试给排序字段和相关条件加上联合索引,可以用到覆盖索引最佳。

三、join

为了演示join,接下来咱们须要用到这两个表:

CREATE TABLE `t30` ( 
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `a` int(11) NOT NULL,
  `b` int(11) NOT NULL,
  `c` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  KEY idx_a(a)
) ENGINE=InnoDB CHARSET=utf8mb4;

CREATE TABLE `t31` ( 
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `a` int(11) NOT NULL,
  `f` int(11) NOT NULL,
  `c` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  KEY idx_a(a)
) ENGINE=InnoDB CHARSET=utf8mb4;

insert into t30(a,b,c) values(1, 1, 1),(12,2,2),(3,3,3),(11, 12, 31),(15,1,32),(33,33,43),(5,13,14),(4,13,14),(16,13,14),(10,13,14);

insert into t31(a,f,c) values(1, 1, 1),(21,2,2),(3,3,3),(12, 1, 1),(31,20,2),(4,10,3),(2,23,24),(22,23,24),(5,23,24),(20,23,24);
复制代码

在MySQL官方文档中 8.8.2 EXPLAIN Output Format[3] 提到:MySQL使用Nested-Loop Loin算法处理全部的关联查询。使用这种算法,意味着这种执行模式:

  • 从第一个表中读取一行,而后在第二个表、第三个表...中找到匹配的行,以此类推;
  • 处理完全部关联的表后,MySQL将输出选定的列,若是列不在当前关联的索引树中,那么会进行回表查找完整记录;
  • 继续遍历,从表中取出下一行,重复以上步骤。

下面咱们所讲到的都是Nested-Loop Join算法的不一样实现。

**多表join:**无论多少个表join,都是用的Nested-Loop Join实现的。若是有第三个join的表,那么会把前两个表的join结果集做为循环基础数据,在执行一次Nested-Loop Join,到第三个表中匹配数据,更多多表同理。

3.一、join走索引(Index Nested-Loop Join)

3.1.一、Index Nested-Loop Join

咱们执行如下sql:

select * from t30 straight_join t31 on t30.a=t31.a;
复制代码

查看执行计划:

image-20200620112938626

能够发现:

  • t30做为驱动表,t31做为被驱动表;
  • 经过a字段关联,去t31表查找数据的时候用到了索引。

该sql语句的执行流程以下图:

  1. 首先遍历t30汇集索引;
  2. 针对每一个t30的记录,找到a的值,去t31的idx_a索引中找是否存在记录;
  3. 若是存在则拿到t30对应索引记录的id回表查找完整记录;
  4. 分别取t30和t31的全部字段做为结果返回。

image-20200620134012986

因为这个过程当中用到了idx_a索引,因此这种算法也称为:Index Nested-Loop(索引嵌套循环join)。其伪代码结构以下:

// A 为t30汇集索引
// B 为t31汇集索引
// BIndex 为t31 idx_a索引
void indexNestedLoopJoin(){
  List result;
  for(a in A) {
    for(bi in BIndex) {
      if (a satisfy condition bi) {
        output <a, b>;
      }
    }
  }
}
复制代码

假设t30记录数为m,t31记录数为n,每一次查找索引树的复杂度为log2(n),因此以上场景,总的复杂度为:m + m*2*log2(n)

也就是说驱动表越小,复杂度越低,越能提升搜索效率。

3.1.二、Index nested-Loop Join的优化

咱们能够发现,以上流程,每次从驱动表取一条数据,而后去被驱动表关联取数,表现为磁盘的随记读,效率是比较低低,有没有优化的方法呢?

这个就得从MySQL的MRR(Multi-Range Read)[4]优化机制提及了。

3.1.2.一、Multi-Range Read优化

咱们执行如下代码,强制开启MMR功能:

set optimizer_switch="mrr_cost_based=off"
复制代码

而后执行如下SQL,其中a是索引:

select * from t30 force index(idx_a) where a<=12 limit 10;
复制代码

能够获得以下执行计划:

image-20200620125153026

能够发现,Extra列提示用到了MRR优化。

这里为了演示走索引的场景,因此加了force index关键词。

正常不加force index的状况下,MySQL优化器会检查到这里即便走了索引仍是须要回表查询,而且表中的数据量很少,那干脆就直接扫描全表,不走索引,效率更加高了。

若是没有MRR优化,那么流程是这样的:

  1. 在idx_a索引中找到a<10的记录;
  2. 取前面10条,拿着id去回表查找完整记录,这里回表查询是随机读,效率较差
  3. 取到的结果经过net buffer返回给客户端。

image-20200620155426146

使用了MRR优化以后,这个执行流程是这样的:

  1. 在idx_abc索引中找到a<10的记录;
  2. 取10条,把id放入read rnd buffer;
  3. read rnd buffer中的id排序;
  4. 排序以后回表查询完整记录,id越多,排好序以后越有可能产生连续的id,去磁盘顺序读;
  5. 查询结果写入net buffer返回给客户端;

image-20200620163852564

3.1.2.二、Batched Key Access

与Multi-Range Read的优化思路相似,MySQL也是经过把随机读改成顺序读,让Index Nested-Loop Join提高查询效率,这个算法称为Batched Key Access(BKA)[5]算法。

咱们知道,默认状况下,是扫描驱动表,一行一行的去被驱动表匹配记录。这样就没法触发MRR优化了,为了可以触发MRR,因而BKA算法登场了。

在BKA算法中,驱动表经过使用join buffer批量在被驱动表辅助索引中关联匹配数据,获得一批结果,一次性传递个数据库引擎的MRR接口,从而能够利用到MRR对磁盘读的优化。

为了启用这个算法,咱们执行如下命令(BKA依赖于MRR):

set optimizer_switch='mrr=on,mrr_cost_based=off,batched_key_access=on';
复制代码

咱们再次执行如下关联查询sql:

select * from t30 straight_join t31 on t30.a=t31.a;
复制代码

咱们能够获得以下的执行计划:

image-20200620163156095

能够发现,这里用到了:Using join buffer(Batched Key Access)

执行流程以下:

  1. 把驱动表的数据批量放入join buffer中;
  2. 在join buffer中批与被驱动表的辅助索引匹配结果,获得一个结果集;
  3. 把上一步的结果集批量提交给引擎的MRR接口;
  4. MRR接口处理同上一节,主要进行了磁盘顺序读的优化;
  5. 组合输出最终结果,能够看到,这里的结果与没有开启BKA优化的顺序有所不一样,这里使用了t31被驱动表的id排序做为输出顺序,由于最后一步对被驱动表t31读取进行MRR优化的时候作了排序。

image-20200620175943902

若是join条件没走索引,又会是什么状况呢,接下来咱们尝试执行下对应的sql。

3.二、join不走索引(Block Nested-Loop Join)

3.2.一、Block Nested-Loop Join (BNL)

咱们执行如下sql:

select * from t30 straight_join t31 on t30.c=t31.c;
复制代码

查看执行计划:

image-20200620182810300

能够发现:

  • t30做为驱动表,t31做为被驱动表;
  • 经过c字段关联,去t31表查找数据的时候没有用到索引;
  • join的过程当中用到了join buffer,这里提示用到了Block Nested Loop Join;

该语句的执行流程以下图:

  1. t30驱动表中的数据分批(分块)存入join buffer,若是一次能够所有存入,则这里会一次性存入;
  2. t31被驱动表中扫描记录,依次取出与join buffer中的记录对比(内存中对比,快),判断是否知足c相等的条件;
  3. 知足条件的记录合并结果输出到net buffer中,最终传输给客户端。

而后清空join buffer,存入下一批t30的数据,重复以上流程。

image-20200620185110428

显然,每批数据都须要扫描一遍被驱动表,批次越多,扫描越多,可是内存判断总次数是不变的。因此总批次越小,越高效。因此,跟上一个算法同样,驱动表越小,复杂度越低,越能提升搜索效率。

3.2.二、BNL问题

洞悉MySQL底层架构:游走在缓冲与磁盘之间 一文中,咱们介绍了MySQL Buffer Pool的LRU算法,以下:

image-20200519225450188

默认状况下,同一个数据页,在一秒钟以后再次访问,那么就会晋升到新子列表(young区)。

恰巧,若是咱们用到了BNL算法,那么分批执行的话,就会重复扫描被驱动表去匹配每个批次了。

考虑如下两种会影响buffer pool的场景:

  • 若是这个时候join扫描了一个很大的冷表,那么在join这段期间,会持续的往旧子列表(old区)写数据页,淘汰队尾的数据页,这会影响其余业务数据页晋升到新子列表,由于极可能在一秒内,其余业务数据就从旧子列表中被淘汰掉了;
  • 而若是这个时候BNL算法把驱动表分为了多个批次,每一个批次扫描匹配被驱动表,都超过1秒钟,那么这个时候,被驱动表的数据页就会被晋升到新子列表,这个时候也会把其余业务的数据页提早重新子列表中淘汰掉。

3.2.三、BNL问题解决方案

3.2.3.一、调大 join_buffer_size

针对以上这种场景,为了不影响buffer pool,最直接的办法就是增长join_buffer_size的值,以减小对被驱动表的扫描次数。

3.2.3.二、把BNL转换为BKA

咱们能够经过把join的条件加上索引,从而避免了BNL算法,转而使用BKA算法,这样也能够加快记录的匹配速度,以及从磁盘读取被驱动表记录的速度。

3.2.3.三、经过添加临时表

有时候,被驱动表很大,可是关联查询又不多使用,直接给关联字段加索引太浪费空间了,这个时候就能够经过把被驱动表的数据放入临时表,在零时表中添加索引的方式,以达成3.2.3.2的优化效果。

3.2.3.四、使用hash join

什么是hash join呢,简单来讲就是这样的一种模型:

把驱动表知足条件的数据取出来,放入一个hash结构中,而后把被驱动表知足条件的数据取出来,一行一行的去hash结构中寻找匹配的数据,依次找到知足条件的全部记录。

通常状况下,MySQL的join实现都是以上介绍的各类nested-loop算法的实现,可是从MySQL 8.0.18[6]开始,咱们可使用hash join来实现表连续查询了。感兴趣能够进一步阅读这篇文章进行了解:[Hash join in MySQL 8 | MySQL Server Blog](mysqlserverteam.com/hash-join-i… only supports inner hash,more often than it does.)

3.三、各类join

咱们在平时工做中,会遇到各类各样的join语句,主要有以下:

INNER JOIN

image-20200621121200860

LEFT JOIN

image-20200621121223213

RIGHT JOIN

image-20200621121238746

FULL OUTER JOIN

image-20200621121307287

LEFT JOIN EXCLUDING INNER JOIN

image-20200621121332845

RIGHT JOIN EXCLUDING INNER JOIN

image-20200621121348116

OUTER JOIN EXCLUDING INNER JOIN

image-20200621120730459

更详细的介绍,能够参考:

3.三、join使用总结

  • join优化的目标是尽量减小join中Nested-Loop的循环次数,因此请让小表作驱动表;
  • 关联字段尽可能走索引,这样就能够用到Index Nested-Loop Join了;
  • 若是有order by,请使用驱动表的字段做为order by,不然会使用 using temporary;
  • 若是不可避免要用到BNL算法,为了减小被驱动表屡次扫描致使的对Buffer Pool利用率的影响,那么能够尝试把 join_buffer_size调大;
  • 为了进一步加快BNL算法的执行效率,咱们能够给关联条件加上索引,转换为BKA算法;若是加索引成本较高,那么能够经过临时表添加索引来实现;
  • 若是您使用的是MySQL 8.0.18,能够尝试使用hash join,若是是较低版本,也能够本身在程序中实现一个hash join。

四、union

经过使用union能够把两个查询结果合并起来,注意:

union all不会去除重复的行,union则会去除重复读的行。

4.一、union all

执行下面sql:

(select id from t30 order by id desc limit 10) union all (select c from t31 order by id desc limit 10)
复制代码

该sql执行计划以下图:

image-20200621231412385

执行流程以下:

  1. 从t30表查询出结果,直接写出到net buffer,传回给客户端;
  2. 从331表查询出结果,直接写出到net buffer,传回给客户端。

image-20200621232801276

4.二、union

执行下面sql:

(select id from t30 order by id desc limit 10) union (select c from t31 order by id desc limit 10)
复制代码

该sql执行计划以下图:

image-20200621233005902

执行流程以下:

  1. 从t30查询出记录,写入到临时表;
  2. 从t30查询出记录,写入临时表,在临时表中经过惟一索引去重;
  3. 把临时表的数据经过net buffer返回给客户端。

image-20200621233853780

五、group by

5.一、彻底走索引

咱们给t30加一个索引:

alter table t30 add index idx_c(c);
复制代码

执行如下group bysql:

select c, count(*) from t30 group by c;
复制代码

执行计划以下:

image-20200622205429403

发现这里只用到了索引,缘由是idx_c索引自己就是按照c排序好的,那么直接顺序扫描idx_c索引,能够直接统计到每个c值有多少条记录,无需作其余的统计了。

5.二、临时表

如今咱们把刚刚的idx_c索引给删掉,执行如下sql:

select c, count(*) from t30 group by c order by null;
复制代码

为了不排序,因此咱们这里添加了 order by null,表示不排序。

执行计划以下:

image-20200622205812372

能够发现,这里用到了内存临时表。其执行流程以下:

  1. 扫描t30汇集索引;
  2. 创建一个临时表,以字段c为主键,依次把扫描t30的记录经过临时表的字段c进行累加;
  3. 把最后累加获得的临时表返回给客户端。

image-20200622211243840

5.三、临时表 + 排序

若是咱们把上一步的order by null去掉,默认状况下,group by的结果是会经过c字段排序的。咱们看看其执行计划:

image-20200622211520817

能够发现,这里除了用到临时表,还用到了排序。

咱们进一步看看其执行的OPTIMIZER_TRACE日志:

"steps": [
  {
    "creating_tmp_table": {
      "tmp_table_info": {
        "table": "intermediate_tmp_table",  // 建立中间临时表
        "row_length": 13,
        "key_length": 4,
        "unique_constraint": false,
        "location": "memory (heap)",
        "row_limit_estimate": 1290555
      }
    }
  },
  {
    "filesort_information": [
      {
        "direction": "asc",
        "table": "intermediate_tmp_table",
        "field": "c"
      }
    ],
    "filesort_priority_queue_optimization": {
      "usable": false,
      "cause": "not applicable (no LIMIT)" // 因为没有 limit,不采用优先级队列排序
    },
    "filesort_execution": [
    ],
    "filesort_summary": {
      "rows": 7,
      "examined_rows": 7,
      "number_of_tmp_files": 0,
      "sort_buffer_size": 344,
      "sort_mode": "<sort_key, rowid>"  // rowid排序模式
    }
  }
]
复制代码

经过日志也能够发现,这里用到了中间临时表,因为没有limit限制条数,这里没有用到优先级队列排序,这里的排序模式为sort_key, rowid。其执行流程以下:

  1. 扫描t30汇集索引;
  2. 创建一个临时表,以字段c为主键,依次把扫描t30的记录经过临时表的字段c进行累加;
  3. 把获得的临时表放入sort buffer进行排序,这里经过rowid进行排序;
  4. 经过排序好的rowid回临时表查找须要的字段,返回给客户端。

image-20200622235326129

临时表是存放在磁盘仍是内存?

tmp_table_size 参数用于设置内存临时表的大小,若是临时表超过这个大小,那么会转为磁盘临时表:

image-20200623084009175

能够经过如下sql设置当前session中的内存临时表大小:SET tmp_table_size = 102400;

5.五、直接排序

查看官方文档的 SELECT Statement[9],能够发现SELECT后面可使用许多修饰符来影响SQL的执行效果:

SELECT
    [ALL | DISTINCT | DISTINCTROW ]
    [HIGH_PRIORITY]
    [STRAIGHT_JOIN]
    [SQL_SMALL_RESULT] [SQL_BIG_RESULT] [SQL_BUFFER_RESULT]
    [SQL_CACHE | SQL_NO_CACHE] [SQL_CALC_FOUND_ROWS]
    select_expr [, select_expr] ...
    [into_option]
    [FROM table_references
      [PARTITION partition_list]]
    [WHERE where_condition]
    [GROUP BY {col_name | expr | position}
      [ASC | DESC], ... [WITH ROLLUP]]
    [HAVING where_condition]
    [ORDER BY {col_name | expr | position}
      [ASC | DESC], ...]
    [LIMIT {[offset,] row_count | row_count OFFSET offset}]
    [PROCEDURE procedure_name(argument_list)]
    [into_option]
    [FOR UPDATE | LOCK IN SHARE MODE]

into_option: {
    INTO OUTFILE 'file_name'
        [CHARACTER SET charset_name]
        export_options
  | INTO DUMPFILE 'file_name'
  | INTO var_name [, var_name] ...
}
复制代码

这里咱们重点关注下这两个:

  • SQL_BIG_RESULT:能够在包含group by 和distinct的SQL中使用,提醒优化器查询数据量很大,这个时候MySQL会直接选用磁盘临时表取代内存临时表,避免执行过程当中发现内存不足才转为磁盘临时表。这个时候更倾向于使用排序取代二维临时表统计结果。后面咱们会演示这样的案例;
  • SQL_SMALL_RESULT:能够在包含group by 和distinct的SQL中使用,提醒优化器数据量很小,提醒优化器直接选用内存临时表,这样会经过临时表统计,而不是排序。

固然,在平时工做中,不是特定的调优场景,以上两个修饰符仍是比较少用到的。

接下来咱们就经过例子来讲明下使用了SQL_BIG_RESULT修饰符的SQL执行流程。

有以下SQL:

select SQL_BIG_RESULT c, count(*) from t30 group by c;
复制代码

执行计划以下:

image-20200623221202616

能够发现,这里只用到了排序,没有用到索引或者临时表。这里用到了SQL_BIG_RESULT修饰符,告诉优化器group by的数据量很大,直接选用磁盘临时表,但磁盘临时表存储效率不高,最终优化器使用数组排序的方式来完成这个查询。(固然,这个例子实际的结果集并不大,只是做为演示用)

其执行结果以下:

  1. 扫描t30表,逐行的把c字段放入sort buffer;
  2. 在sort buffer中对c字段进行排序,获得一个排序好的c数组;
  3. 遍历这个排序好的c数组,统计结果并输出。

image-20200623223416492

5.四、group by 优化建议

  • 尽可能让group by走索引,能最大程度的提升效率;
  • 若是group by结果不须要排序,那么能够加上group by null,避免进行排序;
  • 若是group by的数据量很大,可使用SQL_BIG_RESULT修饰符,提醒优化器应该使用排序算法获得group的结果。

六、distinct[10]

在大多数状况下,DISTINCT能够考虑为GROUP BY的一个特殊案例,以下两个SQL是等效的:

select distinct a, b, c from t30;

select a, b, c from t30 group by a, b, c order by null;
复制代码

这两个SQL的执行计划以下:

image-20200623224533837

因为这种等效性,适用于Group by的查询优化也适用于DISTINCT。

**区别:**distinct是在group by以后的每组中取出一条记录,distinct分组以后不进行排序。

6.一、Extra中的distinct

在一个关联查询中,若是您只是查询驱动表的列,而且在驱动表的列中声明了distinct关键字,那么优化器会进行优化,在被驱动表中查找到匹配的第一行时,将中止继续扫描。以下SQL:

explain select distinct t30.a  from t30, t31 where t30.c=t30.c;
复制代码

执行计划以下,能够发现Extra列中有一个distinct,该标识即标识用到了这种优化[10:1]

image-20200623231333626

七、子查询

首先,咱们来明确几个概念:

**子查询:**能够是嵌套在另外一个查询(select insert update delete)内,子查询也能够是嵌套在另外一个子查询里面。

MySQL子查询称为内部查询,而包含子查询的查询称为外部查询。子查询能够在使用表达式的任何地方使用。

接下来咱们使用如下表格来演示各类子查询:

create table class (
  id bigint not null auto_increment,
  class_num varchar(10) comment '课程编号',
  class_name varchar(100) comment '课程名称',
  pass_score integer comment '课程及格分数',
  primary key (id)
) comment '课程';

create table student_class (
  id bigint not null auto_increment,
  student_name varchar(100) comment '学生姓名',
  class_num varchar(10) comment '课程编号',
  score integer comment '课程得分',
  primary key (id)
) comment '学生选修课程信息';

insert into class(class_num, class_name, pass_score) values ('C001','语文', 60),('C002','数学', 70),('C003', '英文', 60),('C004', '体育', 80),('C005', '音乐', 60),('C006', '美术', 70);

insert into student_class(student_name, class_num, score) values('James', 'C001', 80),('Talor', 'C005', 75),('Kate', 'C002', 65),('David', 'C006', 82),('Ann', 'C004', 88),('Jan', 'C003', 70),('James', 'C002', 97), ('Kate', 'C005', 90), ('Jan', 'C005', 86), ('Talor', 'C006', 92);
复制代码

子查询的用法比较多,咱们先来列举下有哪些子查询的使用方法。

7.一、子查询的使用方法

7.1.一、where中的子查询

7.1.1.一、比较运算符

可使用比较运算法,例如=,>,<将子查询返回的单个值与where子句表达式进行比较,如

查找学生选择的编号最大的课程信息:

SELECT class.* FROM class WHERE class.class_num = ( SELECT MAX(class_num) FROM student_class );
复制代码

7.1.1.二、in和not in

若是子查询返回多个值,则能够在WHERE子句中使用其余运算符,例如IN或NOT IN运算符。如

查找学生都选择了哪些课程:

SELECT class.* FROM class WHERE class.class_num IN ( SELECT DISTINCT class_num FROM student_class );
复制代码

7.1.二、from子查询

在FROM子句中使用子查询时,从子查询返回的结果集将用做临时表。该表称为派生表或实例化子查询。如 查找最热门和最冷门的课程分别有多少人选择:

SELECT max(count), min(count) FROM (SELECT class_num, count(1) as count FROM student_class group by class_num) as t1;
复制代码

7.1.三、关联子查询

前面的示例中,您注意到子查询是独立的。这意味着您能够将子查询做为独立查询执行。

独立子查询不一样,关联子查询是使用外部查询中的数据的子查询。换句话说,相关子查询取决于外部查询。对于外部查询中的每一行,对关联子查询进行一次评估。

下面是比较运算符中的一个关联子查询。

查找每门课程超过平均分的学生课程记录:

SELECT t1.* FROM student_class t1 WHERE t1.score > ( SELECT AVG(score) FROM student_class t2 WHERE t1.class_num = t2.class_num);
复制代码

关联子查询中,针对每个外部记录,都须要执行一次子查询,由于每一条外部记录的class_num可能都不同。

7.1.3.一、exists和not exists

当子查询与EXISTS或NOT EXISTS运算符一块儿使用时,子查询将返回布尔值TRUE或FALSE。

查找全部学生总分大于100分的课程:

select * from class t1 
where exists(
  select sum(score) as total_score from student_class t2 
  where t2.class_num=t1.class_num group by t2.class_num having total_score > 100
)
复制代码

7.二、子查询的优化

上面咱们演示了子查询的各类用法,接下来,咱们来说一会儿查询的优化[11]

子查询主要由如下三种优化手段:

  • Semijoin,半链接转换,把子查询sql自动转换为semijion;
  • Materialization,子查询物化;
  • EXISTS策略,in转exists;

其中Semijoin只能用于IN,= ANY,或者EXISTS的子查询中,不能用于NOT IN,<> ALL,或者NOT EXISTS的子查询中。

下面咱们作一下详细的介绍。

真的要尽可能使用关联查询取代子查询吗?

在《高性能MySQL》[12]一书中,提到:优化子查询最重要的建议就是尽量使用关联查询代替,可是,若是使用的是MySQL 5.6或者更新版本或者MariaDB,那么就能够直接忽略这个建议了。由于这些版本对子查询作了很多的优化,后面咱们会重点介绍这些优化。

in的效率真的这么慢吗?

在MySQL5.6以后是作了很多优化的,下面咱们就逐个来介绍。

7.2.一、Semijoin

Semijoin[13],半链接,所谓半链接,指的是一张表在另外一张表栈道匹配的记录以后,返回第一张表的记录。即便右边找到了几条匹配的记录,也最终返回左边的一条。

因此,半链接很是适用于查找两个表之间是否存在匹配的记录,而不关注匹配了多少条记录这种场景。

半链接一般用于IN或者EXISTS语句的优化。

7.2.1.一、优化场景

上面咱们讲到:接很是适用于查找两个表之间是否存在匹配的记录,而不关注匹配了多少条记录这种场景。

in关联子查询

这种场景,若是使用in来实现,可能会是这样:

SELECT class_num, class_name
    FROM class
    WHERE class_num IN
        (SELECT class_num FROM student_class where condition);
复制代码

在这里,优化器能够识别出IN子句要求子查询仅从student_class表返回惟一的class_num。在这种状况下,查询会自动优化为使用半联接。

若是使用exists来实现,可能会是这样:

SELECT class_num, class_name
    FROM class
    WHERE EXISTS
        (SELECT * FROM student_class WHERE class.class_num = student_class.class_num);
复制代码

优化案例

统计有学生分数不及格的课程:

SELECT t1.class_num, t1.class_name
    FROM class t1
    WHERE t1.class_num IN
        (SELECT t2.class_num FROM student_class t2 where t2.score < t1.pass_score);
复制代码

咱们能够经过执行如下脚本,查看sql作了什么优化:

explain extended SELECT t1.class_num, t1.class_name FROM class t1 WHERE t1.class_num IN         (SELECT t2.class_num FROM student_class t2 where t2.score < t1.pass_score);
show warnings\G;
复制代码

获得以下执行执行计划,和SQL重写结果:

image-20200625134010119

从这个SQL重写结果中,能够看出,最终子查询变为了semi join语句:

/* select#1 */ select `test`.`t1`.`class_num` AS `class_num`,`test`.`t1`.`class_name` AS `class_name` 
from `test`.`class` `t1` 
semi join (`test`.`student_class` `t2`) where ((`test`.`t2`.`class_num` = `test`.`t1`.`class_num`) and (`test`.`t2`.`score` < `test`.`t1`.`pass_score`))
复制代码

而执行计划中,咱们看Extra列:

Using where; FirstMatch(t1); Using join buffer (Block Nested Loop)

Using join buffer这项是在join关联查询的时候会用到,前面讲join语句的时候已经介绍过了,如今咱们重点看一下FirstMatch(t1)这个优化项。

**FirstMatch(t1)是Semijoin优化策略中的一种。**下面咱们详细介绍下Semijoin有哪些优化策略。

7.2.1.二、Semijoin优化策略

MySQL支持5中Semijoin优化策略,下面逐一介绍。

7.2.1.2.一、FirstMatch

在内部表寻找与外部表匹配的记录,一旦找到第一条,则中止继续匹配

案例 - 统计有学生分数不及格的课程:

SELECT t1.class_num, t1.class_name
    FROM class t1
    WHERE t1.class_num IN
        (SELECT t2.class_num FROM student_class t2 where t2.score < t1.pass_score);
复制代码

执行计划:

image-20200625140249165

执行流程,图比较大,请你们放大观看:

  1. 扫描class表,把class表分批放入join buffer中,分批处理;
  2. 在批次中依次取出每一条记录,在student_class表中扫描查找符合条件的记录,若是找到,则马上返回,并从该条匹配的class记录取出查询字段返回;
  3. 依次继续扫描遍历。

image-20200625145910057

您也能够去MariaDB官网,查看官方的FirstMatch Strategy[14]解释。

7.2.1.2.二、Duplicate Weedout

将Semijoin做为一个常规的inner join,而后经过使用一个临时表去重。

具体演示案例,参考MariaDB官网:DuplicateWeedout Strategy[15],如下是官网例子的图示:

image-20200625152823844

能够看到,灰色区域为临时表,经过临时表惟一索引进行去重。

7.2.1.2.三、LooseScan

把内部表的数据基于索引进行分组,取每组第一条数据进行匹配。

具体演示案例,参考MariaDB官网:LooseScan Strategy[16],如下是官网例子的图示:

image-20200625154406338

7.2.1.四、Materialization[17]

若是子查询是独立的(非关联子查询),则优化器能够选择将独立子查询产生的结果存储到一张物化临时表中。

为了触发这个优化,咱们须要往表里面添加多点数据,好让优化器认为这个优化是有价值的。

咱们执行如下SQL:

select * from class t1 where t1.class_num in(select t2.class_num from student_class t2 where t2.score > 80) and t1.class_num like 'C%';
复制代码

执行流程以下:

  1. 执行子查询:经过where条件从student_class 表中找出符合条件的记录,把全部记录放入物化临时表;
  2. 经过where条件从class表中找出符合条件的记录,与物化临时表进行join操做。

image-20200625191620132

物化表的惟一索引

MySQL会报物化子查询全部查询字段组成一个惟一索引,用于去重。如上面图示,灰色连线的两条记录冲突去重了。

join操做能够从两个方向执行:

  • 从物化表关联class表,也就是说,扫描物化表,去与class表记录进行匹配,这种咱们称为Materialize-scan
  • 从class表关联物化表,也就是,扫描class表,去物化表中查找匹配记录,这种咱们称为Materialize-lookup,这个时候,咱们用到了物化表的惟一索引进行查找,效率会很快。

下面咱们介绍下这两种执行方式。

Materialize-lookup

仍是以上面的sql为例:

select * from class t1 where t1.class_num in(select t2.class_num from student_class t2 where t2.score > 80) and t1.class_num like 'C%';
复制代码

执行计划以下:

image-20200625162012156

能够发现:

  • t2表的select_type为MATERIALIZED,这意味着id=2这个查询结果将存储在物化临时表中。并把该查询的全部字段做为临时表的惟一索引,防止插入重复记录;
  • id=1的查询接收一个subquery2的表名,这个表正式咱们从id=2的查询获得的物化表。
  • id=1的查询首先扫描t1表,依次拿到t1表的每一条记录,去subquery2执行eq_ref,这里用到了auto_key,获得匹配的记录。

也就是说,优化器选择了对t1(class)表进行全表扫描,而后去物化表进行因此等值查找,最终获得结果。

执行模型以下图所示:

image-20200625193310540

原则:小表驱动大表,关联字段被驱动表添加索引

若是子查询查出来的物化表很小,而外部表很大,而且关联字段是外部表的索引字段,那么优化器会选择扫描物化表去关联外部表,也就是Materialize-scan,下面演示这个场景。

Materialize-scan

如今咱们尝试给class表添加class_num惟一索引:

alter table class add unique uk_class_num(class_num);
复制代码

而且在class中插入更多的数据。而后执行一样的sql,获得如下执行计划:

image-20200625191102623

能够发现,这个时候id=1的查询是选择了subquery2,也就是物化表进行扫描,扫描结果逐行去t1表(class)进行eq_ref匹配,匹配过程当中用到了t1表的索引。

这里的执行流程正好与上面的相反,选择了从class表关联物化表。

如今,我问你们:**Materialization策略何时会选择从外部表关联内部表?**相信你们内心应该有答案了。

执行模型以下:

image-20200625192804901

原则:小表驱动大表,关联字段被驱动表添加索引

如今留给你们另外一个问题:以上例子中,这两种Materialization的开销分别是多少(从行读和行写的角度统计)

答案:

Materialize-lookup:40次读student_class表,40次写物化临时表,42次读外部表,40次lookup检索物化临时表;

Materialize-scan:15次读student_class表,15次写物化临时表,15次扫描物化临时表,执行15次class表索引查询。

7.2.二、Materialization

优化器使用Materialization(物化)来实现更加有效的子查询处理。物化针对非关联子查询进行优化。

物化经过把子查询结果存储为临时表(一般在内存中)来加快查询的执行速度。MySQL在第一次获取子查询结果时,会将结果物化为临时表。随后若是再次须要子查询的结果,则直接从临时表中读取。

优化器可使用哈希索引为临时表创建索引,以使查找更加高效,而且经过索引来消除重复项,让表保持更小。

子查询物化的临时表在可能的状况下存储在内存中,若是表太大,则会退回到磁盘上进行存储。

为什么要使用物化优化

若是未开启物化优化,那么优化器有时会将非关联子查询重写为关联子查询。

能够经过如下命令查询优化开关(Switchable Optimizations[18])状态:

SELECT @@optimizer_switch\G;
复制代码

也就是说,以下的in独立子查询语句:

SELECT * FROM t1
WHERE t1.a IN (SELECT t2.b FROM t2 WHERE where_condition);
复制代码

会重写为exists关联子查询语句:

SELECT * FROM t1
WHERE EXISTS (SELECT t2.b FROM t2 WHERE where_condition AND t1.a=t2.b);
复制代码

开启了物化开关以后,独立子查询避免了这样的重写,使得子查询只会查询一次,而不是重写为exists语句致使外部每一行记录都会执行一次子查询,严重下降了效率。

7.2.三、EXISTS策略

考虑如下的子查询:

outer_expr IN (SELECT inner_expr FROM ... WHERE subquery_where)
复制代码

MySQL“从外到内”来评估查询。也就是说,它首先获取外部表达式outer_expr的值,而后运行子查询并获取其产生的结果集用于比较。

7.2.3.一、condition push down 条件下推

若是咱们能够把outer_expr下推到子查询中进行条件判断,以下:

EXISTS (SELECT 1 FROM ... WHERE subquery_where AND outer_expr=inner_expr)
复制代码

这样就可以减小子查询的行数了。相比于直接用IN来讲,这样就能够加快SQL的执行效率了。

而涉及到NULL值的处理,相对就比较复杂,因为篇幅所限,这里做为延伸学习,感兴趣的朋友能够进一步阅读:

8.2.2.3 Optimizing Subqueries with the EXISTS Strategy[19]

延伸: 除了让关联的in子查询转为exists进行优化以外。在MariaDB 10.0.2版本中,引入了另外一种相反的优化措施:可让exists子查询转换为非关联in子查询,这样就能够用上非关联资产性的物化优化策略了。

详细能够阅读:EXISTS-to-IN Optimization[20]

7.2.四、总结

总结一会儿查询的优化方式:

  • 首先优先使用Semijoin来进行优化,消除子查询,一般选用FirstMatch策略来作表链接;
  • 若是不可使用Semijoin进行优化,而且当前子查询是非关联子查询,则会物化子查询,避免屡次查询,同时这一步的优化会遵循选用小表做为驱动表的原则,尽可能走索引字段关联,分为两种执行方式:Materialize-lookup,Materialization-scan。一般会选用哈希索引为物化临时表提升检索效率;
  • 若是子查询不能物化,那就只能考虑Exists优化策略了,经过condition push down把条件下推到exists子查询中,减小子查询的结果集,从而达到优化的目的。

八、limit offset, rows

limit的用法:

limit [offset], [rows]

其中 offset表示偏移量,rows表示须要返回的行数。

offset  limit  表中的剩余数据
 _||_   __||__   __||__
|    | |      | |
RRRRRR RRRRRRRR RRR...
       |______|
          ||
         结果集
复制代码

8.一、执行原理

MySQL进行表扫描,读取到第 offset + rows条数据以后,丢弃前面offset条记录,返回剩余的rows条记录。

好比如下sql:

select * from t30 order by id limit 10000, 10;
复制代码

这样总共会扫描10010条。

8.二、优化手段

若是查询的offset很大,避免直接使用offset,而是经过id到汇集索引中检索查找。

  1. 利用自增索引,如:
select * from t30 where id > 10000 limit 10;
复制代码

固然,这也是会有问题的,若是id中间产生了非连续的记录,这样定位就不许确了。写到这里,篇幅有点长了,最后这个问题留给你们思考,感兴趣的朋友能够进一步思考探讨与延伸。


这篇文章的内容就差很少介绍到这里了,可以阅读到这里的朋友真的是颇有耐心,为你点个赞。

本文为arthinking基于相关技术资料和官方文档撰写而成,确保内容的准确性,若是你发现了有何错漏之处,烦请高抬贵手帮忙指正,万分感激。

你们能够关注个人博客:itzhai.com 获取更多文章,我将持续更新后端相关技术,涉及JVM、Java基础、架构设计、网络编程、数据结构、数据库、算法、并发编程、分布式系统等相关内容。

若是您以为读完本文有所收获的话,能够关注个人帐号,或者点赞吧,码字不易,您的支持就是我写做的最大动力,再次感谢!

关注个人公众号,及时获取最新的文章。

更多文章

  • 关注公众号进入会话窗口获取
  • JVM系列专题:公众号发送 JVM

本文做者: arthinking

博客连接: www.itzhai.com/database/ho…

SQL运行内幕:从执行原理看调优的本质

版权声明: BY-NC-SA许可协议:创做不易,如需转载,请联系做者,谢谢!


References


  1. zhuanlan.zhihu.com/p/54378839.… ↩︎

  2. 8.2.1.14 ORDER BY Optimization. Retrieved from https://dev.mysql.com/doc/refman/5.7/en/order-by-optimization.html ↩︎

  3. 8.8.2 EXPLAIN Output Format. Retrieved from https://dev.mysql.com/doc/refman/5.7/en/explain-output.html ↩︎

  4. Batched Key Access: a Significant Speed-up for Join Queries. Retrieved from https://conferences.oreilly.com/mysql2008/public/schedule/detail/582 ↩︎

  5. Batched Key Access Joins. Retrieved from http://underpop.online.fr/m/mysql/manual/mysql-optimization-bka-optimization.html ↩︎

  6. [Hash join in MySQL 8. MySQL Server Blog. Retrieved from mysqlserverteam.com/hash-join-i… only supports inner hash,more often than it does](mysqlserverteam.com/hash-join-i… only supports inner hash,more often than it does) ↩︎

  7. MySQL JOINS Tutorial: INNER, OUTER, LEFT, RIGHT, CROSS. Retrieved from https://www.guru99.com/joins.html ↩︎

  8. How the SQL join actually works?. Retrieved from https://stackoverflow.com/questions/34149582/how-the-sql-join-actually-works ↩︎

  9. 13.2.9 SELECT Statement. Retrieved from https://dev.mysql.com/doc/refman/5.7/en/select.html ↩︎

  10. 8.2.1.18 DISTINCT Optimization. Retrieved from https://dev.mysql.com/doc/refman/8.0/en/distinct-optimization.html ↩︎ ↩︎

  11. Subquery Optimizer Hints. Retrieved from https://dev.mysql.com/doc/refman/8.0/en/optimizer-hints.html#optimizer-hints-subquery ↩︎

  12. 高性能MySQL第3版[M]. 电子工业出版社, 2013-5:239. ↩︎

  13. 8.2.2.1 Optimizing Subqueries, Derived Tables, and View References with Semijoin Transformations. Retrieved from https://dev.mysql.com/doc/refman/5.7/en/semijoins.html ↩︎

  14. FirstMatch Strategy. Retrieved from https://mariadb.com/kb/en/firstmatch-strategy/ ↩︎

  15. DuplicateWeedout Strategy. Retrieved from https://mariadb.com/kb/en/duplicateweedout-strategy/ ↩︎

  16. LooseScan Strategy. Retrieved from https://mariadb.com/kb/en/loosescan-strategy/ ↩︎

  17. Semi-join Materialization Strategy. Retrieved from https://mariadb.com/kb/en/semi-join-materialization-strategy/ ↩︎

  18. Switchable Optimizations. Retrieved from https://dev.mysql.com/doc/refman/5.7/en/switchable-optimizations.html ↩︎

  19. 8.2.2.3 Optimizing Subqueries with the EXISTS Strategy. Retrieved from https://dev.mysql.com/doc/refman/8.0/en/subquery-optimization-with-exists.html ↩︎

  20. EXISTS-to-IN Optimization. Retrieved from https://mariadb.com/kb/en/exists-to-in-optimization/ ↩︎

相关文章
相关标签/搜索