简介:面向使用 Kettle 进行 ETL 开发与数据抽取的初中级工程师,这份文档围绕“设置循环变量”展开,适合需要让作业按多张表循环执行、避免重复配置的场景。压缩包共 1 个文件,为 docx 文档,大小约 282KB,便于直接查阅与本地保存。已有 2546 人学习,说明该主题在实际数据集成工作中具有较高关注度。内容结合 User_Tables 存放表名的示例,梳理从表名取值、变量传递到循环控制的实现思路,涉及 Trans 脚本、获取表数量、循环控制器、获取表行数与计数器累加等步骤,并解释变量一次只能接收一个值时的处理方式。读者可据此理解 Kettle 中表名替换与循环作业的落地方法,核对关键步骤配置,减少在批量抽取相同结构表时的重复操作,提升数据处理效率与灵活性。
1. Kettle 设置循环变量的真实门槛:一张 User_Tables 表背后的三百次重复执行
Kettle 设置循环变量这件事,我第一次做的时候在一张 User_Tables 表上卡了整整两天。表里存着几百个用户分表的表名,需求简单到不能再简单:对每张表跑一句SELECT COUNT(1) FROM ${TABLENAME},把结果落进一张 tmp 开头的统计表。写一个转换不行,因为转换只执行一次,表名替换完就定死了;写几百个转换更不行,改一次统计口径要改几百遍。真正卡住人的不是循环语法,而是两件看不见的事:变量到底在哪个作用域里活着,SQL 里的${TABLENAME}到底在哪一刻被替换。这两点吃透,作业循环就是一小时的事;吃不透,就会一直看到表名被原样当字符串扔给数据库,或者循环老老实实只跑第一轮。下面按我实际搭的顺序讲,从变量机制到作业循环,再到新版里更省事的做法和几个必踩的坑。
2. 变量在 Kettle 里怎么流动:作用域、替换时机与 ${TABLENAME} 的生效条件
在动手拖步骤之前,得先把变量这件事的底层逻辑捋清楚。Kettle 的变量不是编程语言里的局部变量,它本质上是一个挂在 JVM 上的键值表,各层作业和转换共用同一份空间,靠作用域标记决定谁写进去、谁读得到、什么时候失效。很多人第一次做循环失败,不是步骤连错,而是变量的写入位置和读取位置根本不在同一个可见层级里。这一章把作用域、替换时机、生效条件三件事说透,后面两章的实现才有地基。
2.1 三个作用域决定变量能活多久
「设置变量」(Set variables)步骤的配置面板里有一个 Variable scope 下拉框,常见的有三个选项,它决定了这个变量写进哪一层、能被谁看见:
| 作用域选项 | 写入层级 | 可见范围 | 生命周期 |
|---|---|---|---|
| Valid in the whole JVM | JVM 全局 | 当前 JVM 内所有作业和转换 | 进程结束才消失 |
| Valid in the parent job | 父作业 | 当前转换的父作业及其子项 | 父作业结束时失效 |
| Valid in the root job | 根作业 | 最外层作业及其全部子孙 | 根作业结束时失效 |
我一般默认选 Valid in the parent job。原因是整个 JVM 级太「脏」——同一个 Kettle 进程里可能同时跑多个调度任务,都用 TABLENAME 这种通用变量名,互相覆盖是迟早的事,排查起来非常难受。父作业级刚好够用:循环体里的子转换每次执行都能读到父作业设置的 TABLENAME 和计数器 I,父作业一结束变量自然回收。
还有一个容易被忽略的点:Kettle 的变量名是区分大小写的。你在「设置变量」里写的是TABLENAME,SQL 里就得老老实实写${TABLENAME},写成${TableName}或者${tablename}读不到值,Kettle 不会报错,只会把字面量原样保留。这种不报错的失败,比报错难查十倍。
2.2 SQL 的变量替换只发生在步骤启动那一刻
这是整篇最关键的一条原理。表输入(Table input)步骤在初始化时,会拿当前变量值把 SQL 里的${VAR}全部替换一遍,然后把这条替换后的 SQL 交给数据库执行。替换完成之后,这条 SQL 在本次执行里就固定了,后面哪怕有步骤把变量改成别的值,表输入也不会重新解析一遍。
由此能推出三个直接结论:
第一,在同一个转换里做「设置变量 → 表输入」,指望表输入用新值去查,是不成立的。数据流是并行的,步骤的启动顺序和你在画布上的连线顺序并不完全对应,变量替换更不会等你。
第二,作业里每执行一次转换,都是一次全新的转换实例,都会重新初始化、重新替换。这就是作业级循环能成立的根本原因,也是老版本 Kettle 做循环绕不开作业层的原因。
第三,子转换被「对每一行执行」的方式反复调用时,每一行都是一次独立初始化,同样会重新替换。这是新版里更省事方案的基础,第 4 章会展开。
提示:如果你非要在转换内部做多次查询,还有一条路是用「执行 SQL 脚本」步骤配合字段传参,但它适合 DDL 和简单语句,不适合需要结果集的统计查询,别在这上面浪费时间。
2.3 三个条件同时满足,${TABLENAME} 才会生效
变量替换失败的原因基本就三种,挨个对一遍就能定位:
一是变量根本没被定义。表输入拿到一个未定义的${TABLENAME},不会报「变量不存在」,而是把这个字符串原封不动发给数据库,于是你看到的是语法错误或者「表不存在」。第一次遇到这种报错,人容易往数据库权限、连接串上想,其实问题在 Kettle 这边。
二是写法不对。只认${TABLENAME}这种美元加大括号的形式,$TABLENAME、{TABLENAME}、${ TABLENAME }带空格都不行。另外 SQL 里出现${这种字符本身也可能被误判,做字符串拼接时要留意。
三是没勾选替换开关。表输入步骤的选项区有一个「替换 SQL 语句里的变量」(Replace variables in script)复选框,默认不勾。这个设计挺反直觉,很多人第一次用的都是新版本,觉得变量应该自动生效,结果查了半天。同样带这个开关的还有「执行 SQL 脚本」和字段选择里的一些表达式,见到变量就先找这个勾。
2.4 给变量赋值的两种手段与选型
Kettle 的作业项里没有「设置变量」这个东西,赋值动作必须发生在转换里,这一点先记住。转换里常用的赋值方式有两种:
「设置变量」步骤把输入流里的某个字段映射成变量,一次处理一行,适合把查询结果直接变成变量,配置就三项:字段名、变量名、作用域。它的问题是只能做直接映射,做不了计算。
「修改 JavaScript 值」(Modified Java Script Value)适合做计算和条件判断,用getVariable读、setVariable写。计数器累加、表名拼接这类活用它最顺手:
// 步骤:Modified Java Script Value —— 读计数器、算下一个序号、写回变量 // 上游需要一个输入行(用「生成记录」产生一行即可) var cur = parseInt(getVariable("I", "1"), 10); // 读变量,读不到默认 1 var max = parseInt(getVariable("MAX_LOOP", "0"), 10); // 读总表数 if (cur <= max) { // 第三个参数是作用域:r = 根作业,p = 父作业,s = 系统级 // 不同版本对作用域字符串的接受略有差异,报错就换成 p 再试 setVariable("I", String(cur + 1), "r"); true; // 返回 true 输出一行,供下游步骤接收 } else { setVariable("LOOP_DONE", "Y", "r"); false; // 返回 false 则这一行不往下走 }这段代码里有三个参数值得说清楚。getVariable的第二个参数是默认值,强烈建议每次读变量都带上,否则变量未定义时拿到的是空串,parseInt一算就是 NaN,整个循环的终止条件直接失效。setVariable的第三个参数是作用域字符串,写r表示根作业,p表示父作业,我一般和前面「设置变量」步骤的作用域保持一致,别一半写父作业一半写根作业,那样会出现「这个转换里读得到、那个转换里读不到」的怪现象。最后的返回值决定这一行是继续往下流还是被丢弃,做条件分支时很实用。
注意:JavaScript 步骤里读变量还有
parent_job.getVariable(...)这种对象写法,能用但不推荐,换版本或者换运行方式(比如在 IDE 里调试)时容易拿不到,getVariable全局函数更稳。
3. 作业级循环的完整搭建:从取表名到计数器累加的五步
如果 Kettle 的版本比较老,或者团队里统一用的是作业调度的套路,那就走作业级循环这条路。这条路的思路和写 for 循环完全一样:先拿到总数当上限,用一个变量 i 当游标,循环体里按 i 取第 i 个表名、执行 SQL,末尾把 i 加一,再回到条件判断。区别只在于,for 循环的三段代码在 Kettle 里被拆成了五个转换加一个作业,中间靠结果集和变量传递数据。
3.1 先把文件清单定下来
我习惯在动手之前把文件划分好,一次建对,后面调试省事。这套结构对应着你手上那份资源的实现思路,只是把步骤拆得更明确:
| 文件名 | 类型 | 职责 | 关键步骤 |
|---|---|---|---|
| 01_取表名.ktr | 转换 | 从 User_Tables 取出待处理表名 | 表输入 → 复制记录到结果 |
| 02_初始化变量.ktr | 转换 | 算出总数、初始化计数器 | 从结果获取记录 → 分组 → 设置变量 |
| 03_设置当前表.ktr | 转换 | 按序号取出当前表名 | 从结果获取记录 → 添加序列 → 过滤 → 设置变量 |
| 04_统计行数.ktr | 转换 | 执行 count 并落表 | 表输入(带变量)→ 表输出 |
| 05_计数器累加.ktr | 转换 | i 加一并写回 | 生成记录 → JavaScript → 空操作 |
| job_loop.kjb | 作业 | 串起循环与判断 | 转换项 + 简单评估 |
拆成五个小转换而不是一个大转换,看起来很啰嗦,实际是血泪经验:单个转换里步骤越多,出错时越难定位,而且作业层面可以给每个转换单独设日志级别,哪一步慢了、哪一步失败了,日志里一眼就看出来。
3.2 第一步:把表名清单放进结果集
01 这个转换只干一件事,把表名捞出来,塞进作业的结果集里。「复制记录到结果」(Copy rows to result)这个步骤名字起得直白,它的作用就是把上游的行缓存成作业级别的结果集,供后续的转换用「从结果获取记录」读回来。SQL 这么写:
-- 01_取表名.ktr 中的表输入 -- 过滤掉 temp 临时表和各用户的备份表,避免把不该统计的表也卷进来 SELECT Table_Name FROM User_Tables WHERE Table_Name NOT LIKE 'tmp_%' AND Table_Name NOT LIKE '%_BAK' ORDER BY Table_Name;有两个细节。ORDER BY不是为了好看,是为了让每次循环取表的顺序稳定,出问题时能按序号复盘到底卡在哪张表上。过滤条件一定要写,User_Tables 这种表往往混着系统表、临时表和废弃表,一张不通的表就能让整个循环中断。
提示:结果集在同一时刻只保留一份。这意味着你只能让 01 这个转换往结果集里写,其它转换一律不许再写,否则后面的「从结果获取记录」拿到的就是最后一次写入的内容,表名和序号全错位。
3.3 第二步:算出总数,初始化计数器
02 转换负责把上限和游标准备好。「从结果获取记录」(Get rows from result)会读回 01 写进去的全部行,接一个「分组」(Group by)步骤做计数,分组字段留空表示不分组的全表聚合,聚合字段填CNT,聚合类型选 Number of rows(部分汉化版本叫「行数」)。得到一行一列的总数之后,接「设置变量」步骤:
-- 02 转换里核对用的 SQL,可以直接在数据库端验证循环范围对不对 -- 循环跑之前先执行一次,看看 300 还是 298,数字对不上就是过滤条件写错了 SELECT COUNT(1) AS CNT FROM User_Tables WHERE Table_Name NOT LIKE 'tmp_%' AND Table_Name NOT LIKE '%_BAK';设置变量这一步要设两个:MAX_LOOP接分组出来的 CNT 字段,类型 Integer;I用一个「增加常量」(Add constants)步骤生成字段I = 1,再映射成同名变量。两个变量作用域都选父作业级。游标从 1 开始而不是 0,是因为后面取第 i 个表名靠的是序列号,1 起步和「添加序列」步骤的默认起始值对得上,少一次加减,少一个犯错点。
3.4 第三步:按序号取出当前表名
03 转换是循环体里的第一环,负责把「第 i 个表名」变成变量TABLENAME。结构是:从结果获取记录 → 添加序列 → 过滤记录 → 设置变量。
「添加序列」(Add sequence)给每行加一个自增序号,Value name 填seq,起始值 1,增长值 1,别用数据库序列,就走内存计数。接上「过滤记录」(Filter rows),条件写seq = ${I},注意这里${I}是变量替换,需要在过滤步骤对应位置启用变量替换,或者前置一个「获取变量」(Get variables)步骤把 I 读成字段再做字段比较,后者更稳,我一般用后者。
命中之后是一行当前表名,接「设置变量」把Table_Name映射成变量TABLENAME。
这一步是整个方案里性能最差的地方,得提前说清楚:每次循环,「从结果获取记录」都会把 01 那份完整结果集重新读一遍。300 张表的场景下,等于读了 300 次、总共 9 万行,内存和 CPU 都还扛得住。表数上千的时候这个开销就明显了,解法有两个:一是第 4 章的转换内循环方案,二是把表名分批,每批 100 个,批内循环、批间用作业再套一层。别等到跑了几千张表发现越来越慢才回头改。
3.5 第四步:执行统计 SQL 并落表
04 转换就是最终干活的地方,也是最容易翻车的地方——表输入的 SQL 里带变量,必须勾上「替换 SQL 语句里的变量」:
-- 04_统计行数.ktr 中的表输入,务必勾选「替换 SQL 语句里的变量」 -- 把表名本身也作为一列输出,方便落表后知道这一行统计的是哪张表 SELECT '${TABLENAME}' AS Table_Name, COUNT(1) AS CNT FROM ${TABLENAME};表名用${TABLENAME}直接拼进 FROM 子句,这是这份方案最核心的写法。这里有个必须接受的现实:表名不能做参数绑定,只能靠字符串替换,所以 TABLENAME 的来源必须是可信的内部表名清单,绝不能来自外部输入,否则就是给自己挖坑。落表用「表输出」写进tmp_table_stat,表结构至少三列:表名、行数、统计时间。加上统计时间这一列,后面做增量重跑时会感谢自己。
3.6 第五步:计数器累加与作业跳转
05 转换只做一件事:把 I 加一。「生成记录」(Generate rows)产一行空数据当输入,接前面 2.4 那段 JavaScript,把I加一写回父作业作用域,末尾接「空操作」(Dummy)收尾。
作业层面把六个节点串起来,顺序是:Start → 01 取表名 → 02 初始化变量 → 简单评估(判断 I 是否小于等于 MAX_LOOP)→ true 分支走 03 → 04 → 05,05 执行完再回到评估节点;false 分支直接走「成功」。
「简单评估」(Simple evaluation)这个作业项在作业的条件类里,把${I}和${MAX_LOOP}都按 Integer 类型比较,判断条件是 I 小于等于 MAX_LOOP 时走 true。手头的版本里如果没有这个作业项,替代做法是在 05 转换里判断cur > max时把转换故意打失败,作业里用「失败」跳线接成功分支——土,但能用。
这里有个玄学现象顺便解释掉:有人把 05 放到了 04 前面,结果 SQL 里用的表名永远是第一张。原因回到 2.2——04 每次执行都会重新替换变量,它替换时读的是当时变量里的值,你把累加放到前面,等于第一轮就把 I 加到 2 了,取表名那一步自然对不上。顺序这个东西在作业里是硬约束,不能靠感觉调。
4. 转换内的循环替代方案:对每一行执行子转换
上一章那套五个转换加一个作业的结构,在 Kettle 4.x 时代是标准答案。但只要版本过了 5,就有一个成本低得多的做法:主转换里读表名,用一个「转换」(Transformation)步骤勾上「对每一行执行」(Execute for each row),让子转换被反复调用。整条循环收进一个转换文件里,调试、改参数、移植都省一半事。这一章讲清楚它为什么成立、参数怎么配、什么时候不该用。
4.1 它为什么能绕开变量替换的时机问题
回顾 2.2 的结论:表输入的 SQL 只在该步骤初始化时替换一次。子转换被「对每一行执行」调用时,每一行都触发一次完整的子转换初始化——初始化连接、初始化所有步骤、替换所有 SQL、执行完、销毁。也就是说,变量替换跟着每一行走,天然解决了「替换一次就定死」的问题。
这个机制还有一个副产品:子转换里的步骤是干净启动的,不存在上一轮的行缓存、事务残留和计数器状态。作业级循环里偶尔会遇到「第二轮读到的还是上一轮结果集」这种脏状态问题,在子转换方案里基本不会出现,因为每次都是新的实例。
代价也很明确:每行启动一次子转换有固定开销,几百行无所谓,几十万行就要重新设计。这是选型的边界,不是缺陷。
4.2 主转换与子转换的分工
主转换(比如main_loop.ktr)只有三个步骤:表输入取出表名清单、转换步骤调用子转换、空操作收尾。表输入的 SQL 和第 3 章 3.2 那份一样,过滤条件照抄。
中间的「转换」步骤是重点,配置分两块:
第一块是基本信息。Transformation filename 选子转换文件child_count.ktr,勾选「对每一行执行」(Execute for each row),勾选「把结果复制到下一步」(Copy results to the next step),这样每行执行完的结果才能继续往下流。
第二块是参数映射。子转换需要先定义命名参数:打开子转换,在转换属性里加一个参数TABLENAME,默认值可以填空。回到主转换的「转换」步骤,在参数列表里把子转换的参数名TABLENAME和主转换的输入字段Table_Name对上。这一步配好之后,每一行的Table_Name值就会作为参数传给子转换,子转换里的${TABLENAME}就能拿到正确的值。
提示:不同版本里这个页签的叫法不一样,有的叫 Parameters,有的汉化成「参数」,也有的版本只能通过字段映射传值。如果你手上的版本压根没有这个页签,别硬折腾,直接回第 3 章用作业循环,那条路所有版本通吃。
子转换(child_count.ktr)里就三个步骤:表输入SELECT '${TABLENAME}' AS Table_Name, COUNT(1) AS CNT FROM ${TABLENAME},记得勾「替换 SQL 语句里的变量」;中间可以加个「字段选择」把类型规整一下;最后「表输出」写tmp_table_stat。命名参数的好处是,子转换对外只暴露一个TABLENAME,内部 SQL 怎么改都不影响主流程。
4.3 三个影响稳定性的参数
配置看起来简单,真正决定这套方案在生产上能不能扛住的,是下面三件事:
连接复用。子转换里用到的数据库连接,务必在连接配置里勾上连接池相关的选项,或者至少保证每次子转换启动时复用的是同一个连接对象而不是新建物理连接。几百行循环如果每行新建一次连接,数据库端的连接数会瞬间打满,表现是跑到一半开始报连接超时。
提交批大小。子转换里的表输出步骤有一个提交记录数(Commit size),默认 1000 对这个场景太大——每行只写一条统计结果,批大小设成 1 或者干脆关掉事务,让每条结果立刻落地。反过来,如果子转换里写的是大批量数据,才是调大批大小的场景。
串行与并发。这个步骤默认对每一行串行执行,一行跑完再跑下一行,这对统计场景是合适的,因为统计 SQL 本来就吃数据库 IO,并发只会互相拖慢。真要并发,得在作业层开多个作业副本,配合每份副本处理不同范围的表,那是另一个量级的复杂度,先别想。
4.4 两套方案怎么选
| 维度 | 作业级循环 | 转换内逐行执行 |
|---|---|---|
| 适用版本 | 4.x 及以后都行 | 5.x 及以后更顺 |
| 文件数量 | 5 个转换 + 1 个作业 | 1 个主转换 + 1 个子转换 |
| 变量替换 | 靠作业反复初始化转换 | 靠每行重新初始化子转换 |
| 调试难度 | 高,跳转和结果集要一起看 | 低,单个转换里就能断点 |
| 大批量性能 | 结果集反复读取,表多时变慢 | 固定开销大,行数多时变慢 |
| 断点续跑 | 容易加,作业层控制 | 需要在主转换的 SQL 里过滤 |
我的习惯是:表数量在几百以内、版本比较新,直接上转换内方案,一上午能跑通;表数量上千、或者需要严格的重跑和审计,用作业级方案,把断点和日志加厚。
5. 循环变量的五个翻车现场:现象、原因与排查顺序
这套东西跑通一次之后,剩下的时间基本都在跟各种奇怪的失败打交道。下面五个是我真实撞过、也见过别人撞的现场,按出现频率排,每条按现象、原因、解决写,方便你对照自己的日志往下查。
5.1 表名被当成字符串原样发给数据库
现象:转换失败,日志里的 SQL 长这样SELECT COUNT(1) FROM ${TABLENAME},数据库报语法错误或者表不存在。原因:替换开关没勾,或者变量名大小写和定义时不一致,也可能是变量压根没定义。解决:先打开表输入确认「替换 SQL 语句里的变量」已勾选;再在循环体转换的最前面加一个「写日志」步骤,把${TABLENAME}打出来看值对不对——如果日志里也是字面量,问题在变量定义侧,如果日志里是正常表名,问题就在勾选上。
5.2 循环只跑第一轮就结束
现象:结果表里只有第一张表的统计结果,作业显示成功。原因:MAX_LOOP是 0 或者空。最常见的来源是 02 转换里的分组计数没拿到数据——01 转换的「复制记录到结果」漏配了,或者 01 的过滤条件把表全过滤掉了。解决:单独执行 01,看输出行数;再单独执行 02,用「写日志」把MAX_LOOP打出来。这两个数字对不上,问题就锁定在结果集传递上。另外检查一下是不是有别的转换也在往结果集里写,覆盖了 01 的内容。
5.3 循环停不下来,一直跑
现象:作业永远不结束,日志里表名反复横跳。原因:计数器变量没写回,或者作用域写错了。典型场景是 05 转换里用「设置变量」步骤,作用域选了「当前转换」,出了这个转换变量就没了,下一轮评估读到的I还是初始值 1。解决:把作用域改成父作业级或根作业级,和读取方保持一致;在评估节点前加一步日志,把I和MAX_LOOP都打出来,两个值都不动就是写入失败,只有一个动就是比较逻辑写反了。保险起见,在作业里加一个最大循环次数的硬保护
本文还有配套的精品资源,点击获取