Spark SQL FOR 语句指南:SQL 脚本中基于查询结果的行级循环
2026/9/19 17:09:44 网站建设 项目流程

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 脚本 控制流家族的一员,与LOOPWHILEREPEAT共同构成基于谓词或结果集驱动的循环体系,官方语法文档见 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:循环的可选标签,可用于LEAVEITERATE该循环。

在解析器 AstBuilder.scala 中,visitForStatementImpl依次处理标签作用域(enterLabeledScope)、查询体(封装为SingleStatement)、进入FOR专属作用域(enterForScope)解析变量名与循环体(visitCompoundBodyImpl),最后构造ForStatement。解析器测试 SqlScriptingParserSuite.scala 验证了lbl: FOR x AS SELECT 5 DO ... END FOR能被正确解析为ForStatement,且queryvariableNamebodylabel各字段均正确填充。

参数详解

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_namectx.strictIdentifier()解析,Option(...)包裹表示可省略。

stmt

循环体内的一条 SQL 语句。根据 复合语句文档,SQL_statement可以是 DDL、DML、控制流语句或嵌套复合语句;任何SELECTVALUES语句都会产生结果集供脚本调用方消费。因此FOR循环体内同样可以放置任意合法的 SQL 语句,包括嵌套的IFCASELEAVEITERATE,甚至嵌套的FOR

注意事项(语义边界)

官方文档对FOR语句的行为边界给出了明确约定,实际编码时务必注意:

  1. 循环体内修改查询源表的场景:如果查询作用于一张同时在循环体内被修改的表,语义取决于数据源。对于Delta 表,查询结果不受影响(即结果集在迭代前已快照确定)。
  2. 提前终止不保证查询完整执行:如果循环因LEAVE语句或错误条件而提前结束,Spark不保证查询已完整执行。也就是说,结果集的物化程度取决于实际执行到哪一步。
  3. 异常/副作用发生时机不确定:查询执行过程中发生异常或副作用时,Spark不保证这些异常/副作用在循环内的具体时间点发生。因此不要在FOR查询的求值过程中依赖特定的副作用顺序。
  4. 优先考虑关系型替代:通常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 > 10LEAVE sumNumbers终止整个循环;
    • num % 2 = 0ITERATE 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询