Databend 会话变量与脚本变量详解(含 CALL 传参限制)
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 |
| 脚本表达式中引用 | $x | x |
| 能存什么 | 标量值 | 标量、结果集、游标 |
| 引用未定义变量 | 返回 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 删除变量。
注意事项
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) |
| 执行拼出来的 SQL | EXECUTE IMMEDIATE :q |
| 看脚本里的查询结果 | RETURN TABLE(SELECT ...) |
调用 system$xxx 并传变量 | SELECT * FROM xxx(...) |
参考
文中示例的运行环境:Databend Cloud,Databend Query v1.2.956。
