指令碼模式(Script Mode SQL)將多條 SQL 陳述式作為一個整體編譯和提交,產生一份執行計畫,只需排隊一次、執行一次。適用於 ETL 資料管道、周期性批處理,以及需要多條語句協同完成的查詢編排情境。指令碼模式支援普通模式(預設,原子性執行)和逐步執行模式(逐條串列執行)。
適用情境
-
複雜單語句改寫:將深度嵌套的子查詢拆分為一系列可讀的表變數指派陳述式。
-
多語句管道:將邏輯上相關的多條語句合并為一個作業統一提交,減少排隊和調度開銷。普通模式下所有語句原子性執行;逐步執行模式下語句逐條串列執行,適用於需要先寫後讀或跨平台遷移的情境。詳情請參見執行模式。
文法結構
-
語句類型:指令碼模式支援SET語句、部分DDL語句(不支援結果為屏顯類型的語句如DESC、SHOW)、DML語句。
-
語句順序:指令碼的完整形式是
SET→DDL→DML的固定順序,每個語句類型都可以包含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 部分添加 |
|
編譯 |
所有 DML 語句編譯為一個執行計畫。 |
每條 DML 語句獨立編譯為一個執行計畫。注意:非常複雜的指令碼(例如上千行、包含大量操作)可能超出編譯記憶體限制。 |
|
執行 |
所有語句作為一個作業原子執行,所有輸入資料就緒後才開始。任一語句失敗,整個指令碼失敗,所有操作復原。 |
DML 語句按順序逐條執行。某條語句失敗時,已執行的語句不會復原,只能從頭重試。建議將大指令碼拆分為多個較小指令碼。 |
|
先寫後讀 |
不支援。在同一指令碼中對同一張表先寫入再讀取會報錯。 |
支援。目前不支援事務表和分區表。 |
|
暫存資料表 |
不支援。 |
支援。 |
DDL 文法
建立暫存資料表
指令碼模式下支援建立暫存資料表,緩衝中間結果,供同一指令碼內複用。此時由於需要先寫後讀,僅支援在逐步執行模式下運行。
文法
CREATE TEMPORARY TABLE <table_name> (
<col_name> <data_type>, ...
)
[LIFECYCLE <days>]
[AS <select_statement>];
參數說明
|
參數 |
說明 |
|
|
暫存資料表名稱。僅在當前指令碼內可訪問。 |
|
|
可選。表自動刪除前的保留天數。預設值:1。 |
|
|
可選。建立時通過 SELECT 語句填充表資料。 |
使用限制
-
暫存資料表需要開啟逐步執行模式。在指令碼的 SET 部分添加
SET odps.sql.step.script.mode=true;。 -
暫存資料表僅在建立它的指令碼內可訪問。
-
暫存資料表不能是分區表或事務表。
-
如需顯式刪除暫存資料表,
DROP TABLE語句必須放在指令碼末尾。 -
同一個指令碼中建立的暫存資料表不建議超過20個。
建立普通表
指令碼模式支援建立普通表。文法詳情請參見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
使用說明
-
當分支只包含一條語句時,關鍵字
BEGIN和END可省略(類似 Java 中的{ })。 -
IF 分支內不支援
CREATE TABLE、ALTER TABLE、TRUNCATE 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
-s為odpscmd的命令列選項,類似於-f、-e,而非互動環境中的命令。odpscmd的互動環境中暫不支援指令碼模式與表變數。
通過DataWorks使用指令碼模式
在 DataWorks 中建立 ODPS Script 節點。在節點編輯器中編寫指令碼,點擊運行表徵圖提交到 MaxCompute。通過輸出面板中的 Logview URL 查看執行計畫和結果。
單擊左上方 + Create > Create Node,在 MaxCompute 分類下選擇 ODPS Script。
在此節點中進行指令碼模式編輯,編輯完成後單擊工具列的运行表徵圖,提交指令碼到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 中運行指令碼前,需完成以下準備:
編譯並運行指令碼後,MaxCompute Studio 顯示執行計畫。雖然指令碼包含多條語句,但執行計畫渲染為一個 DAG。