SPARK SQL中 CTE(with表达式)会影响性能么?

背景及问题

本文基于spark 3.1.2
最近在排查spark sql问题的时候,出现了一系列的(CTE)with操作,导致该任务运行不出来,而把对应的(CTE)with 替换成了临时表以后,任务很快的就能运行出来
对应的最简化的sql如下:

with temp1 as (
  select 
    null as user_id
    ,a.sku_id
  from xxx.xxx `a`
  where pt between '20211228' and '20220313'
  group by 
    a.sku_id),
temp2 as (
  select  
    a.xxx_code user_id
    ,a.sku_id 
  from xxx.xxx_1`a`
  left join xxx.xxx_2 `c` on c.pt='20220313' and a.xxx_code=c.xxx_code and c.xxx_id=1
  where a.pt='20220313'
  and TO_CHAR(upper_time,'yyyymmdd') >= '20220230'
  group by 
     a.xxx_code 
    ,a.sku_id)
select 
 *
 from (
 select 
 a1.sku_id,
 a1.user_id
 from temp1 `a1`
 -- BroadcastNestedLoopJoin
 full join temp2 `a5` on a1.user_id=a5.user_id and a1.sku_id=a5.sku_id 
 );

先说结论,其实是null as user_id 这块代码在作为join条件的时候被优化成布尔表达式false

分析

运行此sql,我们可以得到一下的物理计划:
在这里插入图片描述

我们看到 temp1和temp2的join的居然是BroadcastNestedLoopJoin,要知道BroadcastNestedLoopJoin的时间复杂度是O(M*N)的,这在数据大的情况下是很难计算出来的。
并且我们查看对应的代码JoinSelection.scala的时候,发现对于有等值条件的join的情况下,而且join的条件是可排序的情况下,最次也是会变成SortMergeJoin,对应的代码如下:

def createJoinWithoutHint() = {
          createBroadcastHashJoin(false)
            .orElse {
              if (!conf.preferSortMergeJoin) {
                createShuffleHashJoin(false)
              } else {
                None
              }
            }
            .orElse(createSortMergeJoin())
            .orElse(createCartesianProduct())
            .getOrElse {
              // This join could be very slow or OOM
              val buildSide = getSmallerSide(left, right)
              Seq(joins.BroadcastNestedLoopJoinExec(
                planLater(left), planLater(right), buildSide, joinType, nonEquiCond))
            }
        }

这部分的代码比较简单,暂且跳过。
就在百思不得其解的时候,还是最重要的一步,查看对应的逻辑计划日志:
直接重点(我们这里只说join条件部分的变化):

  • 解析完后的初始计划 为
 Join FullOuter, (('a1.user_id = 'a5.user_id) AND ('a1.sku_id = 'a5.sku_id))
  • 经过PromoteStrings规则
  Join FullOuter, ((user_id#3 = user_id#13) AND (sku_id#15 = sku_id#98)) 
                   ||
                   \/ 
  Join FullOuter, ((null = user_id#13) AND (sku_id#15 = sku_id#98))
  • 经过NullPropagation规则
Join FullOuter, ((null = user_id#13) AND (sku_id#15 = sku_id#98)) 
                   ||
                   \/
Join FullOuter, (null AND (sku_id#15 = sku_id#98))
  • 经过ReplaceNullWithFalseInPredicate规则
Join FullOuter, (null AND (sku_id#15 = sku_id#98))
                   ||
                   \/ 
Join FullOuter, (false AND (sku_id#15 = sku_id#98))
  • 经过BooleanSimplification规则
Join FullOuter, (false AND (sku_id#15 = sku_id#98))
                   ||
                   \/  
Join FullOuter, false

这样一步一步下来,其实最终的join条件就变成了 布尔表达式 false。
我们再看JoinSelection.scala 中join对应非等值条件case的判断:

      case logical.Join(left, right, joinType, condition, hint) =>
        val desiredBuildSide = if (joinType.isInstanceOf[InnerLike] || joinType == FullOuter) {
          getSmallerSide(left, right)
        } else {
          // For perf reasons, `BroadcastNestedLoopJoinExec` prefers to broadcast left side if
          // it's a right join, and broadcast right side if it's a left join.
          // TODO: revisit it. If left side is much smaller than the right side, it may be better
          // to broadcast the left side even if it's a left join.
          if (canBuildBroadcastLeft(joinType)) BuildLeft else BuildRight
        }
...
      def createJoinWithoutHint() = {
          createBroadcastNLJoin(canBroadcastBySize(left, conf), canBroadcastBySize(right, conf))
            .orElse(createCartesianProduct())
            .getOrElse {
              // This join could be very slow or OOM
              Seq(joins.BroadcastNestedLoopJoinExec(
                planLater(left), planLater(right), desiredBuildSide, joinType, condition))
            }
        }

     createBroadcastNLJoin(hintToBroadcastLeft(hint), hintToBroadcastRight(hint))
       .orElse { if (hintToShuffleReplicateNL(hint)) createCartesianProduct() else None }
       .getOrElse(createJoinWithoutHint())

最终还是会调用createJoinWithoutHint生成BroadcastNestedLoopJoinExec。

解决方法及总结

  • 改写成临时表
    把with改写成临时表,这是有益处的,因为在某些场景下会触发到AQE中的特性,而且改写成临时表后,任务是串行的,能够减少因为资源问题导致的任务运行缓慢问题(笔者曾经有遇到过)
    注意:改成临时表的情况下,不能存在null as user_id的语句,否则会报错:
    Caused by: org.apache.spark.sql.AnalysisException: Cannot create tables with null type.
    
  • null as user_id改写成0 as user_id
    根据之前的分析,导致变成BroadcastNestedLoopJoinExec的原因是null作为了join条件引发的,我们可以改写就好

其实CTE操作并不是影响性能的主要原因,主要原因还是在于spark对于某种case的处理,这种还会得具体case具体分析处理。
当然也可以参考Why is my CTE so slow?.

<think>嗯,用户问的是SparkSQL递归,我需要先理解清楚他们具体想知道什么。可能是指SparkSQL是否支持递归查询,比如处理树形结构数据或者层次查询。SparkSQL本身是基于Hive和DataFrame的,但传统的SQL递归通常是通过CTE(Common Table Expressions)的WITH RECURSIVE实现的。 首先,我得回忆一下Spark的版本情况。在Spark 3.0之前,SparkSQL不支持递归CTE,所以用户可能需要找替代方案。比如使用DataFrame的API进行迭代处理,或者结合其他工具。但如果是Spark 3.0及以上版本,可能有所变化。我需要确认这一点,可能官方文档有更新。 然后,用户可能需要了解如何在Spark中实现递归操作。如果原生不支持,可能需要用其他方法,比如用循环或者UDF来模拟递归。但递归处理大数据可能会有性能问题,比如栈溢出或者效率低下,这时候可能需要考虑优化策略,比如限制递归深度,或者转换递归为迭代。 另外,可能用户的实际应用场景是处理层级数据,比如组织结构、目录树等。这时候,如果SparkSQL不支持递归查询,可以建议将数据预处理成非递归结构,或者使用图处理框架如GraphX结合SparkSQL来处理。 需要确保回答准确,所以得检查Spark的最新版本是否支持递归CTE。查了一下,Spark 3.0确实引入了对递归CTE的支持,但可能有使用限制,比如需要启用特定配置或者遵循ANSI SQL的标准语法。 总结下来,回答的结构应该是:首先说明SparkSQL在3.0版本后支持递归CTE,然后给出示例,接着提到之前的版本如何解决,以及可能的性能考虑和应用场景。确保用户知道不同版本的区别,以及替代方法。</think>关于 Spark SQL 中的递归查询,以下是关键点说明: 1. **版本支持** Spark 3.0 及以上版本开始支持 ANSI SQL 标准的递归查询(通过 `WITH RECURSIVE` 语法),但需注意以下配置: ```sql SET spark.sql.legacy.ctePrecedence.enabled=true; -- 确保递归 CTE 正确解析 ``` 2. **递归查询示例** 假设需要生成 1 到 5 的序列: ```sql WITH RECURSIVE numbers AS ( SELECT 1 AS n UNION ALL SELECT n + 1 FROM numbers WHERE n < 5 ) SELECT * FROM numbers; ``` 输出结果: ``` +---+ | n | +---+ | 1 | | 2 | | 3 | | 4 | | 5 | +---+ ``` 3. **旧版本替代方案** Spark 3.0 之前可通过以下方法模拟递归: - **DataFrame 迭代**:使用循环逐层处理数据 ```scala var df = spark.sql("SELECT 1 AS n") for (_ <- 1 to 4) { df = df.union(df.selectExpr("n + 1 AS n")) } df.filter("n <= 5").show() ``` - **层级展开**:对固定深度结构使用多级 `UNION` 操作 4. **性能注意事项** | 方法 | 优点 | 缺点 | |---------------------|-----------------------|-------------------------------| | 递归 CTE | 语法简洁,符合 SQL 标准 | 深度过大时可能栈溢出 | | DataFrame 迭代 | 可控内存使用 | 需手动管理迭代终止条件 | | 预计算层级结构 | 查询速度快 | 需要提前处理数据 | 5. **典型应用场景** - 组织架构树状查询 - 社交网络关系分析(需结合图算法优化) - 目录层级遍历 - 依赖关系解析 6. **扩展方案** 复杂递归需求可结合其他工具: ```mermaid graph LR A[Spark SQL] --> B{是否需要深度递归?} B -->|是| C[导出到图数据库 Neo4j] B -->|否| D[使用原生递归 CTE] C --> E[通过 Cypher 语言处理] E --> F[结果写回 Spark 分析] ``` 建议根据数据规模选择方案: - 小规模层级数据 → 直接使用递归 CTE - 超深递归/大规模图数据 → 导出到专用图处理系统 - 定期分析需求 → 预计算非递归化存储结构
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值