浅谈Spark SQL语句解析与基于规则优化(RBO)

前言

近些年来,大数据领域“SQL化开发”的理念蔚然成风,这是因为SQL是一种通用、学习成本低的语言,并且还有较强的数据描述能力。不少大数据框架早已支持了SQL化开发,如Spark、Flink、Kafka等。

之前笔者操刀的多数Spark程序都是用传统的RDD API写的,Spark SQL用得很少,Flink也如是。最近抽空对这两门“SQL”做了一些了解,并且断断续续研究了Spark SQL的部分原理,了解到它的内部也是存在基于规则优化(Rule-based optimization, RBO)和基于代价优化(Cost-based optimization, CBO)的,与传统关系型数据库和大数据领域的有些组件(Hive/Presto等)异曲同工。

Spark SQL的核心是Catalyst,即它专属的查询解析和优化器。下面是Spark SQL原始论文中给出的Catalyst执行流程图。

可见主要分为语句解析、逻辑计划与优化、物理计划与优化、代码生成4个阶段,前3个阶段都由Catalyst负责。其中,逻辑计划的优化采用RBO的思路,物理计划的优化则采用CBO的思路。本文只来看RBO,顺便也介绍一下它之前的语句解析、逻辑计划过程,并不会具体到源码分析的级别。物理计划与CBO比起逻辑计划与RBO更加灵活和复杂,等忙过这一阵之后择期再写。

SQL语句解析

Catalyst使用开源的语法分析器Antlr解析SQL语句,并生成未解析的逻辑计划(Unresolved Logical Plan),对应到源码中的类为SqlBaseLexer和SqlBaseParser。

未解析的逻辑计划其实就是一棵原生的抽象语法树(Abstract Syntax Tree, AST),只与语句本身有关,而与表的元数据没有任何关系。用以下基于TPC-H数据集的查询为例,在其Q3查询的基础上简化而来。TPC-H数据集的导入可以参见这篇文章

select avg(revenue) from (
  select l_extendedprice * (100 - 99 - l_discount) as revenue
  from tpch.customer c 
  join tpch.orders o on c.c_mktsegment = 'BUILDING' and c.c_custkey = o.o_custkey 
  join tpch.lineitem l on l.l_orderkey = o.o_orderkey
  where o_orderdate <= '1995-03-17' and l_shipdate >= '1995-03-18'
) temp;

调用SparkSession.sql().explain(true)方法,查看执行计划。下面就是未解析的逻辑计划的全貌。

== Parsed Logical Plan ==
'Project [unresolvedalias('avg('revenue), None)]
+- 'SubqueryAlias temp
   +- 'Project [('l_extendedprice * ((100 - 99) - 'l_discount)) AS revenue#0]
      +- 'Filter (('o_orderdate <= 1995-03-17) && ('l_shipdate >= 1995-03-18))
         +- 'Join Inner, ('l.l_orderkey = 'o.o_orderkey)
            :- 'Join Inner, (('c.c_mktsegment = BUILDING) && ('c.c_custkey = 'o.o_custkey))
            :  :- 'SubqueryAlias c
            :  :  +- 'UnresolvedRelation `tpch`.`customer`
            :  +- 'SubqueryAlias o
            :     +- 'UnresolvedRelation `tpch`.`orders`
            +- 'SubqueryAlias l
               +- 'UnresolvedRelation `tpch`.`lineitem`

如果这样不容易阅读的话,我们手动将这棵抽象语法树画出来,就简明得多。别名逻辑操作符(SubqueryAlias)就不画了。

由上图可见,所有的表都用UnresolvedRelation来表示,也就是仅仅知道它们是表,而对其他信息(表的结构、数据类型、存储位置等等)都一无所知,Project、Filter等操作符中的列名对应的信息自然也是不清楚的。这些东西都要在生成逻辑计划的同时弄明白。

逻辑计划生成

逻辑计划的生成由Analyzer类来实现,它利用SessionCatalog(具体到这里就是Hive的Catalog,即元数据集合)将上一节AST中所有Unresolved的东西解析出来。解析完毕的逻辑计划如下所示。

== Analyzed Logical Plan ==
avg(revenue): double
Aggregate [avg(revenue#0) AS avg(revenue)#72]
+- SubqueryAlias temp
   +- Project [(l_extendedprice#60 * (cast((100 - 99) as double) - l_discount#61)) AS revenue#0]
      +- Filter ((cast(o_orderdate#50 as string) <= 1995-03-17) && (l_shipdate#65 >= 1995-03-18))
         +- Join Inner, (l_orderkey#55 = o_orderkey#46)
            :- Join Inner, ((c_mktsegment#44 = BUILDING) && (c_custkey#38 = o_custkey#47))
            :  :- SubqueryAlias c
            :  :  +- SubqueryAlias customer
            :  :     +- HiveTableRelation `tpch`.`customer`, org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe, [c_custkey#38, c_name#39, c_address#40, c_nationkey#41, c_phone#42, c_acctbal#43, c_mktsegment#44, c_comment#45]
            :  +- SubqueryAlias o
            :     +- SubqueryAlias orders
            :        +- HiveTableRelation `tpch`.`orders`, org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe, [o_orderkey#46, o_custkey#47, o_orderstatus#48, o_totalprice#49, o_orderdate#50, o_orderpriority#51, o_clerk#52, o_shippriority#53, o_comment#54]
            +- SubqueryAlias l
               +- SubqueryAlias lineitem
                  +- HiveTableRelation `tpch`.`lineitem`, org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe, [l_orderkey#55, l_partkey#56, l_suppkey#57, l_linenumber#58, l_quantity#59, l_extendedprice#60, l_discount#61, l_tax#62, l_returnflag#63, l_linestatus#64, l_shipdate#65, l_commitdate#66, l_receiptdate#67, l_shipinstruct#68, l_shipmode#69, l_comment#70]

解析出来的东西有:

  • 各表的元数据(HiveTableRelation)及包含的字段;
  • 聚合操作及对应的函数(Aggregate、avg);
  • 各字段的数据类型与类型转换(as double、as string)。

用树形结构表示如下图。

接下来就要靠RBO对这棵树进行优化了。

基于规则优化

所谓基于规则优化,就是指通过一系列预先定义好的规则(Rule)对逻辑计划进行等价转换,以提高查询效率。

RBO的两个主要思路是:减少参与计算的数据量、降低重复计算的代价。RBO相对于CBO而言要成熟得多,常用的规则都基于经验制定,可以覆盖大部分查询场景,并且方便扩展。其缺点则是不够灵活,毕竟这个阶段对物理上的特征(如表的底层存储形式和真正的数据量)还没有感知。

下面先列出文章开头的查询优化过的逻辑计划。

== Optimized Logical Plan ==
Aggregate [avg(revenue#0) AS avg(revenue)#72]
+- Project [(l_extendedprice#60 * (1.0 - l_discount#61)) AS revenue#0]
   +- Join Inner, (l_orderkey#55 = o_orderkey#46)
      :- Project [o_orderkey#46]
      :  +- Join Inner, (c_custkey#38 = o_custkey#47)
      :     :- Project [c_custkey#38]
      :     :  +- Filter ((isnotnull(c_mktsegment#44) && (c_mktsegment#44 = BUILDING)) && isnotnull(c_custkey#38))
      :     :     +- HiveTableRelation `tpch`.`customer`, org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe, [c_custkey#38, c_name#39, c_address#40, c_nationkey#41, c_phone#42, c_acctbal#43, c_mktsegment#44, c_comment#45]
      :     +- Project [o_orderkey#46, o_custkey#47]
      :        +- Filter (((isnotnull(o_orderdate#50) && (cast(o_orderdate#50 as string) <= 1995-03-17)) && isnotnull(o_custkey#47)) && isnotnull(o_orderkey#46))
      :           +- HiveTableRelation `tpch`.`orders`, org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe, [o_orderkey#46, o_custkey#47, o_orderstatus#48, o_totalprice#49, o_orderdate#50, o_orderpriority#51, o_clerk#52, o_shippriority#53, o_comment#54]
      +- Project [l_orderkey#55, l_extendedprice#60, l_discount#61]
         +- Filter ((isnotnull(l_shipdate#65) && (l_shipdate#65 >= 1995-03-18)) && isnotnull(l_orderkey#55))
            +- HiveTableRelation `tpch`.`lineitem`, org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe, [l_orderkey#55, l_partkey#56, l_suppkey#57, l_linenumber#58, l_quantity#59, l_extendedprice#60, l_discount#61, l_tax#62, l_returnflag#63, l_linestatus#64, l_shipdate#65, l_commitdate#66, l_receiptdate#67, l_shipinstruct#68, l_shipmode#69, l_comment#70]

优化过的逻辑计划与原本的逻辑计划相比有了很大变化。为了对比清晰,将两棵树都画在下面了。

上面的图中包含了3种最常见也是最有效的RBO方式,分别简单阐述一下。英文名称是Spark SQL源码中的字段名称。

  • 常量折叠(ConstantFolding)
    上述语句中有一个纯常量运算表达式,即100 - 99。如果行数很多的话,每行都要计算一次该表达式的值,积少成多就浪费了很多时间(因为该表达式可以更加复杂)。所以通过常量折叠可以将它预先转化为1.0,消除了很多不必要的重复计算。图中红色箭头即是。

  • 谓词下推(PushdownPredicate)
    谓词下推的概念在前面讲解HiveQL优化时已经说过了。如果能够将SQL语句中的谓词逻辑(where条件、join on中的谓词条件)都尽量提前执行,下游处理已经过滤完毕的数据,能够减少工作量。图中绿色箭头即是。

  • 列裁剪(ColumnPruning)
    在未优化的逻辑计划中,Join Inner与Filter操作符都会扫描很多列,然后再由Project操作符筛选出结果列。但实际上,我们可以在初始单独扫描表时就只筛选出符合后续逻辑计划的最小列集合,同样能够节省很多资源。如果表物理上是用Parquet、ORC等列式存储格式持久化的,效率就会更高。图中所有标为橙色的Project操作符即是。

To be continued

又多了一个坑 _(:з」∠)_

并且Spark源码的专栏也好久没写了啊~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,589评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,615评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,933评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,976评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,999评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,775评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,474评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,359评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,854评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,007评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,146评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,826评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,484评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,029评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,153评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,420评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,107评论 2 356

推荐阅读更多精彩内容