mysql优化十二:内核查询优化规则详解

内核查询优化规则详解

之前有说过当mysql执行sql语句的时候会进行一些优化,比如说索引下推,索引合并,回表的时候可能触发MRR机制等等。但是这些优化是在mysql执行的时候优化,在执行之前mysql会对sql语句上的优化。在优化十的时候有一张mysql的执行流程图,其中查询优化器就是用来优化sql的。如果我们能了解这些优化规则,那么在写sql的时候我们自己把sql优化好,就节省了mysql帮你优化的这部分时间。

条件简化

当我们写的sql语句比较复杂,不能高效的执行,mysql会帮我们进行sql的简化。
简化多余的括号
有时候表达式里有许多无用的括号,比如这样:
((a = 5 AND b = c) OR ((a > c) AND (c < 5)))
优化器会把那些用不到的括号给干掉,就是这样:(a = 5 and b = c) OR (a > c AND c < 5)
常量传递(constant_propagation)
比如:a = 5 AND b > a 优化成a = 5 AND b > 5
a = b and b = c and c = 5 优化成 a = 5 and b = 5 and c = 5
移除没用的条件(trivial_condition_removal)
对于一些明显永远为TRUE或者FALSE的表达式,优化器会移除掉它们,比如这个表达式:(a < 1 and b = b) OR (a = 6 OR 5 != 5)
优化成:(a < 1 and TRUE) OR (a = 6 OR FALSE) 继续优化成:a < 1 OR a = 6
表达式计算
在查询开始执行之前,如果表达式中只包含常量的话,它的值会被先计算出来,比如:a = 5 + 1优化成 a = 6
需要注意的是如果说表达式包含函数,或者比较复杂的话,mysql是不会优化的。比如:ABS(a) > 5或者: -a < -8
常量表检测
当mysql检测通过主键索引或者唯一性的二级索引进行等值比较作为搜索条件的时候,会优先查询这张表,那么这张表就叫做常量表。比如SELECT * FROM table1 INNER JOIN table2 ON table1.column1 = table2.column2 WHERE table1.primary_key = 1;
mysql认为table1 通过主键primary_key = 1 定位到记录会很快,快到可以忽略。因此在sql优化的时候优先把这条记录查询来,然后把这条sql涉及table1的条件全部替换成常量,在进行计算查询的成本。假设table1 查到到的column1 = 5
那么上面的sql就会优化成
SELECT table1的全部常量值, table2.* FROM table1 INNER JOIN table2 ON 5 = table2.column2;

外连接消除

关于内连接查询mysql内部会进行优化,自动调整驱动表和被驱动表。而左(外)连接和右 (外)连接的驱动表和被驱动表是固定的。这就导致内连接可能通过优化表的连接顺序 来降低整体的查询成本,而外连接却无法优化表的连接顺序。
外连接和内连接的本质区别就是:

  • 对于外连接的驱动表的记录来说,如 果无法在被驱动表中找到匹配ON子句中的过滤条件的记录,那么该记录仍然会被加入到结果集中,对应的被驱动表记录的各个字段使用NULL值填充;
  • 而内连接的驱动表的记录 如果无法在被驱动表中找到匹配ON子句中的过滤条件的记录,那么该记录会被舍弃。
    例如:
    mysql优化十二:内核查询优化规则详解_第1张图片
    mysql优化十二:内核查询优化规则详解_第2张图片
    如果说我们在搜索条件中指定关于被驱动表相关列的值不为NULL,那么外连接中在被驱动表中找不到符合ON子句条件的驱动表记录也就被排除出最后的结果集了,也就是说:在这种情况下:外连接和内连接 也就没有什么区别了!
    例如:
    mysql优化十二:内核查询优化规则详解_第3张图片
    我们把这种在外连接查询中,指定的WHERE子句中包含被驱动表中的列不为NULL值的条件称之为空值拒绝(英文名:reject-NULL)。在被驱动表的WHERE子句符合空值拒绝的条件后,外连接和内连接可以相互转换。这种转换带来的好处就是查询优化器可以通过评估表的不同连接顺序的成本,选出成本最低的那种连接顺序来执行查询。

子查询MySQL内部优化规则

子查询分类

在一个查询语句A里的某个位置也可以有另一个查询语句B,这个出现在A语句的某个位置中的查询B就被称为子查询,A也被称之为外层查询。子查询可以在一个外层查询的各种位置出现。
SELECT子句中
SELECT (SELECT id FROM s1 LIMIT 1);
FROM子句中
SELECT * FROM (SELECT id+1 FROM s1) as t;
WHERE或ON子句中
SELECT * FROM e1 WHERE m1 IN (SELECT m2 FROM e2);

按返回的结果集区分子查询
因为子查询本身也算是一个查询,所以可以按照它们返回的不同结果集类型而把这些子 查询分为不同的类型:

  • 标量子查询:那些只返回一个单一值的子查询称之为标量子查询
  • 行子查询:就是返回一条记录的子查询,不过这条记录需要包含多个列
  • 列子查询:就是查询出一个列的数据,不过这个列的数据需要包含多条记录
  • 表子查询:既包含很多条记录,又包含很多个列

按与外层查询关系来区分子查询
不相关子查询
如果子查询可以单独运行出结果,而不依赖于外层查询的值,我们就可以把这个子查询 称之为不相关子查询。
SELECT * FROM s1 as a WHERE a.order_no IN (SELECT b.order_no FROM s2 as b)
相关子查询
如果子查询的执行需要依赖于外层查询的值,我们就可以把这个子查询称之为相关子查询。
SELECT * FROM s1 as a WHERE a.order_no IN (SELECT b.order_no FROM s2 as b where b.id < a.id)

[NOT] IN/ANY/SOME/ALL子查询
IN或者NOT IN
SELECT * FROM s1 as a WHERE (a.order_no,a.order_note) IN (SELECT b.order_no,b.order_note FROM s2 as b )
ANY/SOME(ANY和SOME是同义词)
SELECT a.id FROM s1 as a WHERE a.id > ANY(SELECT id FROM s2)
这个sql的意思就是只要s2表中的id有一条记录是小于s1的id的那么就这条sql就为true 。
简单来说就是查出s1中id大于s2的最小值的数据。
SELECT a.id FROM s1 as a WHERE a.id < ANY(SELECT id FROM s2)
查出s1中id小于s2id的最大值的数据
ALL(子查询)
SELECT a.id FROM s1 as a WHERE a.id > ALL(SELECT id FROM s2)
查出s1id大于s2id的最大值的数据
EXISTS子查询
有的时候我们仅仅需要判断子查询的结果集中是否有记录,而不在乎它的记录具体是个啥,可以使用把EXISTS或者NOT EXISTS放在子查询语句前边
SELECT a.id FROM s1 as a WHERE EXISTS (SELECT * FROM s2 );
对于子查询(SELECT * FROM s2)来说,我们并不关心这个子查询最后到底查询出的结果 是什么,所以查询列表里填*、某个列名,或者其他啥东西都无所谓,我们真正关心的是 子查询的结果集中是否存在记录。也就是说只要(SELECT * FROM s2)这个查询中有记录,那么整个EXISTS表达式的结果就为TRUE。

子查询在MySQL中执行和内部优化

那么mysql中的子查询是如何执行的呢。根据上面的分类。来看看各 种不同类型的子查询具体是怎么执行的
标量子查询、行子查询的执行方式
不相关子查询:SELECT * FROM s1 WHERE order_note = (SELECT order_note FROM s2 WHERE key3 = 'a' LIMIT 1);
可以想到,先执行子查询,把查出的结果带入到外层查询
相关子查询:SELECT * FROM s1 WHERE order_note = (SELECT order_note FROM s2 WHERE s1.order_no= s2.order_no LIMIT 1);
可以想到是先执行外层查询,把结果带入到子查询中作为条件筛选。
对于标量子查询、行子查询不管是相关的还是不相关,都可以看成两个sql先后执行。
对于表子查询和列子查询的执行方式
对于这种类型的查询我们用的最多基本在in子查询当中。例如SELECT * FROM s1 WHERE order_note IN (SELECT order_note FROM s2 WHERE order_no = 'a'); 所以我们重点分析mysql对于in子查询的执行和优化
MySQL对IN子查询的优化
SELECT * FROM s1 WHERE order_note IN (SELECT order_note FROM s2 WHERE order_no = 'a');针对于这条sql,一般会认为先执行子查询,然后在执行外层查询,都是把外层查询和子查询当作两个独立的单表查询来对待。但是MySQL为了优 化IN子查询下了很大力气,所以整个执行过程并不像我们想象的那么简单。
在sql成本计算的时候有说过,对于in这种查询会产生单点区间,对于数量少的时候会通过index dive的方式进行成本计算。这种情况可以把它看成两个独立的表进行查询没问题。那如果多呢,上千个,上万个,十万个等等呢。
就会导致这些问题:

  1. 结果集太多,可能内存中都放不下。
  2. 对于外层查询来说,如果子查询的结果集太多,那就意味着IN子句中的参数特别多, 这就导致:无法有效的使用索引,只能对外层查询进行全表扫描。
    在对外层查询执行全表扫描时,由于IN子句中的参数太多,这会导致检测一条记录是否符合和IN子句中的参数匹配花费的时间太长。

对于这种情况MySQL的改进并不是直接将不相关子查询的结果集当作外层查询的参数,而是将该结果集写入一个临时表里。临时表的写入过程是这样的:

  1. 临时表的列就是子查询结果集中的列
  2. 写入的临时表的记录会去重,并且会创建主键或者唯一索引
  3. 一般情况下子查询结果集不会大的离谱,所以会为它建立基于内存使用的Memory存储引擎的临时表,而且会为该表建立哈希索引。
  4. 如果子查询的结果集非常大,超过了系统变量tmp_table_size或者 max_heap_table_size,临时表会转而使用基于磁盘的存储引擎来保存结果集中的记录, 索引类型也对应转变为B+树索引。

MySQL把这个将子查询结果集中的记录保存到临时表的过程称之为物化。为了方便起见,我们就把那个存储子查询结果集的临时表称之为物化表。
正因为物化表中的记录都建立了索引(基于内存的物化表有哈希索引,基于磁盘的有B+树索引),通过索引执行IN语句判断某个操作数在不在子查询结果集中变得非常快,从而提升了子查询语句的性能。那么在转化成物化表之后怎么查询呢,mysql在查询的时候会把之前的in查询转成连接查询。比如说子查询的结果集物化表的名称为materialized_table,字段名称m_val。那么这个查询变成SELECT * FROM s1 inner join materialized_table on order_note = m_val ;转成内连接后就会按照内连接的成本计算,然后选择成本最少的作为执行方案。
将子查询转换为semi-join
但是将子查询进行物化之后再执行查询都会有建立临时表的成本,能不能不进行物化操作直 接把子查询转换为连接呢?
在回顾下上面的这个语句:SELECT * FROM s1 WHERE order_note IN (SELECT order_note FROM s2 WHERE order_no = 'a');
我们可以把这个查询理解成:对于s1表中的某条记录,如果我们能在s2表(准确的说是 执行完WHERE s2.order_no= 'a’之后的结果集)中找到一条或多条记录,这些记录的 order_note的值等于s1表记录的order_note列的值,那么该条s1表的记录就会被加入到最终的结果集。这个过程其实和把s1和s2两个表连接起来的效果很像: SELECT s1.* FROM s1 INNER JOIN s2 ON s1.order_note = s2.order_note WHERE s2.order_no= 'a'; 只能说这两条sql很像,但结果不一定相同。
MySQL在这里提出了一个新概念 — 半连接 (英文名:semi-join)。
将s1表和s2表进行半连接的意思就是:对于s1表的某条记录来说,我们只关心在s2表中 是否存在与之匹配的记录,而不关心具体有多少条记录与之匹配,最终的结果集中只保留s1表的记录。
例如:
EXPLAIN SELECT * FROM s1 WHERE order_note IN (SELECT order_note FROM s2 WHERE order_no = 'a');
SHOW WARNINGS;
将这两条sql同时执行,再看结果2的数据
mysql优化十二:内核查询优化规则详解_第4张图片

mysql优化十二:内核查询优化规则详解_第5张图片
可以看到MySQL将这个子查询改造为了半连接semi join。
怎么实现这种所谓的半连接呢?MySQL准备了好几种办法,比如Table pullout (子查询 中的表上拉)、DuplicateWeedout execution strategy (重复值消除)、LooseScan execution strategy (松散扫描)、Semi-join Materializationa半连接物化、 FirstMatch execution strategy (首次匹配)等等。

不能转为semi-join查询的子查询优化
并不是所有包含IN子查询的查询语句都可以转换为semi-join,对于不能转换 的,MySQL有这几种方法:

  1. 对于不相关子查询来说,会尝试把它们物化之后再参与查询。
    例如:
    EXPLAIN SELECT * FROM s1 WHERE order_note NOT IN (SELECT order_note FROM s2 WHERE order_no= 'a');
    SHOW WARNINGS;
    mysql优化十二:内核查询优化规则详解_第6张图片
  2. 不管子查询是相关的还是不相关的,都可以把IN子查询尝试转为EXISTS子查询。其实对于任意一个IN子查询来说,都可以被转为EXISTS子查询。例如:
    SELECT * FROM s1 WHERE order_no IN (SELECT order_no FROM s2 where s1.order_note = s2.order_note) OR insert_time > ‘2021-03-22 18:28:28’;
    将它转为EXISTS子查询:
    SELECT * FROM s1 WHERE EXISTS (SELECT 1 FROM s2 where s1.order_note = s2.order_note AND s2.order_no= s1.order_no) OR insert_time > '2021-03-22 18:28:28';
    为啥要转换呢?这是因为不转换的话可能用不到索引,转为EXISTS子查询时便可能使用到s2表的idx_order_no索引了。
    mysql优化十二:内核查询优化规则详解_第7张图片

需要注意的是,如果IN子查询不满足转换为semi-join的条件,又不能转换为物化表或者 转换为物化表的成本太大,那么它就会被转换为EXISTS查询。
在MySQL5.5以及之前的版本没有引进semi-join和物化的方式优化子查询时,优化器都会 把IN子查询转换为EXISTS子查询,所以很多技术书籍或者博客都是建议大家把子查询转 为连接,不过随着MySQL的发展,最近的版本中引入了非常多的子查询优化策略,内部的 转换工作优化器会为大家自动实现。
IN子查询小结
如果IN子查询符合转换为semi-join的条件,查询优化器会优先把该子查询转换为semi- join,然后从前面所说的5种执行半连接的策略(既子查询中的表上拉、重复值消除等 等)中选择成本最低的那种执行策略来执行子查询。
如果IN子查询不符合转换为semi-join的条件,那么查询优化器会从下边两种策略中找出 一种成本更低的方式执行子查询:
先将子查询物化之后再执行查询
执行IN to EXISTS转换
ANY/ALL子查询优化
如果ANY/ALL子查询是不相关子查询的话,它们在很多场合都能转换成我们熟悉的方式去 执行,
比方说:
< ANY (SELECT inner_expr ...) 等价于 < (SELECT MAX(inner_expr) ...)
> ANY (SELECT inner_expr ...) 等价于 > (SELECT MIN(inner_expr) ...)
< ALL (SELECT inner_expr ...) 等价于 < (SELECT MIN(inner_expr) ...)
> ALL (SELECT inner_expr ...) 等价于 > (SELECT MAX(inner_expr) ...)

总结
对于mysql的内核查询优化,我们写的sql mysql的内核会自动帮我们去做优化,但是如果我们了解这些优化规则,提前的把sql按照这些规则去优化好,就减少了mysql帮我们去做内核优化的这部分时间,这也是减少mysql的方案。

你可能感兴趣的:(性能优化,mysql,数据库,sql)