Spark SQL同一查询中无法引用AS新建派生列的原因及实现方式
这个现象本质是SQL逻辑执行顺序的规则决定的,和Spark本身的“功能缺失”没有关系:
同一层级SELECT子句中定义的列别名,默认对同层级的其他投影表达式不可见。
ANSI SQL定义的逻辑执行顺序并不是按照你书写代码的先后顺序运行,SELECT层级的执行优先级如下:
- FROM/JOIN 加载关联数据源
- WHERE 过滤原始数据行
- GROUP BY 执行分组
- 运行聚合函数计算
- HAVING 过滤分组结果
- SELECT 批量计算所有投影列、定义列别名
- DISTINCT 结果去重
- ORDER BY 结果排序
- LIMIT 截断返回结果
同一层级SELECT块里的所有表达式是在同一个计算阶段批量完成解析和求值的,不存在“先计算第一个AS别名、再计算第二个引用该别名的表达式”的先后逻辑,所以你写follow_on_price * exchange_rate_usd AS follow_on_price_usd的时候,解析器还没完成follow_on_price别名的绑定,自然会报列不存在的错误。
很多人提到PostgreSQL支持同层级引用别名是“标准SQL行为”,这是个常见误区:这个能力是PostgreSQL、MySQL等数据库在解析层额外做的语法糖,会自动把同层级SELECT里的别名引用替换成对应原始表达式,不属于ANSI SQL的强制规范。Spark SQL默认没有开启这类非标准语法扩展(截至3.4.x稳定版默认行为均是如此),所以不会自动做这个替换。
另外补充:Spark SQL本身是支持ORDER BY子句引用SELECT别名的,因为ORDER BY的执行阶段在SELECT之后,完全符合SQL执行逻辑规范。
不需要额外注册临时视图,以下三种写法都可以直接实现需求,不会产生额外性能损耗:
- 直接嵌套表达式:把别名对应的计算逻辑直接写到引用位置即可,适合逻辑非常简单的场景
SELECT unlock_price - COALESCE(cast(upfront_price as Numeric(38,8)), 0) AS follow_on_price, (unlock_price - COALESCE(cast(upfront_price as Numeric(38,8)), 0)) * exchange_rate_usd AS follow_on_price_usd FROM accounts_2
缺点是重复书写计算逻辑,表达式复杂时可读性很差,后续维护修改容易漏改。
- 使用CTE(公共表表达式)做逻辑分层:这是生产环境最推荐的写法,Spark的Catalyst优化器会自动把CTE逻辑和外层查询拉平优化,做列裁剪、谓词下推,执行效率和摊平写表达式完全一致,不会多跑一轮计算,同时逻辑分层清晰,可读性高
WITH step1_calc AS ( SELECT *, unlock_price - COALESCE(cast(upfront_price as Numeric(38,8)), 0) AS follow_on_price FROM accounts_2 ) SELECT *, follow_on_price * exchange_rate_usd AS follow_on_price_usd FROM step1_calc
- 开启Spark自带的别名引用语法配置:Spark 3.3及以上版本支持通过配置开启同层级SELECT投影的别名引用能力,开启后就可以直接写和PostgreSQL类似的同层引用逻辑
SET spark.sql.analyzer.allowAliasInSameProjection = true;
注意这个配置在部分低版本Spark中属于实验性功能,生产环境开启前需要做对应版本的兼容性验证。
不要觉得用CTE/子查询比直接写同层引用性能差,Spark优化器会对执行计划做全局优化,不会因为多写了一层CTE就产生额外的落盘或shuffle开销,比手动注册临时视图更轻量,不会产生多余的元数据。
内容的提问来源于stack exchange,提问作者bigDataArtist

