深入理解SQL中的GroupingSets语句

原创 元闰子的邀请 2022-07-03 12:17

前言

SQL 中 Group By 语句大家都很熟悉,根据指定的规则对数据进行分组,常常和聚合函数一起使用。

比如,考虑有表 dealer,表中数据如下:

id (Int)city (String)car_model (String)quantity (Int)
100FremontHonda Civic10
100FremontHonda Accord15
100FremontHonda CRV7
200DublinHonda Civic20
200DublinHonda Accord10
200DublinHonda CRV3
300San JoseHonda Civic5
300San JoseHonda Accord8

如果执行 SQL 语句 SELECT id, sum(quantity) FROM dealer GROUP BY id ORDER BY id,会得到如下结果:

+---+-------------+
| id|sum(quantity)|
+---+-------------+
|100|           32|
|200|           33|
|300|           13|
+---+-------------+

上述 SQL 语句的意思就是对数据按 id 列进行分组,然后在每个分组内对 quantity 列进行求和。

Group By 语句除了上面的简单用法之外,还有更高级的用法,常见的是 Grouping SetsRollUp 和 Cube,它们在 OLAP 时比较常用。其中,RollUp 和 Cube 都是以 Grouping Sets 为基础实现的,因此,弄懂了 Grouping Sets,也就理解了 RollUp 和 Cube 。

本文首先简单介绍 Grouping Sets 的用法,然后以 Spark SQL 作为切入点,深入解析 Grouping Sets 的实现机制。

Spark SQL 是 Apache Spark 大数据处理框架的一个子模块,用来处理结构化信息。它可以将 SQL 语句翻译多个任务在 Spark 集群上执行,允许用户直接通过 SQL 来处理数据,大大提升了易用性。

Grouping Sets 简介

Spark SQL 官方文档中 SQL Syntax 一节对 Grouping Sets 语句的描述如下:

Groups the rows for each grouping set specified after GROUPING SETS. (... 一些举例) This clause is a shorthand for a UNION ALLwhere each leg of the UNION ALL operator performs aggregation of each grouping set specified in the GROUPING SETS clause. (... 一些举例)

也即,Grouping Sets 语句的作用是指定几个 grouping set 作为 Group By 的分组规则,然后再将结果联合在一起。它的效果和,先分别对这些 grouping set 进行 Group By 分组之后,再通过 Union All 将结果联合起来,是一样的。

比如,对于 dealer 表,Group By Grouping Sets ((city, car_model), (city), (car_model), ()) 和 Union All((Group By city, car_model), (Group By city), (Group By car_model), 全局聚合) 的效果是相同的:

先看 Grouping Sets 版的执行结果:

spark-sql> SELECT city, car_model, sum(quantity) AS sum FROM dealer 
         > GROUP BY GROUPING SETS ((city, car_model), (city), (car_model), ()) 
         > ORDER BY city, car_model;
+--------+------------+---+
|    city|   car_model|sum|
+--------+------------+---+
|    null|        null| 78|
|    null|Honda Accord| 33|
|    null|   Honda CRV| 10|
|    null| Honda Civic| 35|
|  Dublin|        null| 33|
|  Dublin|Honda Accord| 10|
|  Dublin|   Honda CRV|  3|
|  Dublin| Honda Civic| 20|
| Fremont|        null| 32|
| Fremont|Honda Accord| 15|
| Fremont|   Honda CRV|  7|
| Fremont| Honda Civic| 10|
|San Jose|        null| 13|
|San Jose|Honda Accord|  8|
|San Jose| Honda Civic|  5|
+--------+------------+---+

再看 Union All 版的执行结果:

spark-sql> (SELECT city, car_model, sum(quantity) AS sum FROM dealer GROUP BY city, car_model) UNION ALL 
         > (SELECT city, NULL as car_model, sum(quantity) AS sum FROM dealer GROUP BY city) UNION ALL 
         > (SELECT NULL as city, car_model, sum(quantity) AS sum FROM dealer GROUP BY car_model) UNION ALL 
         > (SELECT NULL as city, NULL as car_model, sum(quantity) AS sum FROM dealer) 
         > ORDER BY city, car_model;
+--------+------------+---+
|    city|   car_model|sum|
+--------+------------+---+
|    null|        null| 78|
|    null|Honda Accord| 33|
|    null|   Honda CRV| 10|
|    null| Honda Civic| 35|
|  Dublin|        null| 33|
|  Dublin|Honda Accord| 10|
|  Dublin|   Honda CRV|  3|
|  Dublin| Honda Civic| 20|
| Fremont|        null| 32|
| Fremont|Honda Accord| 15|
| Fremont|   Honda CRV|  7|
| Fremont| Honda Civic| 10|
|San Jose|        null| 13|
|San Jose|Honda Accord|  8|
|San Jose| Honda Civic|  5|
+--------+------------+---+

两版的查询结果完全一样。

Grouping Sets 的执行计划

从执行结果上看,Grouping Sets 版本和 Union All 版本的 SQL 是等价的,但 Grouping Sets 版本更加简洁。

那么,Grouping Sets 仅仅只是 Union All 的一个缩写,或者语法糖吗

为了进一步探究 Grouping Sets 的底层实现是否和 Union All 是一致的,我们可以来看下两者的执行计划。

首先,我们通过 explain extended 来查看 Union All 版本的 Optimized Logical Plan:

spark-sql> explain extended (SELECT city, car_model, sum(quantity) AS sum FROM dealer GROUP BY city, car_model) UNION ALL (SELECT city, NULL as car_model, sum(quantity) AS sum FROM dealer GROUP BY city) UNION ALL (SELECT NULL as city, car_model, sum(quantity) AS sum FROM dealer GROUP BY car_model) UNION ALL (SELECT NULL as city, NULL as car_model, sum(quantity) AS sum FROM dealer) ORDER BY city, car_model;
== Parsed Logical Plan ==
...
== Analyzed Logical Plan ==
...
== Optimized Logical Plan ==
Sort [city#93 ASC NULLS FIRST, car_model#94 ASC NULLS FIRST], true
+- Union falsefalse
   :- Aggregate [city#93, car_model#94], [city#93, car_model#94, sum(quantity#95AS sum#79L]
   :  +- Project [city#93, car_model#94, quantity#95]
   :     +- HiveTableRelation [`default`.`dealer`, ..., Data Cols: [id#92, city#93, car_model#94, quantity#95], Partition Cols: []]
   :- Aggregate [city#97], [city#97null AS car_model#112, sum(quantity#99AS sum#81L]
   :  +- Project [city#97, quantity#99]
   :     +- HiveTableRelation [`default`.`dealer`, ..., Data Cols: [id#96, city#97, car_model#98, quantity#99], Partition Cols: []]
   :- Aggregate [car_model#102], [null AS city#113, car_model#102, sum(quantity#103AS sum#83L]
   :  +- Project [car_model#102, quantity#103]
   :     +- HiveTableRelation [`default`.`dealer`, ..., Data Cols: [id#100, city#101, car_model#102, quantity#103], Partition Cols: []]
   +- Aggregate [null AS city#114null AS car_model#115, sum(quantity#107AS sum#86L]
      +- Project [quantity#107]
         +- HiveTableRelation [`default`.`dealer`, ..., Data Cols: [id#104, city#105, car_model#106, quantity#107], Partition Cols: []]
== Physical Plan ==
...

从上述的 Optimized Logical Plan 可以清晰地看出 Union All 版本的执行逻辑:

  1. 执行每个子查询语句,计算得出查询结果。其中,每个查询语句的逻辑是这样的:
  • 在 HiveTableRelation 节点对 dealer 表进行全表扫描。
  • 在 Project 节点选出与查询语句结果相关的列,比如对于子查询语句 SELECT NULL as city, NULL as car_model, sum(quantity) AS sum FROM dealer,只需保留 quantity 列即可。
  • 在 Aggregate 节点完成 quantity 列对聚合运算。在上述的 Plan 中,Aggregate 后面紧跟的就是用来分组的列,比如 Aggregate [city#902] 就表示根据 city 列来进行分组。
  • 在 Union 节点完成对每个子查询结果的联合。
  • 最后,在 Sort 节点完成对数据的排序,上述 Plan 中 Sort [city#93 ASC NULLS FIRST, car_model#94 ASC NULLS FIRST] 就表示根据 city 和 car_model 列进行升序排序。
  • 接下来,我们通过 explain extended 来查看 Grouping Sets 版本的 Optimized Logical Plan:

    spark-sql> explain extended SELECT city, car_model, sum(quantity) AS sum FROM dealer GROUP BY GROUPING SETS ((city, car_model), (city), (car_model), ()) ORDER BY city, car_model;
    == Parsed Logical Plan ==
    ...
    == Analyzed Logical Plan ==
    ...
    == Optimized Logical Plan ==
    Sort [city#138 ASC NULLS FIRST, car_model#139 ASC NULLS FIRST], true
    +- Aggregate [city#138, car_model#139, spark_grouping_id#137L], [city#138, car_model#139, sum(quantity#133AS sum#124L]
       +- Expand [[quantity#133, city#131, car_model#1320], [quantity#133, city#131null1], [quantity#133null, car_model#1322], [quantity#133nullnull3]], [quantity#133, city#138, car_model#139, spark_grouping_id#137L]
          +- Project [quantity#133, city#131, car_model#132]
             +- HiveTableRelation [`default`.`dealer`, org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDeData Cols: [id#130, city#131, car_model#132, quantity#133], Partition Cols: []]
    == Physical Plan ==
    ...

    从 Optimized Logical Plan 来看,Grouping Sets 版本要简洁很多!具体的执行逻辑是这样的:

    1. 在 HiveTableRelation 节点对 dealer 表进行全表扫描。
    2. 在 Project 节点选出与查询语句结果相关的列。
    3. 接下来的 Expand 节点是关键,数据经过该节点后,多出了 spark_grouping_id 列。从 Plan 中可以看出来,Expand 节点包含了 Grouping Sets 里的各个 grouping set 信息,比如 [quantity#133, city#131, null, 1] 对应的就是 (city) 这一 grouping set。而且,每个 grouping set 对应的 spark_grouping_id 列的值都是固定的,比如 (city) 对应的 spark_grouping_id 为 1
    4. 在 Aggregate 节点完成 quantity 列对聚合运算,其中分组的规则为 city, car_model, spark_grouping_id。注意,数据经过 Aggregate 节点后,spark_grouping_id 列被删除了!
    5. 最后,在 Sort 节点完成对数据的排序。

    从 Optimized Logical Plan 来看,虽然 Union All 版本和 Grouping Sets 版本的效果一致,但它们的底层实现有着巨大的差别。

    其中,Grouping Sets 版本的 Plan 中最关键的是 Expand 节点,目前,我们只知道数据经过它之后,多出了 spark_grouping_id 列。而且从最终结果来看,spark_grouping_id只是 Spark SQL 的内部实现细节,对用户并不体现。那么:

    1. Expand 的实现逻辑是怎样的,为什么能达到 Union All 的效果?
    2. Expand 节点的输出数据是怎样的
    3. spark_grouping_id 列的作用是什么

    通过 Physical Plan,我们发现 Expand 节点对应的算子名称也是 Expand:

    == Physical Plan ==
    AdaptiveSparkPlan isFinalPlan=false
    +- Sort [city#138 ASC NULLS FIRST, car_model#139 ASC NULLS FIRST], true0
       +- Exchange rangepartitioning(city#138 ASC NULLS FIRST, car_model#139 ASC NULLS FIRST200), ENSURE_REQUIREMENTS, [plan_id=422]
          +- HashAggregate(keys=[city#138, car_model#139, spark_grouping_id#137L], functions=[sum(quantity#133)], output=[city#138, car_model#139, sum#124L])
             +- Exchange hashpartitioning(city#138, car_model#139, spark_grouping_id#137L, 200), ENSURE_REQUIREMENTS, [plan_id=419]
                +- HashAggregate(keys=[city#138, car_model#139, spark_grouping_id#137L], functions=[partial_sum(quantity#133)], output=[city#138, car_model#139, spark_grouping_id#137L, sum#141L])
                   +- Expand [[quantity#133, city#131, car_model#1320], [quantity#133, city#131null1], [quantity#133null, car_model#1322], [quantity#133nullnull3]], [quantity#133, city#138, car_model#139, spark_grouping_id#137L]
                      +- Scan hive default.dealer [quantity#133, city#131, car_model#132], HiveTableRelation [`default`.`dealer`, ..., Data Cols: [id#130, city#131, car_model#132, quantity#133], Partition Cols: []]

    带着前面的几个问题,接下来我们深入 Spark SQL 的 Expand 算子源码寻找答案。

    Expand 算子的实现

    Expand 算子在 Spark SQL 源码中的实现为 ExpandExec 类(Spark SQL 中的算子实现类的命名都是 XxxExec 的格式,其中 Xxx 为具体的算子名,比如 Project 算子的实现类为 ProjectExec),核心代码如下:

    /**
     * Apply all of the GroupExpressions to every input row, hence we will get
     * multiple output rows for an input row.
     * @param projections The group of expressions, all of the group expressions should
     *                    output the same schema specified bye the parameter `output`
     * @param output      The output Schema
     * @param child       Child operator
     */

    case class ExpandExec(
        projections: Seq[Seq[Expression]],
        output: Seq[Attribute],
        child: SparkPlan
    )

      extends UnaryExecNode with CodegenSupport {

      ...
      // 关键点1,将child.output,也即上游算子输出数据的schema,
      // 绑定到表达式数组exprs,以此来计算输出数据
      private[thisval projection =
        (exprs: Seq[Expression]) => UnsafeProjection.create(exprs, child.output)

      // doExecute()方法为Expand算子执行逻辑所在
      protected override def doExecute(): RDD[InternalRow] = {
        val numOutputRows = longMetric("numOutputRows")

        // 处理上游算子的输出数据,Expand算子的输入数据就从iter迭代器获取
        child.execute().mapPartitions { iter =>
          // 关键点2,projections对应了Grouping Sets里面每个grouping set的表达式,
          // 表达式输出数据的schema为this.output, 比如 (quantity, city, car_model, spark_grouping_id)
          // 这里的逻辑是为它们各自生成一个UnsafeProjection对象,通过该对象的apply方法就能得出Expand算子的输出数据
          val groups = projections.map(projection).toArray
          new Iterator[InternalRow] {
            private[thisvar result: InternalRow = _
            private[thisvar idx = -1  // -1 means the initial state
            private[thisvar input: InternalRow = _

            override final def hasNextBoolean = (-1 < idx && idx < groups.length) || iter.hasNext

            override final def next(): InternalRow = {
              // 关键点3,对于输入数据的每一条记录,都重复使用N次,其中N的大小对应了projections数组的大小,
              // 也即Grouping Sets里指定的grouping set的数量
              if (idx <= 0) {
                // in the initial (-1) or beginning(0) of a new input row, fetch the next input tuple
                input = iter.next()
                idx = 0
              }
              // 关键点4,对输入数据的每一条记录,通过UnsafeProjection计算得出输出数据,
              // 每个grouping set对应的UnsafeProjection都会对同一个input计算一遍
              result = groups(idx)(input)
              idx += 1

              if (idx == groups.length && iter.hasNext) {
                idx = 0
              }

              numOutputRows += 1
              result
            }
          }
        }
      }
      ...
    }

    ExpandExec 的实现并不复杂,想要理解它的运作原理,关键是看懂上述源码中提到的 4 个关键点。

    关键点 1 和 关键点 2 是基础,关键点 2 中的 groups 是一个 UnsafeProjection[N] 数组类型,其中每个 UnsafeProjection 代表了 Grouping Sets 语句里指定的 grouping set,它的定义是这样的:

    // A projection that returns UnsafeRow.
    abstract class UnsafeProjection extends Projection {
      override def apply(row: InternalRow): UnsafeRow
    }

    // The factory object for `UnsafeProjection`.
    object UnsafeProjection
        extends CodeGeneratorWithInterpretedFallback[Seq[Expression], UnsafeProjection] {
      // Returns an UnsafeProjection for given sequence of Expressions, which will be bound to
      // `inputSchema`.
      def create(exprs: Seq[Expression], inputSchema: Seq[Attribute]): UnsafeProjection = {
        create(bindReferences(exprs, inputSchema))
      }
      ...
    }

    UnsafeProjection 起来了类似列投影的作用,其中, apply 方法根据创建时的传参 exprs 和 inputSchema,对输入记录进行列投影,得出输出记录。

    比如,前面的 GROUPING SETS ((city, car_model), (city), (car_model), ())例子,它对应的 groups 是这样的:

    其中,AttributeReference 类型的表达式,在计算时,会直接引用输入数据对应列的值;Iteral 类型的表达式,在计算时,值是固定的。

    关键点 3 和 关键点 4 是 Expand 算子的精华所在,ExpandExec 通过这两段逻辑,将每一个输入记录,扩展(Expand)成 N 条输出记录。

    关键点 4 中 groups(idx)(input) 等同于 groups(idx).apply(input)

    还是以前面 GROUPING SETS ((city, car_model), (city), (car_model), ()) 为例子,效果是这样的:

    到这里,我们已经弄清楚 Expand 算子的工作原理,再回头看前面提到的 3 个问题,也不难回答了:

    1. Expand 的实现逻辑是怎样的,为什么能达到 Union All 的效果?

      如果说 Union All 是先聚合再联合,那么 Expand 就是先联合再聚合。Expand 利用 groups 里的 N 个表达式对每条输入记录进行计算,扩展成 N 条输出记录。后面再聚合时,就能达到与 Union All 一样的效果了。

    2. Expand 节点的输出数据是怎样的

      在 schema 上,Expand 输出数据会比输入数据多出 spark_grouping_id 列;在记录数上,是输入数据记录数的 N 倍。

    3. spark_grouping_id 列的作用是什么

      spark_grouping_id 给每个 grouping set 进行编号,这样,即使在 Expand 阶段把数据先联合起来,在 Aggregate 阶段(把 spark_grouping_id 加入到分组规则)也能保证数据能够按照每个 grouping set 分别聚合,确保了结果的正确性。

    查询性能对比

    从前文可知,Grouping Sets 和 Union All 两个版本的 SQL 语句有着一样的效果,但是它们的执行计划却有着巨大的差别。下面,我们将比对两个版本之间的执行性能差异。

    spark-sql 执行完 SQL 语句之后会打印耗时信息,我们对两个版本的 SQL 分别执行 10 次,得到如下信息:

    // Grouping Sets 版本执行10次的耗时信息
    // SELECT city, car_model, sum(quantity) AS sum FROM dealer GROUP BY GROUPING SETS ((city, car_model), (city), (car_model), ()) ORDER BY city, car_model;
    Time taken: 0.289 seconds, Fetched 15 row(s)
    Time taken: 0.251 seconds, Fetched 15 row(s)
    Time taken: 0.259 seconds, Fetched 15 row(s)
    Time taken: 0.258 seconds, Fetched 15 row(s)
    Time taken: 0.296 seconds, Fetched 15 row(s)
    Time taken: 0.247 seconds, Fetched 15 row(s)
    Time taken: 0.298 seconds, Fetched 15 row(s)
    Time taken: 0.286 seconds, Fetched 15 row(s)
    Time taken: 0.292 seconds, Fetched 15 row(s)
    Time taken: 0.282 seconds, Fetched 15 row(s)

    // Union All 版本执行10次的耗时信息
    // (SELECT city, car_model, sum(quantity) AS sum FROM dealer GROUP BY city, car_model) UNION ALL (SELECT city, NULL as car_model, sum(quantity) AS sum FROM dealer GROUP BY city) UNION ALL (SELECT NULL as city, car_model, sum(quantity) AS sum FROM dealer GROUP BY car_model) UNION ALL (SELECT NULL as city, NULL as car_model, sum(quantity) AS sum FROM dealer) ORDER BY city, car_model;
    Time taken: 0.628 seconds, Fetched 15 row(s)
    Time taken: 0.594 seconds, Fetched 15 row(s)
    Time taken: 0.591 seconds, Fetched 15 row(s)
    Time taken: 0.607 seconds, Fetched 15 row(s)
    Time taken: 0.616 seconds, Fetched 15 row(s)
    Time taken: 0.64 seconds, Fetched 15 row(s)
    Time taken: 0.623 seconds, Fetched 15 row(s)
    Time taken: 0.625 seconds, Fetched 15 row(s)
    Time taken: 0.62 seconds, Fetched 15 row(s)
    Time taken: 0.62 seconds, Fetched 15 row(s)

    可以算出,Grouping Sets 版本的 SQL 平均耗时为 0.276s;Union All 版本的 SQL 平均耗时为 0.616s,是前者的 2.2 倍

    所以,Grouping Sets 版本的 SQL 不仅在表达上更加简洁,在性能上也更加高效

    RollUp 和 Cube

    Group By 的高级用法中,还有 RollUp 和 Cube 两个比较常用。

    首先,我们看下 RollUp 语句

    Spark SQL 官方文档中 SQL Syntax 一节对 RollUp 语句的描述如下:

    Specifies multiple levels of aggregations in a single statement. This clause is used to compute aggregations based on multiple grouping sets. ROLLUP is a shorthand for GROUPING SETS. (... 一些例子)

    官方文档中,把 RollUp 描述为 Grouping Sets 的简写,等价规则为:RollUp(A, B, C) == Grouping Sets((A, B, C), (A, B), (A), ())

    比如,Group By RollUp(city, car_model) 就等同于 Group By Grouping Sets((city, car_model), (city), ())

    下面,我们通过 expand extended 看下 RollUp 版本 SQL 的 Optimized Logical Plan:

    spark-sql> explain extended SELECT city, car_model, sum(quantity) AS sum FROM dealer GROUP BY ROLLUP(city, car_model) ORDER BY city, car_model;
    == Parsed Logical Plan ==
    ...
    == Analyzed Logical Plan ==
    ...
    == Optimized Logical Plan ==
    Sort [city#2164 ASC NULLS FIRST, car_model#2165 ASC NULLS FIRST], true
    +- Aggregate [city#2164, car_model#2165, spark_grouping_id#2163L], [city#2164, car_model#2165, sum(quantity#2159AS sum#2150L]
       +- Expand [[quantity#2159, city#2157, car_model#21580], [quantity#2159, city#2157null1], [quantity#2159nullnull3]], [quantity#2159, city#2164, car_model#2165, spark_grouping_id#2163L]
          +- Project [quantity#2159, city#2157, car_model#2158]
             +- HiveTableRelation [`default`.`dealer`, ..., Data Cols: [id#2156, city#2157, car_model#2158, quantity#2159], Partition Cols: []]
    == Physical Plan ==
    ...

    从上述 Plan 可以看出,RollUp 底层实现用的也是 Expand 算子,说明 RollUp 确实是基于 Grouping Sets 实现的。 而且 Expand [[quantity#2159, city#2157, car_model#2158, 0], [quantity#2159, city#2157, null, 1], [quantity#2159, null, null, 3]] 也表明 RollUp 符合等价规则。

    下面,我们按照同样的思路,看下 Cube 语句

    Spark SQL 官方文档中 SQL Syntax 一节对 Cube 语句的描述如下:

    CUBE clause is used to perform aggregations based on combination of grouping columns specified in the GROUP BYclause. CUBE is a shorthand for GROUPING SETS. (... 一些例子)

    同样,官方文档把 Cube 描述为 Grouping Sets 的简写,等价规则为:Cube(A, B, C) == Grouping Sets((A, B, C), (A, B), (A, C), (B, C), (A), (B), (C), ())

    比如,Group By Cube(city, car_model) 就等同于 Group By Grouping Sets((city, car_model), (city), (car_model), ())

    下面,我们通过 expand extended 看下 Cube 版本 SQL 的 Optimized Logical Plan:

    spark-sql> explain extended SELECT city, car_model, sum(quantity) AS sum FROM dealer GROUP BY CUBE(city, car_model) ORDER BY city, car_model;
    == Parsed Logical Plan ==
    ...
    == Analyzed Logical Plan ==
    ...
    == Optimized Logical Plan ==
    Sort [city#2202 ASC NULLS FIRST, car_model#2203 ASC NULLS FIRST], true
    +- Aggregate [city#2202, car_model#2203, spark_grouping_id#2201L], [city#2202, car_model#2203, sum(quantity#2197AS sum#2188L]
       +- Expand [[quantity#2197, city#2195, car_model#21960], [quantity#2197, city#2195null1], [quantity#2197null, car_model#21962], [quantity#2197nullnull3]], [quantity#2197, city#2202, car_model#2203, spark_grouping_id#2201L]
          +- Project [quantity#2197, city#2195, car_model#2196]
             +- HiveTableRelation [`default`.`dealer`, ..., Data Cols: [id#2194, city#2195, car_model#2196, quantity#2197], Partition Cols: []]
    == Physical Plan ==
    ...

    从上述 Plan 可以看出,Cube 底层用的也是 Expand 算子,说明 Cube 确实基于 Grouping Sets 实现,而且也符合等价规则。

    所以,RollUp 和 Cube 可以看成是 Grouping Sets 的语法糖,在底层实现和性能上是一样的。

    最后

    本文重点讨论了 Group By 高级用法 Groupings Sets 语句的功能和底层实现。

    虽然 Groupings Sets 的功能,通过 Union All 也能实现,但前者并非后者的语法糖,它们的底层实现完全不一样。Grouping Sets 采用的是先联合再聚合的思路,通过 spark_grouping_id 列来保证数据的正确性;Union All 则采用先聚合再联合的思路。Grouping Sets 在 SQL 语句表达和性能上都有更大的优势

    Group By 的另外两个高级用法 RollUp 和 Cube 则可以看成是 Grouping Sets 的语法糖,它们的底层都是基于 Expand 算子实现,在性能上与直接使用 Grouping Sets 是一样的,但在 SQL 表达上更加简洁

    文章配图

    可以在 用Keynote画出手绘风格的配图 中找到文章的绘图方法。

    参考

    [1] Spark SQL Guide, Apache Spark

    [2] apache spark 3.3 版本源码, Apache Spark, GitHub

    更多文章请关注微信公众号:元闰子的邀请

    评论
    • RDDI-DAP错误通常与调试接口相关,特别是在使用CMSIS-DAP协议进行嵌入式系统开发时。以下是一些可能的原因和解决方法: 1. 硬件连接问题:     检查调试器(如ST-Link)与目标板之间的连接是否牢固。     确保所有必要的引脚都已正确连接,没有松动或短路。 2. 电源问题:     确保目标板和调试器都有足够的电源供应。     检查电源电压是否符合目标板的规格要求。 3. 固件问题: &n
      丙丁先生 2024-12-01 17:37 57浏览
    • 学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习笔记&记录学习习笔记&记学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&
      youyeye 2024-11-29 14:30 118浏览
    • 《高速PCB设计经验规则应用实践》+PCB绘制学习与验证读书首先看目录,我感兴趣的是这一节;作者在书中列举了一条经典规则,然后进行详细分析,通过公式推导图表列举说明了传统的这一规则是受到电容加工特点影响的,在使用了MLCC陶瓷电容后这一条规则已经不再实用了。图书还列举了高速PCB设计需要的专业工具和仿真软件,当然由于篇幅所限,只是介绍了一点点设计步骤;我最感兴趣的部分还是元件布局的经验规则,在这里列举如下:在这里,演示一下,我根据书本知识进行电机驱动的布局:这也算知行合一吧。对于布局书中有一句:
      wuyu2009 2024-11-30 20:30 88浏览
    • 在电子技术快速发展的今天,KLV15002光耦固态继电器以高性能和强可靠性完美解决行业需求。该光继电器旨在提供无与伦比的电气隔离和无缝切换,是现代系统的终极选择。无论是在电信、工业自动化还是测试环境中,KLV15002光耦合器固态继电器都完美融合了效率和耐用性,可满足当今苛刻的应用需求。为什么选择KLV15002光耦合器固态继电器?不妥协的电压隔离从本质上讲,KLV15002优先考虑安全性。输入到输出隔离达到3750Vrms(后缀为V的型号为5000Vrms),确保即使在高压情况下,敏感的低功耗
      克里雅半导体科技 2024-11-29 16:15 119浏览
    • 国产光耦合器正以其创新性和多样性引领行业发展。凭借强大的研发能力,国内制造商推出了适应汽车、电信等领域独特需求的专业化光耦合器,为各行业的技术进步提供了重要支持。本文将重点探讨国产光耦合器的技术创新与产品多样性,以及它们在推动产业升级中的重要作用。国产光耦合器创新的作用满足现代需求的创新模式新设计正在满足不断变化的市场需求。例如,高速光耦合器满足了电信和数据处理系统中快速信号传输的需求。同时,栅极驱动光耦合器支持电动汽车(EV)和工业电机驱动器等大功率应用中的精确高效控制。先进材料和设计将碳化硅
      克里雅半导体科技 2024-11-29 16:18 159浏览
    • 戴上XR眼镜去“追龙”是种什么体验?2024年11月30日,由上海自然博物馆(上海科技馆分馆)与三湘印象联合出品、三湘印象旗下观印象艺术发展有限公司(下简称“观印象”)承制的《又见恐龙》XR嘉年华在上海自然博物馆重磅开幕。该体验项目将于12月1日正式对公众开放,持续至2025年3月30日。双向奔赴,恐龙IP撞上元宇宙不久前,上海市经济和信息化委员会等部门联合印发了《上海市超高清视听产业发展行动方案》,特别提到“支持博物馆、主题乐园等场所推动超高清视听技术应用,丰富线下文旅消费体验”。作为上海自然
      电子与消费 2024-11-30 22:03 71浏览
    • By Toradex胡珊逢简介嵌入式领域的部分应用对安全、可靠、实时性有切实的需求,在诸多实现该需求的方案中,QNX 是经行业验证的选择。在 QNX SDP 8.0 上 BlackBerry 推出了 QNX Everywhere 项目,个人用户可以出于非商业目的免费使用 QNX 操作系统。得益于 Toradex 和 QNX 的良好合作伙伴关系,用户能够在 Apalis iMX8QM 和 Verdin iMX8MP 模块上轻松测试和评估 QNX 8 系统。下面将基于 Apalis iMX8QM 介
      hai.qin_651820742 2024-11-29 15:29 150浏览
    • 最近几年,新能源汽车愈发受到消费者的青睐,其销量也是一路走高。据中汽协公布的数据显示,2024年10月,新能源汽车产销分别完成146.3万辆和143万辆,同比分别增长48%和49.6%。而结合各家新能源车企所公布的销量数据来看,比亚迪再度夺得了销冠宝座,其10月新能源汽车销量达到了502657辆,同比增长66.53%。众所周知,比亚迪是新能源汽车领域的重要参与者,其一举一动向来为外界所关注。日前,比亚迪汽车旗下品牌方程豹汽车推出了新车方程豹豹8,该款车型一上市就迅速吸引了消费者的目光,成为SUV
      刘旷 2024-12-02 09:32 60浏览
    • 国产光耦合器因其在电子系统中的重要作用而受到认可,可提供可靠的电气隔离并保护敏感电路免受高压干扰。然而,随着行业向5G和高频数据传输等高速应用迈进,对其性能和寿命的担忧已成为焦点。本文深入探讨了国产光耦合器在高频环境中面临的挑战,并探索了克服这些限制的创新方法。高频性能:一个持续关注的问题信号传输中的挑战国产光耦合器传统上利用LED和光电晶体管进行信号隔离。虽然这些组件对于标准应用有效,但在高频下面临挑战。随着工作频率的增加,信号延迟和数据保真度降低很常见,限制了它们在电信和高速计算等领域的有效
      腾恩科技-彭工 2024-11-29 16:11 106浏览
    • 在现代科技浪潮中,精准定位技术已成为推动众多关键领域前进的核心力量。虹科PCAN-GPS FD 作为一款多功能可编程传感器模块,专为精确捕捉位置和方向而设计。该模块集成了先进的卫星接收器、磁场传感器、加速计和陀螺仪,能够通过 CAN/CAN FD 总线实时传输采样数据,并具备内部存储卡记录功能。本篇文章带你深入虹科PCAN-GPS FD的技术亮点、多场景应用实例,并展示其如何与PCAN-Explorer6软件结合,实现数据解析与可视化。虹科PCAN-GPS FD虹科PCAN-GPS FD的数据处
      虹科汽车智能互联 2024-11-29 14:35 149浏览
    • 光伏逆变器是一种高效的能量转换设备,它能够将光伏太阳能板(PV)产生的不稳定的直流电压转换成与市电频率同步的交流电。这种转换后的电能不仅可以回馈至商用输电网络,还能供独立电网系统使用。光伏逆变器在商业光伏储能电站和家庭独立储能系统等应用领域中得到了广泛的应用。光耦合器,以其高速信号传输、出色的共模抑制比以及单向信号传输和光电隔离的特性,在光伏逆变器中扮演着至关重要的角色。它确保了系统的安全隔离、干扰的有效隔离以及通信信号的精准传输。光耦合器的使用不仅提高了系统的稳定性和安全性,而且由于其低功耗的
      晶台光耦 2024-12-02 10:40 58浏览
    • 学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习笔记&记录学习习笔记&记学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&
      youyeye 2024-11-30 14:30 63浏览
    • 艾迈斯欧司朗全新“样片申请”小程序,逾160种LED、传感器、多芯片组合等产品样片一触即达。轻松3步完成申请,境内免费包邮到家!本期热荐性能显著提升的OSLON® Optimal,GF CSSRML.24ams OSRAM 基于最新芯片技术推出全新LED产品OSLON® Optimal系列,实现了显著的性能升级。该系列提供五种不同颜色的光源选项,包括Hyper Red(660 nm,PDN)、Red(640 nm)、Deep Blue(450 nm,PDN)、Far Red(730 nm)及Ho
      艾迈斯欧司朗 2024-11-29 16:55 157浏览
    • 光耦合器作为关键技术组件,在确保安全性、可靠性和效率方面发挥着不可或缺的作用。无论是混合动力和电动汽车(HEV),还是军事和航空航天系统,它们都以卓越的性能支持高要求的应用环境,成为现代复杂系统中的隐形功臣。在迈向更环保技术和先进系统的过程中,光耦合器的重要性愈加凸显。1.混合动力和电动汽车中的光耦合器电池管理:保护动力源在电动汽车中,电池管理系统(BMS)是最佳充电、放电和性能监控背后的大脑。光耦合器在这里充当守门人,将高压电池组与敏感的低压电路隔离开来。这不仅可以防止潜在的损坏,还可以提高乘
      腾恩科技-彭工 2024-11-29 16:12 117浏览
    我要评论
    0
    点击右上角,分享到朋友圈 我知道啦
    请使用浏览器分享功能 我知道啦