SQL脚本模式

更新时间:
复制为 MD 格式

脚本模式(Script Mode SQL)将多条 SQL 语句作为一个整体编译和提交,生成一份执行计划,只需排队一次、执行一次。适用于 ETL 数据管道、周期性批处理,以及需要多条语句协同完成的查询编排场景。脚本模式支持普通模式(默认,原子性执行)和单步执行模式(逐条串行执行)。

  • 脚本模式不支持费用预估,实际费用以账单为准。详情请参见查看账单详情

  • 单个脚本最多引用 10,000 张表。每次引用独立计数,包括对同一张表的重复引用和视图定义中引用的表。

  • 如果多个输入的数据源数据准备完成的时间间隔很长(例如一个01:00可以准备好,一个07:00可以准备好),则不适合通过table variable衔接拼装为一个大的脚本模式SQL。

适用场景

  • 复杂单语句改写:将深度嵌套的子查询拆分为一系列可读的表变量赋值语句。

  • 多语句管道:将逻辑上相关的多条语句合并为一个作业统一提交,减少排队和调度开销。普通模式下所有语句原子性执行;单步执行模式下语句逐条串行执行,适用于需要先写后读或跨平台迁移的场景。详情请参见执行模式

语法结构

  • 语句类型:脚本模式支持SET语句、部分DDL语句(不支持结果为屏显类型的语句如DESC、SHOW)、DML语句。

  • 语句顺序:脚本的完整形式是SETDDLDML 的固定顺序,每个语句类型都可以包含0到多个具体的SQL语句,但是不同类型的语句块不能混用。

-- 1. SET
SET odps.sql.type.system.odps2=true;
[SET odps.stage.reducer.num=xxx;]
[SET odps.sql.step.script.mode=true;] -- 开启单步执行模式
[...]

-- 2. DDL
CREATE TABLE table1 xxx;
[CREATE TEMPORARY TABLE table2 xxx;] -- 创建临时表
[...]

-- 3. DML&DQL
@var1 := SELECT [ALL | DISTINCT] select_expr, select_expr, ...
        FROM table3
        [WHERE where_condition];
@var2 := SELECT [ALL | DISTINCT] select_expr, select_expr, ...
        FROM table4
        [WHERE where_condition];
@var3 := SELECT [ALL | DISTINCT] var1.select_expr, var2.select_expr, ...
        FROM @var1 JOIN @var2 ON ...;
INSERT OVERWRITE|INTO TABLE [PARTITION (partcol1=val1, partcol2=val2 ...)]
        SELECT [ALL | DISTINCT] select_expr, select_expr, ...
        FROM @var3;    
[@var4 := SELECT [ALL | DISTINCT] var1.select_expr, var1.select_expr, ... FROM @var1 
        UNION ALL | UNION 
        SELECT [ALL | DISTINCT] var2.select_expr, var2.select_expr, ... FROM @var2;    
CREATE [EXTERNAL] TABLE [IF NOT EXISTS] table_name 
        AS 
        SELECT [ALL | DISTINCT] select_expr, select_expr, ...
        FROM @var4;]
[...]

执行模式

脚本模式支持两种执行模式。

行为

普通模式(默认)

单步执行模式

开启方式

默认,无需额外配置。

在 SET 部分添加 SET odps.sql.step.script.mode=true;

编译

所有 DML 语句编译为一个执行计划。

每条 DML 语句独立编译为一个执行计划。注意:非常复杂的脚本(例如上千行、包含大量操作)可能超出编译内存限制。

执行

所有语句作为一个作业原子执行,所有输入数据就绪后才开始。任一语句失败,整个脚本失败,所有操作回滚。

DML 语句按顺序逐条执行。某条语句失败时,已执行的语句不会回滚,只能从头重试。建议将大脚本拆分为多个较小脚本。

先写后读

不支持。在同一脚本中对同一张表先写入再读取会报错。

支持。目前不支持事务表和分区表。

临时表

不支持。

支持。

DDL 语法

创建临时表

脚本模式下支持创建临时表,缓存中间结果,供同一脚本内复用。此时由于需要先写后读,仅支持在单步执行模式下运行。

语法

CREATE TEMPORARY TABLE <table_name> (
  <col_name> <data_type>, ...
)
[LIFECYCLE <days>]
[AS <select_statement>];

参数说明

参数

说明

table_name

临时表名称。仅在当前脚本内可访问。

LIFECYCLE <days>

可选。表自动删除前的保留天数。默认值:1。

AS <select_statement>

可选。创建时通过 SELECT 语句填充表数据。

使用限制

  • 临时表需要开启单步执行模式。在脚本的 SET 部分添加 SET odps.sql.step.script.mode=true;

  • 临时表仅在创建它的脚本内可访问。

  • 临时表不能是分区表或事务表。

  • 如需显式删除临时表,DROP TABLE 语句必须放在脚本末尾。

创建普通表

脚本模式支持创建普通表。语法详情请参见CREATE TABLE

DML 和 DQL 语法

语句限制

  • 屏显语句:单个脚本中仅允许包含一条有屏显结果输出的语句(如单独的 SELECT语句),超出将触发报错。建议避免在脚本中使用此类语句

  • CREATE TABLE AS:每个脚本仅允许一次,且必须是最后一条可执行语句。建议先创建表再插入数据。

  • 混合写入模式:同一脚本中不支持同一张表同时使用 OVERWRITE 和 INTO。事务表和普通表的 DML 操作不能混合。

  • 先写后读:普通模式下对表先写入再读取会报错。解决方案:使用表变量替代,或切换为单步执行模式。单步执行模式下支持先写后读,但事务表和分区表不支持。详细示例请参见示例 3:普通模式下的先写后读

变量

语法

-- 使用 @ 声明表变量,支持TABLE类型,或任意Maxcompute数据类型
@var1 <type>

-- 使用 := 赋值:
@var1 := <select_statement>

使用说明

  • 不能将表类型变量赋值给已指定数据类型的变量。例如,不允许以下写法:

    @a TABLE (name STRING);
    @a := SELECT 'tom';
    @b STRING;
    @b := SELECT * FROM @a;
  • 变量可以存储常量值。使用 SELECT * FROM @var将其转换为标量,常量值也可以存放在一个单行的表中,命令示例如下。转化语法请参见子查询(SUBQUERY)

    @a := SELECT 10;                                         -- 赋值常量 10
    @b := SELECT key, value + (SELECT * FROM @a) FROM t2;   -- 将 @a 作为标量使用
    SELECT * FROM @b;

IF 语句

脚本模式下,支持IF语句:IF语句可以使程序根据条件,自动选择执行逻辑。

语法

-- 单分支
IF (condition) BEGIN
  statements
END

-- 多分支
IF (condition) BEGIN
  statements
END ELSE IF (condition2) BEGIN
  statements
END ELSE BEGIN
  statements
END

使用说明

  • 当分支只包含一条语句时,关键字 BEGINEND 可省略(类似 Java 中的 { })。

  • IF 分支内不支持 CREATE TABLEALTER TABLETRUNCATE TABLE 等 DDL 语句。

  • condition 支持两种类型:

    • 布尔表达式:编译期确定分支。

    • 布尔标量子查询:运行期确定分支,MaxCompute 可能提交多个作业。

示例1:IF语句中的 condition为 BOOLEAN类型 的表达式。这种类型的IF ELSE语句可以在编译阶段决定执行哪个分支,示例如下:

@date := '20190101';
@row TABLE(id STRING);
IF (CAST(@date AS BIGINT) % 2 == 0) BEGIN
  @row := SELECT id FROM src1;
END ELSE BEGIN
  @row := SELECT id FROM src2;
END
INSERT OVERWRITE TABLE dest SELECT * FROM @row;

示例2:IF语句中的 condition 为 BOOLEAN 的 Scalar SubQuery。这种类型的IF ELSE语句在编译阶段无法决定执行哪个分支,在运行时才能决定。因此,需要提交多个作业,示例如下:

@i BIGINT;
@t TABLE(id BIGINT, value BIGINT);
IF ((SELECT COUNT(*) FROM src WHERE a = '5') > 1) BEGIN
  @i := 1;
  @t := SELECT @i, @i*2;
END ELSE BEGIN
  @i := 2;
  @t := SELECT @i, @i*2;
END
SELECT id, value FROM @t;

使用示例

示例 1:基础示例

以下脚本对三张源表进行 JOIN 和 UNION 操作,将结果插入两张目标表。所有语句编译为一个 DAG,原子性执行。

CREATE TABLE IF NOT EXISTS dest(key STRING, value BIGINT) PARTITIONED BY (d STRING);
CREATE TABLE IF NOT EXISTS dest2(key STRING, value BIGINT) PARTITIONED BY (d STRING);

@a := SELECT * FROM src  WHERE value > 0;
@b := SELECT * FROM src2 WHERE key IS NOT NULL;
@c := SELECT * FROM src3 WHERE value IS NOT NULL;

@d := SELECT a.key, b.value FROM @a LEFT OUTER JOIN @b ON a.key = b.key AND b.value > 0;
@e := SELECT a.key, c.value FROM @a INNER JOIN @c ON a.key = c.key;
@f := SELECT * FROM @d UNION SELECT * FROM @e UNION SELECT * FROM @a;

INSERT OVERWRITE TABLE dest  PARTITION (d='20171111') SELECT * FROM @f;

@g := SELECT e.key, c.value FROM @e JOIN @c ON e.key = c.key;
INSERT OVERWRITE TABLE dest2 PARTITION (d='20171111') SELECT * FROM @g;

示例 2:单步执行模式下的先写后读

以下脚本开启单步执行模式,实现在同一脚本中先写入表再读取。

SET odps.sql.step.script.mode=true;

DROP TABLE IF EXISTS foo_t1;
DROP TABLE IF EXISTS foo_t2;

CREATE TABLE foo_t1(a STRING) LIFECYCLE 1;
CREATE TABLE foo_t2(a STRING) LIFECYCLE 1;

@x := SELECT 'hello, world' AS a;
INSERT OVERWRITE TABLE foo_t1 SELECT * FROM @x;
INSERT INTO foo_t2 SELECT * FROM foo_t1 UNION ALL SELECT * FROM foo_t1;

SELECT * FROM foo_t2;

示例 3:普通模式下的先写后读

普通模式下在同一脚本中先写入表再读取会报错。以下示例展示该错误及两种解决方案。

数据准备

CREATE TABLE src(key BIGINT, value BIGINT) LIFECYCLE 1;
CREATE TABLE src2(key BIGINT, value BIGINT) LIFECYCLE 1;
INSERT INTO src VALUES(1, 2), (3, 3);

报错示例(普通模式下先写后读)

INSERT OVERWRITE TABLE src2 SELECT * FROM src WHERE key > 0;
@a := SELECT * FROM src2;   -- 报错:src2 在同一脚本中先被写入再被读取
SELECT * FROM @a;

解决方案 1:开启单步执行模式

SET odps.sql.step.script.mode=true;

INSERT OVERWRITE TABLE src2 SELECT * FROM src WHERE key > 0;
@a := SELECT * FROM src2;
SELECT * FROM @a;

解决方案 2:改写 SQL,使用表变量避免先写后读

@a := SELECT * FROM src WHERE key > 0;
INSERT OVERWRITE TABLE src2 SELECT * FROM @a;
SELECT * FROM @a;

示例 4:创建和删除临时表

以下脚本创建临时表缓存 JOIN 中间结果,读取两次后删除。

-- 开启单步执行模式(临时表必须)
SET odps.sql.step.script.mode=true;

DROP TABLE IF EXISTS foo_t1;

CREATE TABLE foo_t1(a BIGINT, b BIGINT);

-- 通过 JOIN 创建临时表
CREATE TEMPORARY TABLE t AS
SELECT t1.a AS a, t2.d AS b FROM
  (SELECT 1 a, 2 b) t1
JOIN
  (SELECT 1 c, 10 d) t2
ON t1.a = t2.c;

INSERT INTO foo_t1 SELECT * FROM t UNION ALL SELECT * FROM t;

SELECT * FROM foo_t1;

-- 删除临时表(必须放在脚本末尾)
DROP TABLE t;

提交脚本

脚本模式支持在 MaxCompute Studio、MaxCompute 客户端(odpscmd)、DataWorks 以及 Java SDK 和 Python SDK 中使用。

通过MaxCompute 客户端(odpscmd)使用脚本模式

使用 odpscmd v0.27 及以上版本。安装 MaxCompute客户端安装包 后,通过 -s 参数提交脚本:

编辑脚本模式的源码myscript.sql文件,在系统命令行窗口调用odpscmd执行如下命令。更多通过系统命令行窗口运行MaxCompute客户端的操作,请参见运行MaxCompute客户端

..\bin>odpscmd -s myscript.sql
说明

-sodpscmd的命令行选项,类似于-f-e,而非交互环境中的命令。odpscmd的交互环境中暂不支持脚本模式与表变量。

通过DataWorks使用脚本模式

在 DataWorks 中创建 ODPS Script 节点。在节点编辑器中编写脚本,点击运行图标提交到 MaxCompute。通过输出面板中的 Logview URL 查看执行计划和结果。

脚本节点

在此节点中进行脚本模式编辑,编辑完成后单击工具栏的运行图标,提交脚本到MaxCompute执行。从输出信息的Logview URL中可以查看执行计划图和结果。

通过SDK使用脚本模式

Java/Python SDK中可以直接执行一个SQL脚本,Java SDK详情请参见Java SDK介绍,Python SDK详情请参见Python SDK介绍。代码示例如下。

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.aliyun.odps.Instance;
import com.aliyun.odps.Odps;
import com.aliyun.odps.OdpsException;
import com.aliyun.odps.account.Account;
import com.aliyun.odps.account.AliyunAccount;
import com.aliyun.odps.data.Record;
import com.aliyun.odps.task.SQLTask;

public class SdkTest {

  public static void main(String[] args) throws OdpsException {
		// 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户
		// 此处以把AccessKey 和 AccessKeySecret 保存在环境变量为例说明。您也可以根据业务需要,保存到配置文件里
		// 强烈建议不要把 AccessKey 和 AccessKeySecret 保存到代码里,会存在密钥泄漏风险
    Account account = new AliyunAccount(System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID"), System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET"));
    Odps odps = new Odps(account);
    odps.setDefaultProject("your project_name");
    odps.setEndpoint("your end_point");

    String sqlScript = "@a := SELECT * FROM jdbc_test;\n"
                       + "SELECT * FROM @a;";

    //一定要加这一行配置
    Map<String, String> hints = new HashMap<>();
    hints.put("odps.sql.submit.mode", "script");

    Instance instance = SQLTask.run(odps, "your project_name", sqlScript, hints, null);
    instance.waitForSuccess();

    List<Record> recordList = SQLTask.getResult(instance);
    for (Record record : recordList) {
      System.out.println(record.get(0));
      System.out.println(record.get(1));
    }
  }

}
import os
from odps import ODPS


# 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户
# 此处以把AccessKey 和 AccessKeySecret 保存在环境变量为例说明。您也可以根据业务需要,保存到配置文件里
# 强烈建议不要把 AccessKey 和 AccessKeySecret 保存到代码里,会存在密钥泄漏风险
o = ODPS(
    os.environ["ALIBABA_CLOUD_ACCESS_KEY_ID"],
    os.environ["ALIBABA_CLOUD_ACCESS_KEY_SECRET"],
    "your project_name",
    "your end_point"
)

sql_script = """
@a := SELECT * FROM jdbc_test;
SELECT * FROM @a;
"""

# 一定要加这一行配置
hints = {"odps.sql.submit.mode", "script"}
instance = o.execute_sql(sql_script, hints=hints)

with instance.open_reader() as reader:
    for rec in reader:
        print(rec[0], rec[1])

通过MaxCompute Studio使用脚本模式

在 MaxCompute Studio 中运行脚本前,需完成以下准备:

  1. 安装IntelliJ IDEA

  2. 管理项目连接

  3. 创建MaxCompute Script Module

编译并运行脚本后,MaxCompute Studio 显示执行计划。虽然脚本包含多条语句,但执行计划渲染为一个 DAG。