Spark SQL FOR 语句指南:SQL 脚本中基于查询结果的行级循环
【免费下载链接】sparkApache Spark - A unified analytics engine for large-scale data processing项目地址: https://gitcode.com/gh_mirrors/sp/spark
导读
本文介绍 Apache Spark SQL 脚本(SQL Scripting)中的FOR语句——一种基于 SQL 查询结果集逐行执行语句列表的循环控制结构。FOR语句只能出现在 复合语句(compound statement) 内部,是编写存储过程式 SQL 逻辑(如累加统计、逐行加工、条件中断)的核心工具。读完本文,你将掌握FOR语句的完整语法、标签(label)与变量(variable_name)的使用规则、与LEAVE/ITERATE的配合方式,以及它在 Spark 源码中的解析与执行机制,并能写出正确、可运行的 SQL 脚本示例。
FOR语句是 SQL 脚本 控制流家族的一员,与LOOP、WHILE、REPEAT共同构成基于谓词或结果集驱动的循环体系,官方语法文档见 docs/control-flow/for-stmt.md。
FOR 语句的作用与适用场景
FOR语句为查询返回的每一行重复执行一组语句。与LOOP/WHILE这类“基于条件判断是否继续循环”的结构不同,FOR的循环次数由查询结果集的行数天然决定——查询执行一次,结果集被逐行迭代。
典型适用场景包括:
- 对查询结果逐行累计计算(如求和、拼接);
- 对结果集的每一行执行数据加工或 DML 操作;
- 在循环体内基于当前行数据做条件判断,并通过
LEAVE提前终止或ITERATE跳过后续语句进入下一轮迭代。
FOR语句只能被用在复合语句内部,即BEGIN ... END块之中,不能出现在脚本顶层。
语法结构
FOR语句的完整语法如下:
[ label : ] FOR [ variable_name AS ] query DO { stmt ; } [...] END FOR [ label ]语法要点逐项说明:
label(可选):循环的标签,在包含该FOR语句的外层语句的所有标签中必须唯一。若指定了结束标签,则必须与起始标签一致。标签用于配合 LEAVE(终止整个循环)与 ITERATE(跳过当前迭代、继续下一次迭代)跳转。variable_name(可选):一个标识符,作为访问当前行各列的限定符。需要注意:限定列引用时用的是variable_name,而不是label。stmt:循环体中的一条 SQL 语句(DDL、DML、控制流语句等均可),多条语句以分号分隔。query:定义迭代结果集的查询。查询只执行一次,其结果集作为循环的行来源。
语法解析实现
在源码层面,FOR语句被解析为逻辑计划ForStatement,定义于 SqlScriptingLogicalPlans.scala:
case class ForStatement( query: SingleStatement, variableName: Option[String], body: CompoundBody, label: Option[String]) extends CompoundPlanStatement从源码注释可以确认其语义约定:
query:查询执行一次,然后其结果集被逐行迭代("executed once, then it's result set is iterated on, row by row");variableName:迭代期间用于访问当前行的变量名;body:针对结果集每一行执行的语句集合(CompoundBody);label:循环的可选标签,可用于LEAVE或ITERATE该循环。
在解析器 AstBuilder.scala 中,visitForStatementImpl依次处理标签作用域(enterLabeledScope)、查询体(封装为SingleStatement)、进入FOR专属作用域(enterForScope)解析变量名与循环体(visitCompoundBodyImpl),最后构造ForStatement。解析器测试 SqlScriptingParserSuite.scala 验证了lbl: FOR x AS SELECT 5 DO ... END FOR能被正确解析为ForStatement,且query、variableName、body、label各字段均正确填充。
参数详解
label
- 循环标签在包含该语句的外层语句的所有标签中必须唯一;
- 结束标签(
END FOR后)若指定,必须与开始标签一致; - 标签的作用是配合
LEAVE/ITERATE实现跳转控制; - 限定(访问)循环当前行的列时,使用
variable_name,而不是label。
从解析器测试可以看到,lbl: FOR x AS SELECT 5 DO SELECT 1; END FOR;中标签lbl被记录到ForStatement.label,且该循环体可再嵌套另一个FOR语句(测试forStmt.body.collection.head.isInstanceOf[ForStatement]),说明FOR支持嵌套。
variable_name
- 可选名称,作为引用游标(当前结果行)中列的限定符;
- 在
FOR循环体中,通过variable_name.column_name访问当前行的列值; - 在 AstBuilder.scala 中,
variable_name由ctx.strictIdentifier()解析,Option(...)包裹表示可省略。
stmt
循环体内的一条 SQL 语句。根据 复合语句文档,SQL_statement可以是 DDL、DML、控制流语句或嵌套复合语句;任何SELECT或VALUES语句都会产生结果集供脚本调用方消费。因此FOR循环体内同样可以放置任意合法的 SQL 语句,包括嵌套的IF、CASE、LEAVE、ITERATE,甚至嵌套的FOR。
注意事项(语义边界)
官方文档对FOR语句的行为边界给出了明确约定,实际编码时务必注意:
- 循环体内修改查询源表的场景:如果查询作用于一张同时在循环体内被修改的表,语义取决于数据源。对于Delta 表,查询结果不受影响(即结果集在迭代前已快照确定)。
- 提前终止不保证查询完整执行:如果循环因
LEAVE语句或错误条件而提前结束,Spark不保证查询已完整执行。也就是说,结果集的物化程度取决于实际执行到哪一步。 - 异常/副作用发生时机不确定:查询执行过程中发生异常或副作用时,Spark不保证这些异常/副作用在循环内的具体时间点发生。因此不要在
FOR查询的求值过程中依赖特定的副作用顺序。 - 优先考虑关系型替代:通常
FOR循环可以用关系型查询(relational queries)替代,而关系型查询往往高效得多。SQL 引擎擅长集合运算,逐行循环应作为最后手段。
完整示例:累加 1 到 10 的所有奇数
官方文档给出的经典示例——对range(1, 20)中大于 10 的数跳出循环、偶数跳过、奇数累加:
-- 累加 1 到 10 的所有奇数 > BEGIN DECLARE sum INT DEFAULT 0; sumNumbers: FOR row AS SELECT num FROM range(1, 20) AS t(num) DO IF num > 10 THEN LEAVE sumNumbers; ELSEIF num % 2 = 0 THEN ITERATE sumNumbers; END IF; SET sum = sum + row.num; END FOR sumNumbers; VALUES (sum); END; 25逐行拆解这段脚本:
BEGIN开启复合语句块,DECLARE sum INT DEFAULT 0;声明局部变量并初始化为 0;sumNumbers:为循环打上标签;row是限定当前行的变量名;SELECT num FROM range(1, 20) AS t(num)生成 1 到 19 的序列;- 循环体内用
IF/ELSEIF做三层判断:num > 10时LEAVE sumNumbers终止整个循环;num % 2 = 0时ITERATE sumNumbers跳过本轮累加,直接进入下一行;- 其余情况
SET sum = sum + row.num累加当前列值;
- 循环结束后
VALUES (sum)输出结果25(即 1 + 3 + 5 + 7 + 9 = 25)。
注意循环体内同时使用了row.num(通过变量名限定列)与裸num两种列引用方式,二者都可用。
更高效的等价关系型写法
文档同时给出了性能提示的对照示例——用一条纯 SQL 聚合替代整个循环:
> SELECT sum(num) FROM range(1, 10) AS t(num) WHERE num % 2 = 1; 25两者结果一致,但关系型写法无需逐行解释执行,通常更快。这正是“能用关系型查询就别用循环”的最佳实践佐证。
FOR 与其他控制流语句的协作
FOR语句并非孤立存在,它依赖于复合语句提供的变量、条件与异常处理环境,并与以下控制流语句协同工作:
- LEAVE 语句:终止包含指定标签的循环(或复合语句)的执行。在
FOR循环体内使用LEAVE sumNumbers;可提前结束循环。 - ITERATE 语句:终止当前迭代的执行,并在循环条件(对
FOR而言即是否还有下一行)满足时继续下一轮迭代。ITERATE sumNumbers;跳过本轮剩余语句。 - IF 语句与CASE 语句:在循环体内实现条件分支,决定何时
LEAVE、何时ITERATE。 - LOOP / WHILE / REPEAT 语句:与
FOR并列的其他循环结构。LOOP无条件循环、WHILE先判断条件、REPEAT后判断条件,而FOR按查询结果集逐行迭代。
在 SqlScriptingLogicalPlans.scala 中,WhileStatement(L141)、RepeatStatement(L171)、LoopStatement(L318)与ForStatement(L344)均实现CompoundPlanStatement接口,共享同一套标签作用域与LEAVE/ITERATE跳转语义。
从解析器的标签校验逻辑(AstBuilder.scala)还可以看到:LEAVE/ITERATE的标签必须指向合法的循环或复合语句标签,例如对复合语句(BeginEndCompoundBlockContext)使用ITERATE会抛出invalidIterateLabelUsageForCompound错误——说明标签必须匹配正确的语句类型。
运行前提与适用限制
FOR语句属于 SQL 脚本(SQL Scripting)特性,整个脚本必须以复合语句块(BEGIN ... END)开头和存在,具体参见 SQL Scripting 与 复合语句 文档。- 脚本可以通过会话变量(session variables)或参数标记(parameter markers)与调用方交换数据。
- 变量与游标遵循从内层作用域向外层解析的规则(详见 Name Resolution),复合语句标签可用于消歧嵌套作用域中的同名变量。
- 若脚本中存在异常,可通过声明
EXIT/CONTINUE条件处理器(handler)拦截;NOT FOUND(SQLSTATE'02xxx')类处理器常与游标迭代(FETCH超出结果集末尾触发CURSOR_NO_MORE_ROWS)配合使用。 - 性能上应优先使用关系型聚合/集合操作;确需逐行处理时,注意上述“查询完整执行”与“副作用时机”的不保证语义。
总结
FOR语句是 Spark SQL 脚本中唯一一个由查询结果集驱动的循环结构:查询执行一次、结果逐行迭代、循环体可自由使用LEAVE/ITERATE控制流向,配合variable_name可方便地访问当前行列值。理解其语法、标签规则与语义边界(特别是 Delta 表行为、提前终止与副作用时机的不保证),并坚持“能用关系型查询就不用循环”的原则,即可在存储过程式 SQL 脚本中写出正确、高效的逻辑。
【免费下载链接】sparkApache Spark - A unified analytics engine for large-scale data processing项目地址: https://gitcode.com/gh_mirrors/sp/spark
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考