Presto Query Planner

在深入研究Presto查询规划器和基于成本的优化如何工作之前,让我们先建立一个查询,并针对这个查询进行分析,以帮助理解查询规划的过程。

实例使用了TPC-H数据集,目的是汇总每个nation的所有order的totalprice值并列出排名前五的。

-- 实例一:
SELECT
    (SELECT name FROM region r WHERE regionkey = n.regionkey) AS region_name,
    n.name AS nation_name,
    sum(totalprice) orders_sum
FROM nation n, orders o, customer c
WHERE n.nationkey = c.nationkey
    AND c.custkey = o.custkey
GROUP BY n.nationkey, regionkey, n.name
ORDER BY orders_sum DESC
LIMIT 5;

如上SQL所示:
子查询:目的是从region表中提取region_name

一、Parsing and Analysis

在计划执行之前,需要对其进行转化和分析,Presto根据语法规则校验SQL文本,下一步就是对查询进行分析:

1.1、确认查询中的Tables

Presto中的表是根据catalogs+Schemas进行组织的,二者确定唯一,因此多个表可以具有相同的名字,例如,TPC-H数据中就有多个表明为orders的表,但是他们在不同的Schema下面,如:sf10.orders 以及 sf100.orders

1.2、标识查询中使用的colums

如SQL中所示,orders.totalprice即明确的引用了order表中的totalprice 列,当SQL中涉及的Table里没有相同字段时,通常直接写Column名就可以,Presto Analyzer会自动确定Column来自哪个表。

1.3、确定ROW中Field的引用

单纯的给出一个表达式,如:c.bonus;它的含义可能表示c表中的bonus列,但也可能是指一个复杂类型的列的名字为C,其中一个字段为bonus,这个分析工作主要由Presto决定,当有冲突时,优先按第一种情况处理。 解析过程会遵循SQL语言的作用域和可见性规则, 收集到的信息比如标识符消歧,这些收集到的信息稍后也会在查询计划规划的过程中使用, 这样Planner 就不需要再次理解理解查询语言的规则,避免重复工作。

Query Analyzer具有复杂的功能, 它的实现是非常有技术性的。对于用户来说,只要用户输入的查询是正确的,那么Query Analyzer对用户就是透明的,只有当查询违反了SQL语法,超过用户权限或由于其他原因导致错误时,Query Analyzer才会主动提示用户;

一旦分析完成,处理并解析了查询中的所有标识符,Presto进入下一个阶段:Query Planning

二、Initial Query Planning

Query Planning可以看做是获取查询结果的流程,需要注意的是SQL是一种声明式的语言,即用户编写一个SQL来指定他们希望从系统获得的数据。 这与命令式程序有很大的不同,命令式程序通常需要指定如果处理数据,而使用SQL时,用户不指定如何处理数据以获得结果,这些步骤和顺序留给Query PlannerOptimizer来确定。

这一系列步骤通常称为Query Plan。理论上,很多的不同的Query Planning可以产生相同的查询结果,但彼此的性能可能会相差很大,这就是为什么Presto planner和Optimizer总是试图确定最优计划。我们将那些可以产生相同执行结果的计划称为:equivalent plans
让我们考虑本文最开始提到的那个SQL,关于这个SQL最简单的查询计划就是按照SQL查询语法结构进行规划,正如如实例2所示, 执行计划就是一棵树,它的执行从叶子节点开始,沿着树结构向上进行。

- Limit[5]
    - Sort[orders_sum DESC]
        - LateralJoin[2]
            - Aggregate[by nationkey...; orders_sum := sum(totalprice)]
                - Filter[c.nationkey = n.nationkey AND c.custkey = o.custkey]
                    - CrossJoin
                        - CrossJoin
                            - TableScan[nation]
                            - TableScan[orders]
                        - TableScan[customer]
                - EnforceSingleRow[region_name := r.name]
                    - Filter[r.regionkey = n.regionkey]
                        - TableScan[region]

查询计划的每个元素的具体实现都很简单,例如 :
***TableScan***访问表的底层存储并返回一个包含该表数据的结果集。
FilTer 会过滤掉数据中的一些行,值保留满足条件的行;
CrossJoin 对来自子节点的两个数据集进行操作, 它在这些数据集中生成所有行的组合,也可能将其中一个数据集存储在内存中,这样就不需要多次访问底层存储。

最新的Presto版本更改了查询计划中不同操作的命名。例如,TableScan 修改为 ScanProject,而Filter修改为FilterProject,但相应的功能没有改变。

现在让我们考虑这个查询计划的计算复杂度。在不知道所有实际数据细节的情况下,我们无法完全把握其复杂性。但是我们可以进行如下的假设:一个查询计划节点的复杂度的下限是他所生成数据的大小,即查询节点的复杂度与他生成的数据的行数正相关。因此我们使用Big Omega(Ω)来进行描述,表示最低限的近似值。如果 N,O,C以及R分别表示 nation,Orders,custoner以及region几张表里的行数,我们可以进行如下描述:

  • TableScan[orders]读取order表,返回了O行数据,所以他的复杂度是:Ω(O)。同理其他两个TableScans分别返回N行和C行数据;即Ω(N) 和Ω©
  • 在 TableScan[nation]和TableSca[orders]之上的CrossJoin 对来自nation和orders表的数据进行合并,他的复杂度是:Ω(N × O)
  • 在上一层的CrossJoin将读取customer数据的TableScan[Customer]和上一个复杂度为Ω(N × O)的CrossJoin的数据进行合并,复杂度为:Ω(N × O × C).
  • 位于底层的TableScan[region]复杂度为:Ω®。但是由于LateralJoin他被调用N次,N就是Aggregate返回的行数,所以他的复杂度是:Ω(R × N)
  • Sort操作需要对N行进行排序因此他花费的时间不能少于 N × log(N)

如果暂时不考虑其他成本,执行计划的消耗至少是:Ω[N + O + C + (N × O)+ (N × O × C) + (R × N) + (N × log(N))]
在不知相对表大小的情况下可以将其简化为Ω[(N × O × C) + (R × N) + (N × log(N))]
按照一般经验,region和nation通常很小,如果我们假设,region是最小的表,并且nation是第二小的表,那么我们可以忽略结果的第二部分和第三部分得到最终结果:Ω(N × O × C)

代数公式讲的差不多了,是时候看看这在实践中意味着什么了,让我们举个例子,一个广受欢迎的购物网站有来自200个nations的1亿用户,他们总共下了10亿份orders。那么这两个表的CrossJoin需要(20,000,000,000,000,000,000)行数据。 对于一个健壮的拥有100节点的中等集群,每个节点每秒处理100万行, 那么计算该查询对应的中间数据将花费63个世纪。

当然,Presto肯定不会去执行这样一个不切实际的计划。不过一个幼稚的计划也有他的作用。这个最初的计划可以作为SQL语法和查询优化二者之前的桥梁。 查询优化的作用是将初始计划转换为一个与之等效的计划,且转化后的计划可以在Presto集群资源有限的情况下尽可能快地完成执行任务,至少在合理的时间内完成执行任务。

下一篇文章我们讨论一下查询优化是如何达到这个目标的,未完待续…

展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客
应支付0元
点击重新获取
扫码支付

支付成功即可阅读