跳到主要内容

Databend 会话变量与脚本变量详解(含 CALL 传参限制)

· 阅读需 17 分钟
wubx
Data AI

Databend 的变量分两种:会话变量和脚本变量。两者定义方式、作用范围、引用写法都不同,$name、:name、name 三种写法容易混用;CALL system$xxx(...) 对参数又有单独的限制。

先给结论:

  • 会话变量:SET VARIABLE 定义,$name 引用,在当前会话内有效。
  • 脚本变量:LET 定义,只在 BEGIN ... END 内有效。SQL 语句里写 :name,IF、RETURN 等脚本表达式里写 name。
  • 两者命名空间独立,可以互相传值。
  • CALL system$xxx(...) 只接受固定字符串,传不了变量,需要改用 SELECT * FROM xxx(...)。

两种变量对比​

会话变量脚本变量
定义SET VARIABLE x = ...LET x := ...
适用于任意 SQL 会话脚本、存储过程、Task
作用范围当前会话所在的 BEGIN ... END 块
SQL 中引用$x:x
脚本表达式中引用$xx
能存什么标量值标量、结果集、游标
引用未定义变量返回 NULL报错
典型场景查询参数、动态表名判断、循环、动态 SQL

注意 SET max_threads = 8 这类语句改的是 Settings(会话配置项),不是变量,不能用 $ 引用。

会话变量​

定义​

SET VARIABLE 右边可以是常量、表达式或查询结果,也可以一次设多个:

SET VARIABLE threshold = 100;            -- 常量
SET VARIABLE start_day = today() - 7; -- 表达式
SET VARIABLE (x, y) = ('xx', 'yy'); -- 一次设多个
SET VARIABLE (a, b) = (SELECT 3, 55); -- 用查询结果赋值

引用​

SELECT * FROM sales WHERE amount > $threshold;

-- 与 $threshold 等价
SELECT * FROM sales WHERE amount > getvariable('threshold');

-- 用变量指定表名(见下文 IDENTIFIER 一节)
SET VARIABLE t = 'sales_data';
SELECT * FROM IDENTIFIER($t);

SHOW VARIABLES 查看当前会话的变量,UNSET VARIABLE threshold 删除变量。

注意事项​

引用不存在的变量返回 NULL,不报错
SELECT $undefined_var;
结果
getvariable('undefined_var')
----------------------------
NULL

变量名拼错时查询照常执行,只是条件全部落空。从列名也能看出,$name 是 getvariable('name') 的简写。

会话变量跟着会话走。BendSQL 里连续执行的语句属于同一个会话,变量可以一直复用;通过 HTTP API 或 Driver 执行时,不同请求不一定在同一个会话里,前一个请求设的值,后一个请求可能读不到。

适用场景​

会话变量适合交互式分析:把日期范围、阈值、租户 ID 设成变量,后续查询都引用它,改条件只改一处。配合 IDENTIFIER($t) 还能在同一组 SQL 里切换表名或列名(见下文)。

脚本变量​

在哪里使用​

脚本变量只存在于 BEGIN ... END 块中,常见的有三种:

-- 1. 临时执行一段脚本
EXECUTE IMMEDIATE $$ BEGIN ... END; $$;

-- 2. 存储过程:参数和 DECLARE 里的变量也是脚本变量
CREATE PROCEDURE p(limit_rows INT)
RETURNS INT LANGUAGE SQL
AS $$ BEGIN ... END; $$;

-- 3. 定时任务
CREATE TASK t SCHEDULE = 5 MINUTE
AS EXECUTE IMMEDIATE $$ BEGIN ... END; $$;

可以存什么​

EXECUTE IMMEDIATE $$
BEGIN
-- 标量:查询结果中的一个值
LET n := (SELECT count(*) FROM numbers(10));
-- 带类型的标量
LET rate FLOAT := 0.07;
-- 结果集
LET rs RESULTSET := SELECT number FROM numbers(3);

LET total := 0;
FOR r IN rs DO -- r 是循环行变量
total := total + r.number; -- 不带 LET 是重新赋值
END FOR;

RETURN n + total; -- 10 + 3 = 13
END;
$$;

另外还有游标:LET cur CURSOR FOR SELECT ...,用 OPEN、FETCH ... INTO、CLOSE 逐行读取。变量声明后未赋值就读取,会报 code=3129: unknown var。

作用范围​

变量只在声明它的块内有效。内层块用 LET 声明同名变量会遮蔽外层的值,离开内层块后恢复:

EXECUTE IMMEDIATE $$
BEGIN
LET t := 10;
IF t > 0 THEN
LET t := 5; -- 只在 IF 块内有效
END IF;
RETURN t; -- 10
END;
$$;

引用规则:name 还是 :name​

脚本里的代码分两层,同一个变量在两层里写法不同:

  • 脚本表达式:IF 条件、LET 右侧、赋值、RETURN 等,由脚本引擎解析,直接写 n。
  • SQL 语句:SELECT、INSERT、SET VARIABLE、FOR ... IN SELECT ... 等,由 SQL 引擎解析,要写 :n。脚本编译器在执行前把 :n 替换成变量的值。
EXECUTE IMMEDIATE $$
BEGIN
LET n := 5;
IF n > 0 THEN -- 脚本表达式:n
-- SQL 语句::n
RETURN TABLE(SELECT :n AS v, :n * 2 AS v2);
END IF;
END;
$$;

两个方向写反都会报错:

错误写法报错
SQL 语句漏了冒号
SELECT n
column n doesn't exist(1065)
脚本表达式多了冒号
IF :n > 0
variable doesn't need to be quoted in this context, try removing the colon(3128)

如果报 column xxx doesn't exist,而 xxx 是脚本里定义的变量,就是漏了冒号。

冒号用来区分变量和列。SQL 语句中的名字可能是列也可能是变量,加冒号表示变量,所以变量和列同名也不会冲突:

EXECUTE IMMEDIATE $$
BEGIN
LET number := 2;
-- number 是 numbers() 的列,:number 是变量 2;返回 0、1
RETURN TABLE(
SELECT number FROM numbers(5) WHERE number < :number
);
END;
$$;

字符串里的 :n 不会被替换,SELECT ':n' 返回的就是文本 :n。

这套写法沿用了 Snowflake Scripting 的约定,不是 ISO SQL 标准。Snowflake 文档 Working with variables 的说法是:在 SQL 语句中使用变量(binding)要加冒号,在表达式和 RETURN 等脚本语言元素中不需要。PL/pgSQL、MySQL 存储过程在 SQL 中直接写变量名,从这两个数据库迁移过来时要特别留意。

适用场景​

脚本变量用在需要逻辑控制的地方:

  • 查出一个指标存进变量,再用 IF 决定是否执行后续操作,比如发告警、写审计表。
  • 用 RESULTSET 或 FOR ... IN SELECT 逐行处理,把结果累加到变量。
  • 在变量里拼出 SQL 字符串,交给 EXECUTE IMMEDIATE 执行(动态 SQL)。
  • 用 IDENTIFIER(:t) 指定表名或列名(见下文)。
  • 把逻辑封装成存储过程或 Task,参数就是脚本变量。

动态 SQL 的写法:

EXECUTE IMMEDIATE $$
BEGIN
LET n := 5;
LET q := 'SELECT ' || TO_STRING(n) || ' AS v';
LET rs RESULTSET := EXECUTE IMMEDIATE :q;
RETURN TABLE(rs); -- v = 5
END;
$$;

两种变量之间的关系​

会话变量和脚本变量是两个独立的命名空间,同名互不影响:

EXECUTE IMMEDIATE $$
BEGIN
SET VARIABLE msg = 'session';
LET msg := 'script';
-- s = session,l = script
RETURN TABLE(SELECT $msg AS s, :msg AS l);
END;
$$;

两者之间可以互相传值。

会话变量传给脚本变量:脚本里可以直接读会话变量,$name 在脚本表达式和 SQL 语句里都能用,不用加冒号。

EXECUTE IMMEDIATE $$
BEGIN
SET VARIABLE x = 10;
LET n := $x; -- 也可以写 getvariable('x')
IF $x > 5 THEN -- 直接用在 IF 条件里
RETURN n + 1; -- 11
END IF;
END;
$$;

脚本变量传给会话变量:SET VARIABLE 是 SQL 语句,引用脚本变量要加冒号。

EXECUTE IMMEDIATE $$
BEGIN
LET n := 7;
SET VARIABLE y = :n * 2;
RETURN TABLE(SELECT $y AS y); -- 14
END;
$$;

如何选择​

需求用哪种
BendSQL 里连续跑几条查询,复用同一个参数会话变量
一段逻辑里有判断、循环,要传递中间结果脚本变量
存储过程、Task 里的逻辑脚本变量,不依赖会话状态
保存整个查询结果,后面逐行处理脚本变量(RESULTSET、CURSOR)
从外部给脚本传参存储过程参数,或先 SET VARIABLE 再读 $name

用 IDENTIFIER 引用表名和列名​

变量直接写在 SQL 里代表一个值,不是表名或列名。要把变量当作对象名,需要用 IDENTIFIER() 包起来:会话变量写 IDENTIFIER($name),脚本变量写 IDENTIFIER(:name)。

加与不加的区别​

SET VARIABLE column_name = 'number';

SELECT $column_name AS without_identifier,
IDENTIFIER($column_name) AS with_identifier
FROM numbers(3);
结果
without_identifier | with_identifier
-------------------+----------------
number | 0
number | 1
number | 2

$column_name 每行返回的都是字符串 number,IDENTIFIER($column_name) 取的才是 number 列的值。

会话变量​

IDENTIFIER() 可以用在 SELECT、FROM、WHERE、ORDER BY 等位置:

SET VARIABLE (db, table_name, column_name)
= ('system', 'tables', 'name');

SELECT IDENTIFIER($column_name)
FROM IDENTIFIER($db).IDENTIFIER($table_name)
WHERE database = 'system'
AND IDENTIFIER($column_name) LIKE 'set%'
ORDER BY IDENTIFIER($column_name);
结果
name
--------
settings
会话变量里不要放"库名.表名"

IDENTIFIER($t) 把变量值当成一个标识符:

SET VARIABLE t = 'system.one';
SELECT * FROM IDENTIFIER($t);
-- Unknown table "default"."default".system.one

system.one 被整体当成当前库里的一张表。库名和表名要分开存,写成 IDENTIFIER($db).IDENTIFIER($t),或者把库名写死:system.IDENTIFIER($t)。

脚本变量​

EXECUTE IMMEDIATE $$
BEGIN
LET col := 'number';
-- mx = 9,c = 5
RETURN TABLE(
SELECT max(IDENTIFIER(:col)) AS mx, count(*) AS c
FROM numbers(10)
WHERE IDENTIFIER(:col) > 4
);
END;
$$;

脚本变量的值可以带库名,LET t := 'system.one'; 之后 SELECT * FROM IDENTIFIER(:t) 能正常查询,这一点和会话变量不同。为了两种写法通用,库名和表名最好分开存。

参数只能是变量​

IDENTIFIER() 的参数只能是一个变量,不能是字符串字面量或表达式:

写法结果
IDENTIFIER($t)、IDENTIFIER(:t)正常
IDENTIFIER('system.one')语法错误:expecting IdentVariable
IDENTIFIER($db || '.' || $tbl)语法错误:unexpected '||'

需要拼接时,先把结果存进一个变量再交给 IDENTIFIER(),或者把整条 SQL 拼成字符串,用 EXECUTE IMMEDIATE 执行。

CALL system$xxx 不支持变量​

system$ 开头的系统函数可以用 CALL 调用,比如发通知:

CALL system$send_notification('feishu_alerts', 'Daily load completed.');

CALL 是语法糖,Databend 会把它改写成下面的 SELECT 再执行:

SELECT * FROM send_notification('feishu_alerts', 'Daily load completed.');

CALL 的参数在语法上只接受字符串、标识符、整数和布尔值这几种字面量。改写时,每个参数都原样加上引号拼进 SQL,不做求值(见源码 src/query/sql/src/planner/binder/call.rs)。所以会话变量、脚本变量和函数表达式都传不进去。以 CALL system$send_notification('feishu', <第二个参数>) 为例:

第二个参数结果
'text'正常
CONCAT('a', 'b')语法错误:unexpected '('
getvariable('msg')语法错误:unexpected '('
:msg语法错误:unexpected ':'
$msg不报错,发出的是文本 $msg
some_ident不报错,发出的是文本 some_ident

最后两种情况不报错,但发出去的内容不对,最容易漏掉。

在 BEGIN ... END 脚本里,CALL 整条语句都会被拒绝,报 CALL in script is not supported yet(1002),参数是常量也一样(见源码 src/query/script/src/compiler.rs)。

改用 SELECT 形式即可。它的参数是普通表达式,两种变量和函数都能用:

EXECUTE IMMEDIATE $$
BEGIN
LET n := 3;
SET VARIABLE msg = 'from session';

-- 脚本变量
SELECT * FROM send_notification(
'feishu', CONCAT('errors=', TO_STRING(:n))
);
-- 会话变量
SELECT * FROM send_notification('feishu', $msg);
END;
$$;

执行后,system.notification_history 中记录的是变量的值。

简单的规则:固定内容、手动执行时可以用 CALL;内容里有变量或表达式,或者在脚本、存储过程、Task 里,都用 SELECT 形式。

以上说的是 CALL system$xxx。CALL PROCEDURE(调用存储过程)是另一种语句。

调试脚本​

脚本里单独写的 SELECT 会执行,但结果不会返回,EXECUTE IMMEDIATE 只返回 NULL。要查看变量的值,用 RETURN TABLE(...) 包起来。

发通知、写表这类有副作用的脚本,可以先把变量写成常量,预览变量值、判断条件和最终内容:

EXECUTE IMMEDIATE $$
BEGIN
LET failed_count := 3;
RETURN TABLE(
SELECT :failed_count AS failed_count,
:failed_count > 0 AS will_send,
CONCAT('Query errors: ', TO_STRING(:failed_count))
AS message
);
END;
$$;
结果
failed_count | will_send | message
-------------+-----------+----------------
3 | 1 | Query errors: 3

确认无误后,把常量换回真实查询,把 RETURN TABLE(...) 换成实际操作,手动执行一次,再放进存储过程或 Task。

完整示例:飞书告警 Task​

最近 10 分钟有查询报错时发送飞书通知:

CREATE TASK notify_query_errors
WAREHOUSE = 'monitoring_wh'
SCHEDULE = 5 MINUTE
AS
EXECUTE IMMEDIATE $$
BEGIN
-- 脚本变量:存查询结果
LET failed_count := (
SELECT count(*)
FROM system_history.query_history
WHERE event_time >= now() - INTERVAL 10 MINUTE
AND log_type = 3
);

-- 脚本表达式:不加冒号
IF failed_count > 0 THEN
-- 脚本里用 SELECT 形式,不用 CALL;SQL 语句里加冒号
SELECT * FROM send_notification(
'feishu_alerts',
CONCAT('最近 10 分钟查询错误数:',
TO_STRING(:failed_count))
);
END IF;
END;
$$;

system_history.query_history 需要开通 Query History 功能,未开通会报 Unknown table,可以换成其他条件查询。

速查​

想做的事写法
定义会话变量SET VARIABLE x = ...
引用会话变量$x / getvariable('x')
定义脚本变量LET x := ...
脚本表达式中用脚本变量x
SQL 语句中用脚本变量:x
变量指定表名、列名IDENTIFIER($t) / IDENTIFIER(:t)
执行拼出来的 SQLEXECUTE IMMEDIATE :q
看脚本里的查询结果RETURN TABLE(SELECT ...)
调用 system$xxx 并传变量SELECT * FROM xxx(...)

参考​

文中示例的运行环境:Databend Cloud,Databend Query v1.2.956。