peekResult(...) 用于在全部 sql 执行完成前,提前输出少量中间结果到前端或日志中。
适用场景:
peekResult(子sql)
子sql 是一条完整的 select 语句,peekResult 会立即执行它并输出结果。${param} / #{param})和 @list} 变量替换。-- 在长流水线中提前查看中间结果
select * from big_table where dt = '2026-01-01';
peekResult(select count(*) as cnt from @last);
-- 上面的结果会立即输出到前端/日志,不影响后续执行
select * from @last where amount > 100;
TaskLog,最多输出 1000 行。JobStatus,结果会作为 currentData 推送到前端,最多 1000 行。logger.info 输出到控制台。peekResult 不会消费或改变堆栈——它只是”偷看”一眼,不影响后续语句的数据流。sybn sql 提供了一组列操作命令,用于在前一条 sql 的结果集上增删改列。这些命令都是后置命令,必须跟在一条 select 语句之后,用分号 ; 分隔。
五个命令的职责:
| 命令 | 作用 | 是否改变行数 |
|---|---|---|
alignColumns |
对齐/筛选列 | 否 |
appendColumns |
在结尾追加新列 | 否 |
prependColumns |
在开头插入新列 | 否 |
removeColumns |
删除指定列(可按空值条件删) | 否 |
splitColumns |
将一列按分隔符拆成多列 | 否 |
共同特点:都不改变行数,只改变列结构;新列的表达式可以引用同行的已有列。
select ...; alignColumns([字段1, 字段2 ...])
-- 不传参数:union 后两行列数不一致,alignColumns 补齐缺失列
select 1 as a, 2 as b
union select 11 as a, 33 as c;
alignColumns();
-- 结果:两行都有 a/b/c 三列,b 行 2 的 c 为 null,c 行 33 的 b 为 null
-- 传参数:仅保留 b、c 两列,丢弃 a 列
select 1 as a, 2 as b
union select 11 as a, 33 as c;
alignColumns(b, c);
-- 结果:两行都只有 b、c 两列
select ...; appendColumns(表达式1 as 字段1, 表达式2 as 字段2, ...) [on concurrent=N]
表达式 可以引用同行的已有列(包括前面刚追加的列)。on concurrent=N 可选,对结果集的每一行并发执行表达式,N 为并发数。-- 基础用法:追加 b、c、d 三列,d 引用了刚追加的 b 和 c
select 1 as a;
appendColumns(2 as b, a+1 as c, b+c as d);
-- 结果:{a:1, b:2, c:2, d:4},列顺序为 a,b,c,d
-- 并发:对 5 行数据并发追加 name 列
select * from [{ip:'169.254.11.1'},{ip:'169.254.11.2'},{ip:'169.254.11.3'},{ip:'169.254.11.4'},{ip:'169.254.11.5'}];
appendColumns(1 as name) on concurrent=5;
-- 结果:每行追加 name:1,列顺序为 ip,name
select ...; prependColumns(表达式1 as 字段1, 表达式2 as 字段2, ...) [on concurrent=N]
appendColumns 完全一致,唯一区别是新列被插入到列列表的开头。-- 新列 b、c、d 插入到 a 之前
select 1 as a;
prependColumns(2 as b, a+1 as c, b+c as d);
-- 结果:{b:2, c:2, d:4, a:1},列顺序为 b,c,d,a
-- 并发:对 5 行数据并发在开头插入 name 列
select * from [{ip:'169.254.11.1'},{ip:'169.254.11.2'},{ip:'169.254.11.3'},{ip:'169.254.11.4'},{ip:'169.254.11.5'}];
prependColumns(1 as name) on concurrent=5;
-- 结果:每行在开头插入 name:1,列顺序为 name,ip
select ...; removeColumns(字段1, 字段2, ...) [on all_empty=模式]
on all_empty 时,直接删除指定列。on all_empty=模式 时,仅当该列在所有行中都为”空”时才删除,否则保留。模式可选值:| 模式 | 判定”空”的规则 |
|---|---|
auto(默认) |
仅空字符串 '' 视为空;空格、0、null 都不视为空 |
blank |
trim 后为空字符串视为空(含 ''、' '、'\t') |
zero |
数值 0 或 0.0 视为空(字符串 '0' 不视为空) |
zeroString |
在 zero 基础上,字符串 '0'(含 ' 0 ')也视为空 |
(v1, v2, ...) |
自定义空值列表,如 (null, '', '0', 'N/A') |
-- 基础用法:直接删除 a 列
select 1 as a, 2 as b;
removeColumns(a);
-- 结果:{b:2}
-- auto 模式:只有空字符串被删除,空格和 0 保留
select '' as a, ' ' as b, 0 as c;
removeColumns(a, b, c) on all_empty=auto;
-- 结果:{b:' ', c:0}(a 被删除)
-- blank 模式:trim 后为空的列都被删除
select '' as a, ' ' as b, '\t' as c;
removeColumns(a, b, c) on all_empty=blank;
-- 结果:{}(三列都被删除)
-- zero 模式:数值 0 被删除,字符串 '0' 保留
select 0 as a, 0.0 as b, '0' as c;
removeColumns(a, b, c) on all_empty=zero;
-- 结果:{c:'0'}(a、b 被删除)
-- zeroString 模式:字符串 '0' 也被删除
select '0' as a, ' 0 ' as b, 'abc' as c;
removeColumns(a, b, c) on all_empty=zeroString;
-- 结果:{c:'abc'}(a、b 被删除)
-- 自定义空值列表
select null as a, '' as b, '0' as c, 'N/A' as d, 'other' as e;
removeColumns(a, b, c, d, e) on all_empty=(null, '', '0', 'N/A');
-- 结果:{e:'other'}(前四列被删除)
select ...; splitColumns(字段, [分隔符, [列名前缀]])
字段:要拆分的列名。分隔符:可选,默认为逗号 ,。列名前缀:可选,拆分后的新列名为 前缀 + 拆分值,默认无前缀(直接用拆分值作为列名)。-- 默认逗号分隔:把 b 列按逗号拆成 x、y、z 三列
select 1 as a, 'x,y,z' as b;
splitColumns(b);
-- 结果:{a:1, x:'x', y:'y', z:'z'}
-- 自定义分隔符和前缀:按空格拆分,新列名加 f_ 前缀
select 1 as a, 'x y z' as b;
splitColumns(b, ' ', 'f_');
-- 结果:{a:1, f_x:'x', f_y:'y', f_z:'z'}
; 分隔。select ...; appendColumns(...); removeColumns(...);。appendColumns 和 prependColumns 的表达式可以引用前面刚追加的列(如 b+c as d 中的 b 和 c 是同一条命令追加的)。removeColumns 的 on all_empty 模式是全表判定——只有该列在所有行中都为空才会被删除。builderInsert(...) 用于将前一条 sql 的结果集转换为标准的 INSERT INTO table(字段...) VALUES (值...) 语句。
常用于:把查询结果导出为可执行的 SQL 脚本,或者在不支持跨库直连的场景下,先生成 INSERT 语句再在目标库执行。
select ...; builderInsert([表名或字段列表])
table。中文等非标准字段名会自动用反引号
`包裹。
-- 不传参数:所有字段作为列,表名占位为 table
select * from [{a:'tab1', b:1, c:null}];
builderInsert();
-- 输出:INSERT INTO table(a,b,c) VALUES ('tab1',1,NULL);
-- 传一个字段:用 a 字段的值作为表名
select * from [{a:'tab1', b:1, c:null}];
builderInsert(a);
-- 输出:INSERT INTO table(a) VALUES ('tab1');
-- 中文字段名自动加反引号
select * from [{'大':'tab1', b:1, c:null}];
builderInsert();
-- 输出:INSERT INTO table(`大`,b,c) VALUES ('tab1',1,NULL);
-- 指定中文字段名
select * from [{'大':'tab1', b:1, c:null}];
builderInsert(大);
-- 输出:INSERT INTO table(`大`) VALUES ('tab1');
builderInsert 生成的是 SQL 文本,不是执行 INSERT,需要把生成的语句拿到目标库执行。table,需要自行替换为实际表名(或用第一个字段值作为表名)。NULL,字符串会被单引号包裹。sybn sql 是基于 SQL-92 扩展的多语句流水线查询语言。它在标准 SQL 基础上增加了三类能力:
; 连接,前一条的结果自动传递给后一条。本文是 sybn sql 的执行模型总览,适合作为 nl2sql(自然语言转 SQL)应用的命令选择参考。
sybn sql 用分号 ; 连接多条语句,按顺序执行:
select * from users where age > 18;
appendColumns(now() as query_time);
removeColumns(password);
执行流程:
select 查询用户表,结果压入堆栈。appendColumns 从堆栈取出结果,追加 query_time 列,新结果压栈。removeColumns 从堆栈取出结果,删除 password 列,新结果压栈。流水线只有最后一个语句的结果会返回,且多语句之间必须有承接关系——后一条语句必须能消费前一条压栈的结果。每条命令对堆栈有明确的「消费量 / 生产量」(见 1.2 节分类):
| 写法 | 是否合法 | 说明 |
|---|---|---|
select ...; appendColumns(...) |
✅ | convert 消费 1 个 source 结果 |
select ...; removeColumns(...); pivot(...) |
✅ | 多个 convert 串联 |
select ...; execBatch(子sql) on ... |
✅ | execBatch 消费 1 个 source 作为输入列表 |
select A; select B; left join ... |
✅ | join 消费 2 个 source,是「两个 select」唯一合法的用法 |
select 1; select 2 |
❌ | 两个独立 select 串联,无命令消费,会报错 |
select ...; select ... from @sybn_last_select@table |
⚠️ | 变量承接在此环境可能失败 |
select ...; clearQueue(); select ... |
✅ | clearQueue 清栈后可重新 select |
多步骤查询的正确写法:用一条 select + 子查询 + 转换命令组合。如果需要合并两个独立查询结果,用 join(而不是串联两个 select)。如果确实需要分步,用 execBatch 或 cacheResult 承接。
每条语句执行后,结果会被压入一个 stackData 堆栈:
语句1: select ... → 压栈 [结果1]
语句2: appendColumns → 弹出[结果1], 处理后压栈 [结果2]
语句3: removeColumns → 弹出[结果2], 处理后压栈 [结果3]
↑ 最终返回
return 命令可以指定返回堆栈中的任意一层。clearQueue 命令可以清空堆栈。每条命令对 stackData 堆栈有明确的「消费量 / 生产量」,这是判断能否承接的依据(对应源码 SySqlCommandConverExecutor.getConsumed()):
| 分类 | 消费栈顶 | 压栈 | 命令 | 注入变量 |
|---|---|---|---|---|
| source(产生数据) | 0 | 1 | select、union all/union、intersect/except、with as、execBatch(子sql)、dynamicSourceQuery、cacheResult |
@sybn_last_select@table |
| convert(转换栈顶) | 1 | 1 | appendColumns、prependColumns、removeColumns、alignColumns、splitColumns、builderInsert、pivot、unwind、unnest、fill |
@sybn_last_conver@table |
| multi-input(多输入转换) | 2 | 1 | INNER/LEFT/RIGHT/FULL JOIN、lookup |
@sybn_last_join@table |
| stack-control(清栈) | N 或全部 | 0 | clearQueue([N]) |
— |
| control-flow(控制流) | 0 | 0 | return、if exists、explain |
— |
| auxiliary(辅助) | 0 | 0 | set @var、show tables/databases/functions、desc、show value stat |
— |
| peek(旁路输出) | 0 | 0 | peekResult |
— |
消费量来自源码:
ConvertSySqlJoin.getConsumed()=2、SySqlExecBatchQueryExecutor.getConsumed()=1、SySqlClearQueueQueryExecutor.getConsumed()=Integer.MAX_VALUE,convert 类命令通过SySqlCommandConverExecutor接口默认消费 1 个。
核心模式:用 () 包裹一个多语句流水线,外层可以接标准 SQL 操作(where / group by / order by / having / limit 等),把流水线结果当作一张表来用。
/* 把用户表和订单 join 后, 外层再按城市分组聚合 */
select city, sum(total) as city_total
from (
/* 第1步: 左表 */
select id, name, city from users where status = 1;
/* 第2步: 右表 */
select user_id, sum(amount) as total from orders group by user_id;
/* 第3步: join 消费栈顶2个, 输出1个 */
left join select(name, city, total) on left.id = right.user_id
) where total > 100 /* 外层过滤 */
group by city /* 外层分组 */
order by city_total desc /* 外层排序 */
limit 10;
关键规则:
() 内部是独立流水线,有自己的堆栈,内部多语句必须有承接关系(source → convert/multi-input)。() 内部最终返回栈顶,作为外层 from 的数据源。where / group by / having / order by / limit / join 等。() 内部两个独立 select 串联会报错(select 1; select 2 无承接),必须用 join 或转换命令承接。适用场景:多步骤处理(join/转换)后还需要用标准 SQL 做聚合、过滤、排序时,用 () 包裹流水线,外层写标准 SQL。这比把所有逻辑塞进流水线更清晰。
嵌套层级:() 可以多层嵌套,每层都是独立流水线:
select * from (
select * from (
select ...;
select ...;
join ...
) where ... /* 内层流水线 + 外层过滤 */
group by ...
) where ... /* 外层再过滤 */
order by ...
sybn sql 的命令分为 7 大类,nl2sql 时根据意图选择。分类按功能划分;按流水线行为(source/convert/multi-input)划分见 1.3 节。
标准 SQL 查询,产生新数据压栈。from 子句支持多种数据源(库表 + 字面量)。
标准库表查询:
| 命令 | 语法 | 用途 |
|---|---|---|
select |
select ... from 库.表 where ... |
基础查询 |
union / union all |
select ... union all select ... |
联合(去重/不去重) |
intersect / except |
select ... intersect select ... |
交集/差集 |
with as |
with t as (select ...) select * from t |
CTE 临时表 |
内联字面量数据源(无需查库,数据写在 SQL 里,源码 TableLoader.loadList()):
| 数据源 | 语法 | 示例 |
|---|---|---|
| JSON 数组 | from [{...},{...}] |
select * from [{a:1,b:1},{a:2,b:2}] |
| 引号 JSON | from "[{...}]" |
select * from "[{a:1},{a:2}]" |
| CSV 文本块 | from csv(三反引号包裹的CSV文本) |
select * from csv(三反引号 name,age 换行 张三,20 三反引号)` |
| TSV 文本块 | from tsv(三反引号包裹的TSV文本) |
select * from tsv(三反引号 name\tage 换行 张三\t20 三反引号)` |
| VALUES | from (VALUES (...),(...)) AS t(列) |
from (VALUES ("v1"),("v2")) AS t(code) |
| factory | from factory({engine:'...',...}) |
factory({engine:'timeScope', key:'date', timeScope:'2026-01-01~2026-01-07'}) |
| generate_series | from generate_series(1, 10) as a |
生成 1-10 整数序列 |
CSV/TSV 用三反引号文本块包裹,第一行表头(列名),其后数据行。详见 from factory 和 web-sql part2。
跨轮次对话数据合并:把上一轮查询结果手动复制为 JSON/CSV 字面量嵌入当前 SQL,用 join 或 union 合并(详见 from-factory 文档)。
/* 本轮数据 union 上一轮结果(JSON字面量) */
select '本轮' as round, cinema_name, amount from orders where date = curdate()
union all
select '上轮' as round, cinema_name, amount
from [{cinema_name:"影城A", amount:1200}, {cinema_name:"影城B", amount:980}]
作用于前一条语句的结果,消费栈顶 1 个,转换后压栈 1 个。不改变行数(除 unwind/unnest)。
| 命令 | 用途 | 改变行数 |
|---|---|---|
appendColumns |
结尾追加列 | 否 |
prependColumns |
开头插入列 | 否 |
removeColumns |
删除列(可按空值条件) | 否 |
alignColumns |
对齐/筛选列 | 否 |
splitColumns |
拆分列为多列 | 否 |
builderInsert |
结果转 INSERT 语句 | 否 |
pivot |
行转列 | 否 |
unwind |
数组/Map 拆成多行(可多字段) | 是 |
unnest |
数组/Map 拆成多行(自定义列名) | 是 |
fill |
按维度补齐缺失数据行 | 是 |
消费栈顶 2 个(左表 + 右表),join 后输出 1 个结果压栈。前面必须有 2 条 source 语句。源码:ConvertSySqlJoin.getConsumed()=2。
| 命令 | 语法 | 用途 |
|---|---|---|
INNER JOIN |
INNER JOIN SELECT(字段) ON left.x = right.y |
内连接 |
LEFT JOIN |
LEFT JOIN SELECT(字段) ON left.x = right.y |
左连接 |
RIGHT JOIN |
RIGHT JOIN SELECT(字段) ON left.x = right.y |
右连接 |
FULL JOIN |
FULL JOIN SELECT(字段) ON left.x = right.y |
全连接 |
lookup |
lookup SELECT(字段) ON left.x = right.y |
mongo 风格 join |
示例:
/* 将用户表和订单表 join 起来 */
select * from (
select id, name from users;
select user_id, sum(amount) as total from orders group by user_id;
left join select(name, total) on left.id = right.user_id
) order by total desc
语法说明:
SELECT(...):指定返回字段,支持 as 别名和 udf 函数;左右表同名字段优先返回左表值ON:支持 left.x = right.y、x = y、using(字段);多条件用 AND 连接join 需要2张前表,而不是N张.可能是前面的某行代码少写了分号.作用于子查询结果,语法比嵌套 select 更简洁。
| 命令 | 语法 | 用途 |
|---|---|---|
where |
from (...) where 条件 |
过滤 |
order by |
from (...) order by 字段 desc |
排序 |
limit |
from (...) limit N |
分页 |
三者可组合:
from (...) where 条件 order by 字段 limit N
执行一个子 SQL,把结果压栈。常用于批量、动态数据源、缓存。
| 命令 | 用途 | 流水线行为 |
|---|---|---|
execBatch |
批量执行子任务,支持并发/忽略错误 | 消费栈顶1个作为 @item 列表,压栈1个 |
dynamicSourceQuery |
动态切换数据源执行子查询 | source,压栈1个 |
cacheResult |
缓存子查询结果,跨语句复用 | source,压栈1个 |
peekResult |
提前输出中间结果到前端/日志 | peek,旁路不压栈 |
控制流水线的执行流程。不产生数据。
| 命令 | 语法 | 用途 | 流水线行为 |
|---|---|---|---|
return |
return [N / select ...] |
提前返回并终止后续 | 不消费栈不压栈 |
if exists |
if exists (select ...) then ... |
条件执行 | 不消费栈不压栈 |
clearQueue |
clearQueue([N]) |
清理堆栈 | 消费 N 个或全部 |
explain |
explain select ... |
查看查询计划(仅 JDBC) | 不消费栈不压栈 |
不产生数据,用于变量、表结构查看。
| 命令 | 用途 |
|---|---|
set @var := 值 |
设置变量 |
use 数据库 |
切换数据库 |
show tables / show databases |
查看表/库 |
show create table / desc 表 |
查看表结构 |
show functions |
查看所有函数 |
show value stat |
查看字段统计特征 |
sybn sql 支持 5 种变量,是流水线语句间数据传递的核心:
这些变量由系统自动注入,引用堆栈中的历史结果:
| 变量 | 含义 | 注入时机 |
|---|---|---|
@sybn_last_select@table |
最后一个 select 的结果 | 每次 select 执行后 |
@sybn_last_conver@table |
最后一个转换命令的结果 | appendColumns/removeColumns 等执行后 |
@sybn_last_join@table |
最后一个 join 的结果 | join 执行后 |
select * from users;
select count(*) as cnt from @sybn_last_select@table;
/* @sybn_last_select@table 被替换为上一条 select 的结果 */
set @t := str_to_date('2026-01-01', '%Y-%m-%d');
select * from orders where create_time > @t;
:= 和 = 赋值。set @types@list := (select type from config where enabled = 1);
select * from orders where type in (@types@list);
@var@list 用于 in 查询传值。select * from users where name = ${name} and age > ${age}
${var} 由 sql 外部传入,可替换任意位置。#{var} 同样支持(预编译占位)。select * from ${${env}_table}
$${var} 引用全局变量,可替换任意位置。┌─────────────────────────────────────────────────────────┐
│ 输入: 多语句 SQL(分号分隔) │
└──────────────────────┬──────────────────────────────────┘
↓
┌──────────────────────┐
│ 语句1: select ... │ → 压栈 → 注入 @sybn_last_select@table
└──────────┬───────────┘ (source: 消费0 压栈1)
↓
┌──────────────────────┐
│ 语句2: 转换命令 │ → 弹栈1个处理 → 压栈 → 注入 @sybn_last_conver@table
│ (appendColumns等) │ (convert: 消费1 压栈1)
└──────────┬───────────┘
↓
┌──────────────────────┐
│ 语句3: join │ → 弹栈2个(左+右) → join → 压栈 → 注入 @sybn_last_join@table
│ (left join 等) │ (multi-input: 消费2 压栈1)
└──────────┬───────────┘
↓
┌──────────────────────┐
│ 语句4: 子查询命令 │ → 执行子SQL → 压栈
│ (execBatch等) │ (source: 消费0/1 压栈1)
└──────────┬───────────┘
↓
┌──────────────────────┐
│ 语句N: return │ → 指定返回栈中某层
└──────────┬───────────┘ (control-flow: 不消费不压栈)
↓
返回最终结果
自然语言转 SQL 时,按用户意图映射命令:
| 用户意图 | 推荐命令 | 示例自然语言 |
|---|---|---|
| 查询数据 | select |
“查出所有成年用户” |
| 合并两个查询结果 | INNER/LEFT/RIGHT/FULL JOIN |
“把用户表和订单表按用户id关联” |
| mongo 数据关联 | lookup |
“把 mongo 用户表和订单表关联” |
| 追加计算列 | appendColumns |
“加一列显示年龄是否大于60” |
| 删除敏感列 | removeColumns |
“去掉密码和手机号列” |
| 行转列 | pivot |
“把按月份的行数据转成列” |
| 列转行 | unwind / unnest |
“把 tags 数组拆成多行” |
| 补齐缺失日期 | fill |
“把缺少的日期补上空行” |
| 批量执行 | execBatch |
“对每个用户分别查询订单” |
| 跨库查询 | dynamicSourceQuery |
“从另一个 mysql 库查数据” |
| 缓存慢查询 | cacheResult |
“这个结果缓存一下复用” |
| 条件执行 | if exists |
“如果表存在就查询” |
| 提前返回 | return |
“拿到结果就停止” |
| 数据脱敏 | masking() 函数 |
“手机号中间4位打星号” |
sybn-core/src/main/java/cn/sybn/util/io/db/common/helper/loader/TableLoader.java — from 字面量数据源加载(JSON/CSV/TSV/VALUES/factory/generate_series)clearQueue(...) 用于清理查询堆栈中的中间结果。
sybn sql 用分号 ; 连接多条语句时,每条语句的结果会压入一个堆栈,下一条语句可以引用堆栈中的数据。clearQueue 会清空堆栈,使后续语句无法再引用之前的结果。
clearQueue([清理数量])
-- 清空整个堆栈
select 'tab1' as t;
select 'tab2' as t;
select 'tab3' as t;
clearQueue();
-- 堆栈被清空,后续语句无法引用上述结果
通常用于:
unnest 和 unwind 都是列转行命令,用于将数组或 Map 类型的字段拆分成多行,其余字段复制到每一行。
| 命令 | 语法风格 | 来源 |
|---|---|---|
unwind |
unwind(字段1, 字段2, ...) |
类似 MongoDB 的 unwind |
unnest |
UNNEST(字段) as t(字段1, 字段2) |
类似 Flink/PG 的 unnest |
两者的核心区别:
unwind 可以同时拆分多个字段,做笛卡尔积。unnest 一次拆分一个字段,但可以自定义输出列名。select unwind(字段1, 字段2, ...) from ...
-- 或作为后置命令
select ...; unwind(字段1, 字段2, ...)
-- 把 b 字段(数组)拆成两行,a 字段复制到每一行
select * from [{'a':1, 'b':['b1','b2']}];
-- 结果:[{a:1, b:'b1'}, {a:1, b:'b2'}]
-- 也可以在 select 中直接用 unwind
select unwind(b) b from [{'a':1, 'b':['b1','b2']}];
-- 结果:[{a:1, b:'b1'}, {a:1, b:'b2'}]
-- 同时拆分 b 和 c,做笛卡尔积
select * from [{'a':1, 'b':['b1','b2'], 'c':['c1','c2','c3']}];
unwind(b, c);
-- 结果:6 行(2×3 笛卡尔积)
-- [{a:1,b:'b1',c:'c1'}, {a:1,b:'b1',c:'c2'}, {a:1,b:'b1',c:'c3'},
-- {a:1,b:'b2',c:'c1'}, {a:1,b:'b2',c:'c2'}, {a:1,b:'b2',c:'c3'}]
-- 用 toList 把字符串转成数组再 unwind
select unwind(toList(b)) b from [{'a':1, 'b':'b1,b2'}];
-- 结果:[{a:1, b:'b1'}, {a:1, b:'b2'}]
-- 把 Map 拆成 key/value 两列
select * from [{'a':1, 'b':{'x':1, 'y':2}}];
unwind(b);
-- 结果:[{a:1, b_key:'x', b_value:1}, {a:1, b_key:'y', b_value:2}]
select ... from UNNEST(字段) as t(列名1, 列名2)
字段:要拆分的 Map 或数组字段名(可用反引号包裹)。t:别名(可省略)。列名1, 列名2:拆分后的输出列名。
-- 拆分 Map,输出 mapKey 和 mapValue 两列
select * from UNNEST(pageInfo) as t(mapKey, mapValue);
-- 格式化后:unnest(pageInfo) as t(`mapKey` AS mapKey, `mapValue` AS mapValue)
-- 不加反引号也可以
select * from UNNEST(pageInfo) as t(mapKey, mapValue);
-- 格式化后:unnest(pageInfo) as t(mapKey, mapValue)
unwind 可以同时拆多个字段做笛卡尔积,unnest 一次只拆一个。unwind 拆 Map 时输出列名固定为 字段_key 和 字段_value;unnest 可以自定义列名。unwind 可以在 select 字段列表中使用(select unwind(b) b),也可以作为后置命令。unwind 会先用 toList 尝试转换(如把逗号字符串转数组)。