Data Warehouse

Presto Query Planing

2020-05-22  本文已影响0人  雨钓Moowei

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

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

-- 实例一:
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所示:
子查询:(SELECT name FROM region WHERE regionkey = n.regionkey)目的是从region表中提取region_name

Parsing and Analysis

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

确认查询中的Tables

表是根据catalogs以及Schemas进行组织的,因此多个表可以具有相同的名字,例如,TPC-H数据提供多个达标不同的orders表,但是他们在不同的Schema下面:sf10.orders 以及 sf100.orders
标识查询中使用的colums
如SQL中所示,orders.totalprice即明确的引用了order表中的totalprice 列,当SQL中Table中相同字段时,通常直接写Column名就可以,Presto Analyzer会确定Column来自哪个表。

确定ROW中Field的引用

 一个废弃的表达式:c.bonus可能引用c表中的bonus列,但也可能是引用c 列中的bonus field(带有命名field的结构),这个分析工作主要由Presto决定,且当有冲突时,列优先, 析需要遵循SQL语言的作用域和可见性规则, 收集到的信息,比如标识符消歧,稍后在规划过程中使用, 这样planner 就不需要再次理解理解查询语言的规则。

如您所见,Query Analyzer具有复杂的横切功能, 它的角色是非常技术性的,并且从用户的角度来看,只要查询是正确的,它对用户就是透明的,只有当查询违反SQL语法、超过用户权限或由于其他原因不正常时,Query Analyzer才会提示用户;

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

Initial Query Planning

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

这一系列步骤通常称为Query Plan。理论上,很多的查询计划可以产生相同的查询结果,但性能可能会相差很大,这就是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几张表里的行的数目,我们可以进行如下描述:

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

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

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

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

上一篇下一篇

猜你喜欢

热点阅读