13518219792

建站动态

根据您的个性需求进行定制 先人一步 抢占小程序红利时代

庖丁解牛-图解MySQL8.0优化器查询解析篇

一、背景和架构

我们都知道,利用编写程序来动态实现我们应用所需要的逻辑,从而程序执行时得到我们需要的结果。那么数据库就是一种通过输入SQL字符串来快速获取数据的应用。当然,假设没有数据库这种系统应用,用程序如何实现呢?我们可能会发现,即使不管数据如何存储、数据是否并发访问,仍然需要不断通过修改程序处理不同应用对数据的不同请求。比如大数据领域,我们通常通过非关系型数据库的API,实现对数据的获取。然而这种方式虽然入门简单,但是维护极难,而且通用性不强,即使不断进行软件架构设计或者抽象重构,仍然需要不断地变换应用,这也是为何非关系型数据库回头拥抱数据库SQL优化器的原因。

10年积累的成都网站设计、网站制作经验,可以快速应对客户对网站的新想法和需求。提供各种问题对应的解决方案。让选择我们的客户得到更好、更有力的网络服务。我虽然不认识你,你也不认识我。但先网站制作后付款的网站建设流程,更有阿勒泰免费网站建设让你可以放心的选择与我们合作。

SQL优化器本质上是一种高度抽象化的数据接口的实现,经过该设计,客户可以使用更通用且易于理解的SQL语言,对数据进行操作和处理,而不需要关注和抽象自己的数据接口,极大地解放了客户的应用程序。

本文就来通过图形解说的方式介绍下MySQL 8.0 SQL优化器如何把一个简单的字符串(SQL),变成数据库执行器可以理解的执行序列,最终将数据返还给客户。强大的优化器是不需要客户关注SQL如何写的更好来更快获得需要的数据,因此优化器对原始SQL一定会做一些等价的变化。在 《MySQL 8.0 Server层最新架构详解》 一文中我们重点介绍了MySQL最新版本关于Server层解析器、优化器和执行器的总体介绍,包括一些代码结构和变化的详细展示,并且通过simple_joins函数抛砖引玉展示了MySQL优化器在逻辑变换中如何简化嵌套Join的优化。本文我们会一步一步带你进入神奇的优化器细节,详细了解优化器优化部分的每个步骤如何改变着一个SQL最终的执行。

本文基于最新MySQL8.0.25版本,因为优化器转换部分篇幅比较长,我们分成两篇文章来介绍,第一部分介绍基于基本结构的Setup和Resolve的解析转换过程,第二部分介绍更为复杂的子查询、分区表和连接的复杂转换过程,大纲如下:

Setup and Resolve

二、详细转换过程

转换的整个框架是由Query_expression到Query_block调用prepare函数(sql/sql_resolver.cc)并且根据不同转换规则的要求自顶向下或者自底向上的过程。

1.传递null到join的内表列表(propagate_nullability)

prepare开始先要处理nullable table,它指的是table可能包含全为null的row,根据JOIN关系(top_join_list)null row可以被传播。如果能确定一个table为nullable会使得一些优化退化,比如access method不能为EQ_REF、outer join不能优化为inner join等。

2.解析设置查询块的leave_tables(setup_tables)

 
 
 
 
  1. SELECT 
  2.   t1.c1 
  3. FROM t1, 
  4.      (SELECT 
  5.        t2.c1 
  6.      FROM t2, 
  7.           (SELECT 
  8.             t3.c1 
  9.           FROM t3 
  10.           UNION 
  11.           SELECT 
  12.             t4.c1 
  13.           FROM t4) AS t3a) AS t2a; 

未在setup_table调用之前,每个Query_block的leaf_tables是为0的。

该函数的作用就是构建leaf_tables,包括base tables和derived tables列表,用于后续的优化。setup_tables并不会递归调用,而是只解决本层的tables,并统计出本层derived table的个数。但是随后会调用resolve_placeholder_tables()->resolve_derived()->derived(Query_expression)::prepare->Query_block::prepare来专门递归处理derived table对应的Query_expression。

接下来我们根据prepare的调用顺序,继续看下针对于derived table处理的函数resolve_placeholder_tables。

3.解析查询块Derived Table、View、Table函数 (resolve_placeholder_tables)

这个函数用于对derived table、view和table function的处理,如果该table已经merged过了,或者是由于使用transform_grouped_to_derived()被调用到,已经决定使用materialized table方式,则直接忽略。

前面已经介绍过resolve_derived()的作用,我们重点介绍merge_derived()函数,merge_derived是改变Query_expression/Query_block框架结构,将derived table或者view合并到到query block中。

merge_derived 处理和合并Derived table

1)merge_derived transformation的先决条件

2)merge_derived transformation的转换过程

过程简化为:

merge_derived 图解过程

看起来官方的derived merge还是不够完美,无法自底向上的递归merge

包含的opt trace:

 
 
 
 
  1. trace_derived.add_utf8_table(derived_table) 
  2.        .add("select#", derived_query_block->select_number) 
  3.        .add("merged", true); 
  4.  
  5.  
  6. trace_derived.add_alnum("transformations_to_derived_table", "removed_ordering"); 

该优化可以通过set optimizer_switch="derived_merge=on/off"来控制。

setup_materialized_derived 设置物化Derived Table

对于剩下不能采用 merge 算法的 derived table ,会转为materialize 物化方式去处理。但此时只是做一些变量设置等预处理,实际的物化执行是在executor阶段执行。

 
 
 
 
  1. trace_derived.add_utf8_table(this) 
  2.        .add("select#", derived->first_query_block()->select_number) 
  3.        .add("materialized", true); 

setup_table_function 处理表函数

如果 query block 中有 table function,整个过程会处理两遍。第一遍会跳过 table function 的 table ,第二遍才专门再对table function 的 table 执行一遍上述逻辑。这里的考虑应该是先 resolve 了外部环境(相对于table function),因为有可能函数参数会有依赖外部的 derived table。

 
 
 
 
  1. trace_derived.add_utf8_table(this) 
  2.        .add_utf8("function_name", func_name, func_name_len) 
  3.        .add("materialized", true); 

4.将SELECT *的通配符展开成具体的fields(setup_wild)

5.建立Query_block级别的base_ref_items(setup_base_ref_items)

base_ref_items记录了所有Item的位置,方便查询块的其他Item可以进行引用,或者通过Item_ref及其Item_ref子类进行直接引用,例如子查询的引用(Item_view_ref)、聚合函数引用(Item_aggregate_ref)、外查询列的引用(Item_outer_ref)、subquery 子查询产生NULL value的引用辅助(Item_ref_null_helper)。

举例说明比较复杂的Item_outer_ref:

6.对select_fields进行fix_fields()和列权限检查(setup_fields)

下图是比较复杂的带子查询的fixed field过程。有些field和表关联,有的要添加相应的Item_xxx_ref引用。

7.解析和fixed_fields WHERE条件和Join条件(setup_conds)

setup_join_cond如果有nested_join会递归调用setup_join_cond进行解析和设置。这里也顺带介绍下simplify_const_condition函数的作用,如果发现可以删除的const Item,则会用Item_func_true/Item_func_false来替代整个的条件,如图。

8.解析和设置ROLLUP语句(resolve_rollup)

在数据库查询语句中,在 GROUP BY 表达式之后加上 WITH ROLLUP 语句,可以使得通过单个查询语句来实现对数据进行不同层级上的分析与统计。

 
 
 
 
  1. SELECT YEAR, 
  2.        country, 
  3.        product, 
  4.        SUM(profit) AS profit 
  5. FROM sales 
  6. GROUP BY YEAR, 
  7.          country, 
  8.          product WITH ROLLUP; 
  9.  
  10.  
  11. +------+---------+------------+--------+ 
  12. | year | country | product    | profit | 
  13. +------+---------+------------+--------+ 
  14. | 2000 | Finland | Computer   |   1500 | 
  15. | 2000 | Finland | Phone      |    100 | 
  16. | 2000 | Finland | NULL       |   1600 | 
  17. | 2000 | India   | Calculator |    150 | 
  18. | 2000 | India   | Computer   |   1200 | 
  19. | 2000 | India   | NULL       |   1350 | 
  20. | 2000 | USA     | Calculator |     75 | 
  21. | 2000 | USA     | Computer   |   1500 | 
  22. | 2000 | USA     | NULL       |   1575 | 
  23. | 2000 | NULL    | NULL       |   4525 | 
  24. | 2001 | Finland | Phone      |     10 | 
  25. | 2001 | Finland | NULL       |     10 | 
  26. | 2001 | USA     | Calculator |     50 | 
  27. | 2001 | USA     | Computer   |   2700 | 
  28. | 2001 | USA     | TV         |    250 | 
  29. | 2001 | USA     | NULL       |   3000 | 
  30. | 2001 | NULL    | NULL       |   3010 | 
  31. | NULL | NULL    | NULL       |   7535 | 
  32. +------+---------+------------+--------+ 
  33.  
  34.  
  35. 相当于做了下面的查询: 
  36.  
  37.  
  38. SELECT * 
  39. FROM 
  40.   (SELECT YEAR, 
  41.           country, 
  42.           product, 
  43.           SUM(profit) AS profit 
  44.    FROM sales 
  45.    GROUP BY YEAR, 
  46.             country, 
  47.             product 
  48.    UNION ALL SELECT YEAR, 
  49.                     country, 
  50.                     NULL, 
  51.                     SUM(profit) AS profit 
  52.    FROM sales 
  53.    GROUP BY YEAR, 
  54.             country 
  55.    UNION ALL SELECT YEAR, 
  56.                     NULL, 
  57.                     NULL, 
  58.                     SUM(profit) AS profit 
  59.    FROM sales 
  60.    GROUP BY YEAR 
  61.    UNION ALL SELECT NULL, 
  62.                     NULL, 
  63.                     NULL, 
  64.                     SUM(profit) AS profit 
  65.    FROM sales) AS sum_table 
  66. ORDER BY YEAR, country, product; 
  67.  
  68.  
  69. +------+---------+------------+--------+ 
  70. | YEAR | country | product    | profit | 
  71. +------+---------+------------+--------+ 
  72. | NULL | NULL    | NULL       |   7535 | 
  73. | 2000 | NULL    | NULL       |   4525 | 
  74. | 2000 | Finland | NULL       |   1600 | 
  75. | 2000 | Finland | Computer   |   1500 | 
  76. | 2000 | Finland | Phone      |    100 | 
  77. | 2000 | India   | NULL       |   1350 | 
  78. | 2000 | India   | Calculator |    150 | 
  79. | 2000 | India   | Computer   |   1200 | 
  80. | 2000 | USA     | NULL       |   1575 | 
  81. | 2000 | USA     | Calculator |     75 | 
  82. | 2000 | USA     | Computer   |   1500 | 
  83. | 2001 | NULL    | NULL       |   3010 | 
  84. | 2001 | Finland | NULL       |     10 | 
  85. | 2001 | Finland | Phone      |     10 | 
  86. | 2001 | USA     | NULL       |   3000 | 
  87. | 2001 | USA     | Calculator |     50 | 
  88. | 2001 | USA     | Computer   |   2700 | 
  89. | 2001 | USA     | TV         |    250 | 
  90. +------+---------+------------+--------+ 

排序由于有NULL的问题,所以分级汇总的效果非常难弄,而且group 列不同改变,SQL复杂度来回变化,而ROLLUP很简单就可以实现效果,下面看下rollup在解析过程做了什么样的转换达到了意想不到的效果。

9.解析和设置GROUP BY/ORDER BY语句(setup_group/setup_order)

其中一个函数find_order_in_list(): 尝试在select fields里去寻找可以映射的列,否则就得在最后投影的all fields里加上当前列,同时也做fix_fields。

例如场景:

 
 
 
 
  1. SELECT DISTINCT c1, max(c2) from t1 group by c1; 

10.解析和设置Window函数(Window::setup_windows1)

 
 
 
 
  1. SELECT id, 
  2.        release_year, 
  3.        rating, 
  4.        avg(rating) over(PARTITION BY release_year) AS year_avg 
  5. FROM tw; 
  6.  
  7.  
  8. +------+--------------+--------+-------------------+ 
  9. | id   | release_year | rating | year_avg          | 
  10. +------+--------------+--------+-------------------+ 
  11. |    1 |         2015 |      8 |               8.5 | 
  12. |    3 |         2015 |      9 |               8.5 | 
  13. |    2 |         2015 |    8.5 |               8.5 | 
  14. |    4 |         2016 |    8.2 |               8.3 | 
  15. |    5 |         2016 |    8.4 |               8.3 | 
  16. |    6 |         2017 |      7 |                 7 | 
  17. +------+--------------+--------+-------------------+ 

执行的过程和结果类似于下图:

我们看下它在开始Query_block::prepare解析过程做了哪些事情:

select_lex->m_windows 不为空,就调用  Window::setup_windows1

三、综述

本文重点介绍了下优化器的基于规则的其中一部分优化,更多的偏重于SQL中的基本操作符,如表、列、函数、聚合、分组、排序等元素的解析和设置以及一些显而易见的结构变化。下一篇文章我们将继续介绍子查询、分区表和JOIN操作的转换部分,敬请期待。


本文题目:庖丁解牛-图解MySQL8.0优化器查询解析篇
当前链接:http://cdbrznjsb.com/article/cdidcho.html

其他资讯

让你的专属顾问为你服务