神秘莫测的 SQL 执行计划

政采云技术团队.png

南音.png

“神秘莫测”的Spark SQL执行计划

​ 作为一个数据仓库开发工程师,在工作中,我们除了要掌握建模过程,优化模型,任务分层等知识外,SQL 开发也是日常开发中重要的一环。SQL 写的好与不好,会直接影响到资源的利用和集群性能的问题。那我们如何去定位我们的 SQL 会不会有性能问题呢?

​ 在 Spark SQL 中,其实也不止 Spark SQL ,所有的 SQL 执行引擎都会提供执行计划给我们,让我们能直观看到我们的 SQL 到底是如何被执行的,是否按照我们的预期执行,然后我们可以针对性的进行优化。

​ 但是,对于不熟悉执行计划的人来说,特别是没有很深入用过 Spark Core 的人来说,执行计划看起来可能特别难懂。

​ 在这篇文章中,我会详细介绍执行计划中的每个点。

环境

​ 下面我们的执行计划基于这么一个SQL:

​ 此 SQL 意思就是获取每个用户在 2022 年的订单数量,同时和另一张表做关联,获取到更为详细的信息(注意⚠️:此处为了取所有字段使用了

  select *

实际生产过程中,是不允许这么写的 ,要补全所有字段名称),乍一看很简单的一个语句,那么大家是否清楚它的执行计划如何呢?下边我将一一道来。

执行计划

​ 我们可以通过调用 explain formatted 得到对应的物理执行计划:

​ explain 后面可以跟三个关键字: formatted , cost, codegen 中的一个。formatted 会返回一个更加直观和格式化的结果,cost 能让我们看到每一步要处理的数据量,codegen 会告诉我们将要执行的代码长什么样子。

​ 我们还可以通过 Spark UI 查看 SQL 的物理执行计划的图形表示:

​ 我们下面就来介绍每个术语代表的含义。

CollapseCodegenStages

​ 从上图中,我们能看到,操作符被划分成了一个个蓝框。每个蓝框都代表一个 codegen stage。如果了解 Spark,就能知道这就是一个 Stage。Spark 中根据是否进行 shuffle 划分 Stage。

​ CollapseCodegenStages 会将一些操作符合并到一起,这样能减少函数调用栈,从而提高资源的利用效率。但是,并非所有的操作符都能做合并,比如 Exchange(也就是shuffle)不支持。从物理计划的 codegen id 这部分输出,我们能判断操作符是否支持进行合并。如果支持,codegen id 后面会带对应 stage 的 id。

​ 那么,不同的操作符都是什么含义呢?

Scan parquet

​ 这个操作符代表从 Parquet 中读取数据。从这部分输出我们能看到会从 Parquet 中读取哪些列。因为 Parquet 是列式存储,所以只需要读取需要的列就好,这样能大幅提升性能。在这部分我们能看到有这么两个关键字: PartitionFilters 和 PushedFilters。PartitionFilters 代表我们要过滤掉哪些分区,而 PushedFilters 代表我们会将哪些操作下推给Parquet。也就是常说的执行下推。通过执行下推,我们可以在读数据时就过滤掉不需要的数据,从 Parquet 传输给 Spark 的数据量就会小,就能提升性能。

​ 如果列已经排过序,那么执行下推会发挥巨大的作用。因为 Parquet 的 Footer 会包含 min/max 等统计信息,Parquet 可以根据这个统计信息就能过滤掉数据,而不需要进入 Block 内部进行查找。排过序的话,就只需要读特定的 Block 就好了。而没有排过序的话,极端情况下需要读取全部 Block。

Filter

​ 这个操作符就如其名,就是过滤数据。但是它一般不对应 SQL 中的 where 条件。因为 Catalyst 优化器会进行优化:

​ • PushDownPredicates:Catalyst 会尽量将过滤条件下推。但是有些是下推不了的,因为下推是编译时能够确定的才能下推,而有些涉及到表达式计算的需要在执行时才能确定,比如 first 等操作,这些就下推不了。

​ • CombineFilters:会将两个能合并的算子合并成一个。

​ • InferFiltersFromConstraints:这个规则会创建一个新的过滤器。就比如我们上面 SQL 中的 Join 操作,会创建一个 user_id 不为空的过滤器。

​ • PruneFilters:删掉冗余的过滤器。

Project

​ 这个操作符表示要 select 哪些列。在从逻辑计划转变为物理计划之前,也会做一些优化:

​ • ColumnPruning:删掉不需要读取的列。

​ • CollapseProject:将两个 CollapseProject 合成为一个。

​ • PushProjectionThroughUnion:将 Project 下推给 union。

Exchange

​ 就是 shuffle。这部分会包括采用了哪种 Partitioner:

​ 上面这幅图的意思就是说,采用 HashPartitioner,将 user_id 按照 hash 分成 200 个分区。所有相同 user_id 的数据都会被放到一个分区中去。除了 HashPartitioner ,还有下面几种其它的 Partitioner:

​ • RoundRobinPartitioning:就是随机分区。了解负载均衡的同学应该很容易理解。

​ • SinglePartition:只有一个分区,全部数据都在一个分区。如果调用了窗口函数,导致整个 DataFrame 的数据都在一个窗口中,就会采用 SingleParititoner。

​ • RangePartitioning:用于对数据进行排序。Spark SQL 中的全局排序就是靠它。

HashAggregate

​ 表示数据聚合。一般会有两个 HashAggregate,中间夹着一个 Exchange。因为现在 Task 级别进行聚合,然后再全局聚合会提升性能。像 Spark 中的 reduceByKey,aggregateByKey,sortByKey 等都是这种思想。

BroadcastHashJoin & BroadcastExchange

​ 就是通过 Broadcast Join 的方式进行 Join。Broadcast Join 适用于一张表数据很小,一张表很大的情况。还有其它的一些 Join 方法,如 SortMergeJoin 和 ShuffleHashJoin。

​ Broadcast Join 总是会和 Broadcast Exchange 一起出现。

引发的思考

​ 我们来看下面的 SQL :

很简单,就是先根据分区过滤数据,然后做 count。我们看下它生成的执行计划:

*(2) HashAggregate(keys=[], functions=[count(1)])
+- Exchange SinglePartition
   +- *(1) HashAggregate(keys=[], functions=[partial_count(1)])
      +- *(1) Project
         +- *(1) FileScan parquet wrk.test_funnel_log_parquet_properties_20220401_20220410[event_id#56,pdate#57] Batched: true, Format: Parquet, Location: PrunedInMemoryFileIndex[hdfs://ddmcNS/user/hive/warehouse/wrk.db/test_funnel_log_parquet_properti..., PartitionCount: 44, PartitionFilters: [isnotnull(pdate#57), (pdate#57 >= 2022-04-01), (pdate#57 <= 2022-04-04), event_id#56 INSET (559,..., PushedFilters: [], ReadSchema: struct<>

​ 很容易理解,先根据分区字段进行过滤,然后读 Parquet 文件,然后每个 Task 本地做局部 count,经过一次 Shuffle 之后再做一个全局排序。

​ 了解 Parquet 文件都明白,Parquet 中每个 RowGroup 是保存了行数的:

​ 那为什么 Spark 不在读 Parquet 文件的时候,直接读 num_rows 就好了,还要读整个 parquet 文件再去计算行数呢?这个问题我暂时还没找到答案,我认为这儿是有优化空间的。

总结

​ 今天介绍了 SparkSQL 的物理执行计划、一些具体的参数效果和实际例子以及自己的一点思考,希望对大家有所帮助,也祝愿大家后面能通过解析物理执行计划把自己的 SQL 优化的效率杠杠的,永无 BUG!

参考文献

SPARK 官网

Spark Project SQL 3.2.1 API

推荐阅读

人工智能 NLP 简述

浅析 ElasticJob-Lite 3.x 定时任务

雪花算法详解

基于流量域的数据全链路治理

招贤纳士

政采云技术团队(Zero),一个富有激情、创造力和执行力的团队,Base 在风景如画的杭州。团队现有300多名研发小伙伴,既有来自阿里、华为、网易的“老”兵,也有来自浙大、中科大、杭电等校的新人。团队在日常业务开发之外,还分别在云原生、区块链、人工智能、低代码平台、中间件、大数据、物料体系、工程平台、性能体验、可视化等领域进行技术探索和实践,推动并落地了一系列的内部技术产品,持续探索技术的新边界。此外,团队还纷纷投身社区建设,目前已经是 google flutter、scikit-learn、Apache Dubbo、Apache Rocketmq、Apache Pulsar、CNCF Dapr、Apache DolphinScheduler、alibaba Seata 等众多优秀开源社区的贡献者。如果你想改变一直被事折腾,希望开始折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊……如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的技术团队的成长过程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 zcy-tc@cai-inc.com

微信公众号

文章同步发布,政采云技术团队公众号,欢迎关注

政采云技术团队.png