用户自定义变量
存储在用户自定义的变量中的值,可以被其他语句引用,以实现将值 从一个语句传递到另一个语句。
用户变量的书写格式为 @var_name,其中变量名 var_name 由字母、数字、.、_ 和 $ 组成。如果将用户变量名称引用为字符串或标识符(例如,@'my-obvar'、@"my-obvar" 或 @`my-obvar`),则该名称可以包含其他字符。用户变量名不区分大小写,名称的最大长度为 64 个字符。
用户自定义变量只用于指定会话。一个客户端定义的用户变量对其他客户端不可见或不可使用。当该客户端退出时,将自动释放指定客户端会话的所有变量。
设置用户定义变量的一种方法是使用 SET 语句:
SET @var_name = expr [, @var_name = expr] ...
详细信息,请参见 SET。
对于 SET 语句,= 或 := 都可以用作赋值运算符。
可以为用户 变量分配以下数据类型的值:整数、十进制、浮点、二进制或非二进制字符串,或 NULL 值。十进制和实数值的赋值不会保留值的精度或小数位数。其他数据类型的值被转换为允许的数据类型。例如,时间或空间数据类型的值被转换为二进制字符串。
要读取一个用户自定义变量,可以使用 SELECT 语句。
如果为用户变量分配了非二进制(字符)字符串值,则它具有与字符串相同的字符集和字符序。分配给用户变量的十六进制或位值被视为二进制字符串。要将十六进制或位值作为数字分配给用户变量,可以添加 0 或使用 CAST 函数 CAST(... AS UNSIGNED):
SET @v1 = X'43';
SET @v2 = X'43'+0;
SET @v3 = CAST(X'43' AS UNSIGNED);
SELECT @v1, @v2, @v3;
+------+------+------+
| @v1 | @v2 | @v3 |
+------+------+------+
| C | 67 | 67 |
+------+------+------+
1 row in set (0.001 sec)
SET @v1 = b'1000011';
SET @v2 = b'1000011'+0;
SET @v3 = CAST(b'1000011' AS UNSIGNED);
SELECT @v1, @v2, @v3;
+------+------+------+
| @v1 | @v2 | @v3 |
+------+------+------+
| C | 67 | 67 |
+------+------+------+
1 row in set (0.000 sec)
如果在结果集中选择了用户变量的值,则将其作为字符串返回给客户端。如果引用未初始化的变量,则它的值为 NULL 且类型为字符串。
限制说明
seekdb 的用户自定义变量具有如下使用限制:
- seekdb 可以在
SET以外的语句中为用户变量赋值,但可能会在未来版本中删除。 - 未定义涉及用户变量的表达式的求值顺序。例如,
SELECT @a, @a:=@a+123不能保证首先计算@a,然后执行赋值表达式。为避免此问题,请不要在单个语句中为某一变量赋值并同时读取该变量的值。 - 并行执行时无法保证用户自定义变量计算的正确性,需要使用
/*+USE_DAS(...)*/指定串行执行。后续版本计划优化为无需指定 Hint,自适应走串行执行。
seekdb 兼容性说明
- 在 SQL 语句里不使用
@var:=val语法,只是读取用户变量,跟 MySQL 是兼容的,例如SELECT c1, c2 FROM t1 WHERE c1=@var;。 - 在 SQL 执行过程中使用
:=运算实时修改用户变量值,则用户变量行为是 Undefined 的,具体的表现跟执行顺序有关。 - 在 SQL 执行过程中使用
:=运算实时修改用户变量值,seekdb 在以下场景保持跟 MySQL5.6/5.7/8.0 兼容,其余情况的结果则跟具体执行计划的类型有关,可能跟 MySQL 行为不一致。- 出现在
INSERT VALUES子句中,例如,INSERT INTO t1 VALUES(@a:=1, @a);。
- 出现在
若 SQL 语句中进行了用户变量赋值,并且执行结果受到该变量先前值的影响,那么在执行失败重试的情况下,由于用户变量初始值变化,赋值结果可能与 MySQL 存在兼容性差异。
初始化用户变量
用户自定义变量如果没有进行初始化赋值,则数据库默认值为 NULL,因此,在使用用户自定义变量模拟窗口函数的行为中,通常会在 SQL 的 FROM 子查询中通过 SELECT @var:=val from dual 来对用户变量进行初始化,跟主查询构成一个 JOIN 查询,例如:
SELECT A.c1, (@rownum:= @rownum+1) AS rownum FROM t1 A,(SELECT @rownum:=0) B;
SELECT @rownum;
如果该查询选择使用 NESTED LOOP JOIN 算法,并且将 B 表作为 JOIN 的内表,当外表数据是空集时,seekdb 执行引擎将采用短路执行策略,并不会驱动 B 表进行计算,因此 B 表上的用户变量初始化操作将不会被执行,而 MySQL 无论外表是否为空,都会执行 SELECT @var:=val from dual 这类初始化语句。
--MySQL 无论 t1 中是否存在数据,均会执行 rownum:=0 的初始化动作
SELECT A.c1, (@rownum:= @rownum+1) AS rownum FROM t1 A,(SELECT @rownum:=0) B;
Empty set, 2 warnings
SELECT @rownum;
+---------+
| @rownum |
+---------+
| 0 |
+---------+
1 row in set
-- seekdb 使用 Nested Loop Join,当 t1 数据为空,则不会执行 rownum:=0
SELECT /*+use_nl(A, B)*/ A.c1, (@rownum:= @rownum+1) AS rownum FROM t1 A,(SELECT @rownum:=0) B;
Empty set
SELECT @rownum;
+---------+
| @rownum |
+---------+
| NULL |
+---------+
1 row in set
为了兼容 MySQL 这一行为,seekdb 将出现在 FROM 子句中,并且不包含实体表的用户变量赋值语句视作用户变量初始化操作,在语句执行最初期,会按照语句指定的先 后顺序优先计算这些初始化动作的赋值语句。 例如:
SELECT /*+use_nl(t1, A)*/ * FROM t1, (SELECT @rownum:=0 FROM DUAL) A;
--SELECT @rownum:=0 为变量初始化操作,先执行该子句
SELECT * FROM t1 WHERE (SELECT @rownum:=0 FROM DUAL)=0;
--SELECT @rownum:=0 没有在 FROM 子句中,不是初始化语句,是否被执行取决于 t1 是否有数据
SELECT /*+use_nl(t1, A)*/ * FROM t1, (SELECT @rownum:=(SELECT c1 FROM t2)) A;
--SELECT @rownum:=(SELECT c1 FROM t1) 虽然位于 FROM 子句,但访问实体表 t2,不是初始化语句
SELECT * FROM t1 WHERE (SELECT rownum FROM (SELECT @rownum:=0) t)=0;
--SELECT @rownum:=0 位于 FROM 子句中,是初始化语句
@var:=val 出现在 FROM 子查询中
当用户变量赋值操作出现在 FROM 子查询中,MySQL 会禁止 FROM 子查询之间的合并,并且两层查询间的数据引用关系不再以流式的方式进行迭代,而是将包含用户变量 := 的 FROM 子查询的结果全部物化到临时表中供上层查询访问,例如:
CREATE TABLE t1(c1 INT);
Query OK, 0 rows affected (0.039 sec)
INSERT INTO t1 VALUES(1), (2), (3);
Query OK, 3 rows affected (0.003 sec)
Records: 3 Duplicates: 0 Warnings: 0
SELECT @a, c1 FROM (SELECT @a:=c1, c1 FROM t1) t;
+------+------+
| @a | c1 |
+------+------+
| 3 | 1 |
| 3 | 2 |
| 3 | 3 |
+------+------+
3 rows in set (0.002 sec)
从最终的结果变现可以看到 SELECT @a:=c1, c1 FROM t1 先物化后再执行了外层查询 SELECT @a, c1 FROM t。
MySQL 这一特性的结果是稳定的,并且被用户用来模拟窗口函数的各种分组计算,seekdb 优化器兼容该行为。
当用户变量 := 操作出现在 FROM 子查询中,OB 优化器将禁止掉各层 FROM 子查询间的 VIEW MERGE 改写,并在每层子查询之间添加 MATERIALoperator 来阻塞查询间的数据流式计算,从而保证跟 MySQL 的行为兼容。
EXPLAIN SELECT @a, c1 FROM (SELECT @a:=c1, c1 FROM t1) t;
+-------------------------------------------------------------------+
| Query Plan |
+-------------------------------------------------------------------+
| ================================================= |
| |ID|OPERATOR |NAME|EST.ROWS|EST.TIME(us)| |
| ------------------------------------------------- |
| |0 |SUBPLAN SCAN |t |3 |3 | |
| |1 |└─TABLE FULL SCAN|t1 |3 |3 | |
| ================================================= |
| Outputs & filters: |
| ------------------------------------- |
| 0 - output([('a')], [t.c1]), filter(nil) |
| access([t.c1]) |
| 1 - output([(T_OP_ASSIGN, 'a', t1.c1)], [t1.c1]), filter(nil) |
| access([t1.c1]), partitions(p0) |
| is_index_back=false, is_global_index=false, |
| range_key([t1.__pk_increment]), range(MIN ; MAX)always true |
+-------------------------------------------------------------------+
14 rows in set (0.002 sec)
需要注意的是,在 FROM 子查询中使用 := 可能导致 FROM 子查询被提前计算和结果物化,这样可能会影响原始语句的 JOIN 条件下推到基表等优化,导致 NESTED LOOP JOIN 无法使用内表上的索引条件,从而影响执行效率,例如:
CREATE TABLE t1(a INT, b INT);
CREATE TABLE t2(a INT PRIMARY KEY, b INT);
EXPLAIN SELECT /*+use_nl(t1 t)*/ * FROM t1 JOIN (SELECT @b:=b AS b, a FROM t2) t ON t1.a=t.a WHERE t1.b=1;
+------------------------------------------------------------------------------------+
| Query Plan |
+------------------------------------------------------------------------------------+
| =================================================== |
| |ID|OPERATOR |NAME|EST.ROWS|EST.TIME(us)| |
| --------------------------------------------------- |
| |0 |MERGE JOIN | |1 |5 | |
| |1 |├─SUBPLAN SCAN |t |1 |3 | |
| |2 |│ └─TABLE FULL SCAN|t2 |1 |3 | |
| |3 |└─SORT | |1 |3 | |
| |4 | └─TABLE FULL SCAN|t1 |1 |3 | |
| =================================================== |
| Outputs & filters: |
| ------------------------------------- |
| 0 - output([t1.a], [t1.b], [t.b], [t.a]), filter(nil) |
| equal_conds([t1.a = t.a]), other_conds(nil) |
| merge_directions([ASC]) |
| 1 - output([t.a], [t.b]), filter(nil) |
| access([t.a], [t.b]) |
| 2 - output([(T_OP_ASSIGN, 'b', t2.b)], [t2.a]), filter(nil) |
| access([t2.a], [t2.b]), partitions(p0) |
| is_index_back=false, is_global_index=false, |
| range_key([t2.a]), range(MIN ; MAX)always true |
| 3 - output([t1.a], [t1.b]), filter(nil) |
| sort_keys([t1.a, ASC]) |
| 4 - output([t1.a], [t1.b]), filter([t1.b = 1]) |
| access([t1.a], [t1.b]), partitions(p0) |
| is_index_back=false, is_global_index=false, filter_before_indexback[false], |
| range_key([t1.__pk_increment]), range(MIN ; MAX)always true |
+------------------------------------------------------------------------------------+
26 rows in set (0.011 sec)
@var:=val 出现在 DML 语句中
seekdb 允许使用用户变量 := 来提取 DML 执行过程中的新旧值,优化客户端和数据库的交互次数,但有以下限制:
-
DML 因为内部执行的限制,无法保证表达式的计算顺序和用户定义顺序严格一致,因此不要在 DML 子句中对同一个用户变量进行赋值和访问运算,例如:
INSERT INTO t1(c1, c2) VALUES(@a:=1, @a+1);UPDATE t1 SET c1=@a:=1, c2=@a+1 WHERE c1=1;这两个例子中均无法保证@a:=1一定先于@a+1被执行。 -
DML 中使用
@var:=val只有在 DML affected_row=1 时才跟 MySQL 兼容,在 affected_row>1 时,因为 SQL 数据的执行顺序可能跟 MySQL 不一致,最终用户变量的结果可能存在差异。
@var:=val 出现在 SELECT 子句中
部分业务场景可能存在同一个子句中对部分表达式进行重复冗余计算,从而利用用户变量赋值后再引用的方式来复用前者的计算结果,例如 SELECT @a:=(c1+1), @a+c2, @a*c3 FROM t1,但 MySQL 官方文档明确说明这种情况无法保证包含用户变量的表达式的执行顺序,因此无法保证结果符合用户期望。
通过对 MySQL 具体行为调研得出结论,当上述查询出现在最上层查询的 SELECT 子句,或者 FROM 子查询中的 SELECT 子句中表达式的执行顺序跟用户定义顺序保持一致,在其它子句中不保证一定一致,例如:
SELECT @a:=(c1+1), @a+c2, @a*c3 FROM t1;
--出现在顶层查询中,表达式执行顺序按照用户定义顺序执行
SELECT * FROM (SELECT @a:=(c1+1), @a+c2, @a*c3 FROM t1) t;
--出现在 FROM 子查询中,表达式执行顺序按照用户定义顺序执行
SELECT * FROM t1 WHERE (c1, c2) in (SELECT @a:=c1, @a+1 FROM t2);
--出现在 WHERE 子查询中,包含用户变量的表达式不保证按照用户定义顺序执行,查询结果可能存在不确定性
seekdb 只兼容 MySQL 确定性行为,对于不确定性行为可能跟 MySQL 不兼容。
@var:=val 出现在 WHERE 子句中
用户变量 := 操作在 WHERE 子句中执行结果取决于谓词条件的执行顺序,MySQL 的表现跟实现有关,这类场景 seekdb 均不承诺兼容,例如:
SELECT * FROM t1 WHERE (@a:=c1)>10;
--查询的结果以及 @a 最终结果均跟执行有关,不跟 MySQL 兼容
SELECT * FROM t1 WHERE c1 in (SELECT @a:=1 FROM t2);
--位于 WHERE 子句中,结果跟实现有关,不跟 MySQL 兼容
@var:=val 出现在 ORDER BY 子句中
ORDER BY 子句会对数据进行重排,从而打乱数据的迭代顺序,ORDER BY 中出现用户变量 := 将导致结果出现无法预期的表现,seekdb 关于用户变量出现在 ORDER BY 子句无法做到跟 MySQL 完全兼容,当 ORDER BY 子句使用用户变量,各自的行为是实现定义的,例如:
CREATE TABLE t1(a INT PRIMARY KEY, b INT);
INSERT INTO t1 VALUES(1, 1), (2, 1), (3, 1), (4, 1), (5, 5), (6, 6);
--MySQL 行为
SELECT @rownum:=@rownum+1 AS rn, MAX(a), sum(a) FROM t1, (SELECT @rownum:=1) t GROUP BY b ORDER BY rn;
+------+--------+--------+
| rn | max(a) | sum(a) |
+------+--------+--------+
| 2 | 4 | 10 |
| 3 | 5 | 5 |
| 4 | 6 | 6 |
+------+--------+--------+
3 rows in set, 2 warnings
--当 ORDER BY 的排序顺序是正序时,rownum 的最终结果是指向最后一行值的结果,为4
SELECT @rownum;
+---------+
| @rownum |
+---------+
| 4 |
+---------+
1 row in set
SELECT @rownum:=@rownum+1 AS rn, MAX(a), SUM(a) FROM t1, (SELECT @rownum:=1) t GROUP BY b ORDER BY rn desc;
+------+--------+--------+
| rn | max(a) | sum(a) |
+------+--------+--------+
| 4 | 6 | 6 |
| 3 | 5 | 5 |
| 2 | 4 | 10 |
+------+--------+--------+
3 rows in set, 2 warnings
--当 ORDER BY 的排序顺序是逆序时,rownum 的最终结果是指向最后一行值的结果,为2
SELECT @rownum;
+---------+
| @rownum |
+---------+
| 2 |
+---------+
1 row in set
--seekdb 行为
SELECT @rownum:=@rownum+1 AS rn, MAX(a), SUM(a) FROM t1, (SELECT @rownum:=1) t GROUP BY b ORDER BY rn;
+------+--------+--------+
| rn | MAX(a) | SUM(a) |
+------+--------+--------+
| 2 | 4 | 10 |
| 3 | 5 | 5 |
| 4 | 6 | 6 |
+------+--------+--------+
3 rows in set, 1 warning (0.002 sec)
--当 ORDER BY 的排序顺序是正序时,rownum 的最终结果是指向最后一行值的结果,为4
SELECT @rownum;
+---------+
| @rownum |
+---------+
| 4 |
+---------+
1 row in set (0.001 sec)
SELECT @rownum:=@rownum+1 AS rn, MAX(a), SUM(a) FROM t1, (SELECT @rownum:=1) t GROUP BY b ORDER BY rn desc;
+------+--------+--------+
| rn | MAX(a) | SUM(a) |
+------+--------+--------+
| 4 | 6 | 6 |
| 3 | 5 | 5 |
| 2 | 4 | 10 |
+------+--------+--------+
3 rows in set, 1 warning (0.002 sec)
--当 ORDER BY 的排序顺序是逆序时,rownum 的最终结果是指向第一行值的结果,依然为4
SELECT @rownum;
+---------+
| @rownum |
+---------+
| 4 |
+---------+
1 row in set (0.000 sec)
因此在 ORDER BY 子句中使用了用户自定义变量 := 进行赋值运算,或者间接引用了用户自定义变量 := 运算后的列进行排序运算,seekdb 无法保证经过排序运算后用户自定义变量的结果依然跟 MySQL 保持兼容,因此不要在经过 ORDER BY 子句后再次使用该用户自定义变量进行相关的运算。
@var:=val 将导致查询无法并行
当查询中包含 @var:=val 运算,虽然 seekdb 无法做到所有场景都兼容 MySQL 行为,但 seekdb 需要做到包含这类运算的查询最终可以自我解释,因此在包含 @var:=val 赋值操作的查询中将强制执行流程串行,针对这类查询显示执行并行将无法生效,例如:
EXPLAIN EXTENDED_NOADDR SELECT /*+parallel(2)*/ @a:=c1 FROM t1;
+-------------------------------------------------------------------+
| Query Plan |
+-------------------------------------------------------------------+
| =========================================================== |
| |ID|OPERATOR |NAME|EST.ROWS|EST.TIME(us)| |
| ----------------------------------------------------------- |
| |0 |DISTRIBUTED TABLE FULL SCAN|t1 |1 |4 | |
| =========================================================== |
| Outputs & filters: |
| ------------------------------------- |
| 0 - output([(T_OP_ASSIGN, 'a', t1.c1)]), filter(nil) |
| access([t1.c1]), partitions(p0) |
| is_index_back=false, is_global_index=false, |
| range_key([t1.__pk_increment]), range(MIN ; MAX)always true |
| Used Hint: |
| ------------------------------------- |
| /*+ |
| |
| PARALLEL(2) |
| */ |
| Qb name trace: |
| ------------------------------------- |
| stmt_id:0, stmt_type:T_EXPLAIN |
| stmt_id:1, SEL$1 |
| Outline Data: |
| ------------------------------------- |
| /*+ |
| BEGIN_OUTLINE_DATA |
| FULL(@"SEL$1" "test"."t1"@"SEL$1") |
| USE_DAS(@"SEL$1" "test"."t1"@"SEL$1") |
| PARALLEL(2) |
| OPTIMIZER_FEATURES_ENABLE('4.0.0.0') |
| END_OUTLINE_DATA |
| */ |
| Optimization Info: |
| ------------------------------------- |
| t1: |
| table_rows:1 |
| physical_range_rows:1 |
| logical_range_rows:1 |
| index_back_rows:0 |
| output_rows:1 |
| table_dop:1 |
| dop_method:DAS DOP |
| avaiable_index_name:[t1] |
| stats version:0 |
| dynamic sampling level:0 |
| Plan Type: |
| LOCAL |
| Note: |
| Degree of Parallelism is 1 because of hint |
+-------------------------------------------------------------------+
48 rows in set