做数据接入的人,几乎都跟 KETTLE(PDI)打过交道。真正跑过几条生产作业后,你会发现最耗精力的往往不是复杂的多表关联、分组聚合,反而是 NULL 和空字符串这两个看起来人畜无害的东西。它们都是“空”,但一个是“未知”,一个是“长度为 0 的字符串”,语义完全不同。如果不提前把它们处理明白,轻则目标表多出一堆默认值,重则主键冲突直接让整条作业死掉。这篇内容就聚焦 KETTLE 里的 NULL 与空字符串案例,我会把常见的识别手段、处理方案、选型思路和踩坑记录都拆开讲清楚。不管你是刚接触 ETL 的新人,还是已经维护过几条生产作业的开发者,这套处理思路都可以直接抄作业。
1. 先搞清楚一件事:null和空字符串到底差在哪
1.1 两种“空”的本质区别与来源
很多初学者会下意识觉得 NULL 和空字符串是一回事,反正“都是没值”。但在数据库层面,这两者有着本质区别。
NULL 表示“该字段从未被赋值”。它不占字符串空间,不能用等号比较,只能通过 IS NULL 判断。你可以把它理解成“这个人根本没有填写报名表”。而空字符串 '' 是一个真实的、长度为 0 的字符串值,它占极小的存储空间,可以用= ''直接比较。同类比的话,空字符串更像是“交了一张什么都没写的报名表”。
为什么同一个业务字段会同时出现这两种“空”?根源基本在源系统。
- 数据库表结构设计不同:Oracle 有个经典行为,会把空字符串当作 NULL 处理,所以从 Oracle 导出来的数据往往只有 NULL,没有空字符串。而 MySQL、SQL Server、PostgreSQL 则是“空字符串是空字符串,NULL 是 NULL”,两者会同时存在。
- 导出工具行为不同:很多 CSV 导出工具在遇到空单元格时,有的写 NULL,有的写空字符串,甚至有的 Java 程序会把 null 直接转成文本
"null"四个字母写入文件。 - 应用代码习惯不同:有的开发喜欢用
isEmpty()之前先给字段赋空字符串,有的习惯保留 null,同一个字段在不同子系统中表现就可能完全不同。
所以在动手写转换之前,第一步永远是“确认源的语义”,而不是默认空值都是 NULL,或者默认空值都是空字符串。
1.2 不处理会引发哪些线上问题
我在实际项目里见过不止一次因为空值没处理好导致的线上事故,这里挑几个有代表性的。
第一个是唯一索引冲突。目标表某个字段上有唯一约束,如果这一列是空字符串,那么两条记录的空字符串是“同一个值”,会直接撞唯一索引。而 NULL 在多数数据库的唯一索引里是允许多个存在的。结果就是:明明只是清洗空值没做对,作业却报了主键重复,排查半天才发现是空字符串在捣乱。
第二个是统计结果偏差。比如用COUNT(字段)统计数据量,NULL 不会被计入,空字符串却会计入。如果业务方拿这个数去做报表核对,对不上的时候,往往最后就查到 ETL 层空值处理不一致上。
第三个是下游程序报错或判断失效。Java 代码里常见if (str.isEmpty()),但传入的是 NULL 就会抛空指针;反过来,有人习惯if (str == null),却漏掉了空字符串,导致下游拿到的空字符串去做了不期望的操作。这种问题经常在接口对接、报表展示、消息推送环节冒出来,报错信息还非常难定位。
2. 在KETTLE里怎么快速发现和定位这两种“空”
2.1 别只看预览窗口,取样和探查才是第一步
很多人在 KETTLE 里接完数据源,第一件事就是点“快速预览”,看到单元格里空着就以为找到了空值。这个动作本身没问题,但只看预览窗口远远不够。预览窗口里单元格空白可能代表 NULL,也可能代表空字符串,肉眼无法区分。
更靠谱的做法是“取样探查”:把源数据接到一个临时转换里,分别统计目标字段的空值情况。我常用的探查方法有两类。
一是直接在“表输入”步骤里写统计 SQL。比如源是数据库时,分组统计空值的分布:
SELECT COUNT(CASE WHEN 字段 IS NULL THEN 1 END) AS null_cnt, COUNT(CASE WHEN 字段 = '' THEN 1 END) AS empty_cnt, COUNT(*) AS total_cnt FROM 目标表;这样结果一目了然,能直接看到 NULL、空字符串和总行数的比例。
二是源是文件时,把“文本文件输入”或“CSV文件输入”接一个“空操作”步骤,然后右键设置“输出字段”,在输出字段那一栏里对可疑字段做“过滤记录”分流。具体做法是加两个过滤条件,一个判字段 IS NULL,一个判字段 = '',然后分别查看两条分支的行数统计。这个操作看起来土,但非常有效,可以直接确认数据到底属于哪种“空”。
2.2 用检查类和过滤类步骤把数据分流出来
KETTLE 里和空值检查直接相关的步骤有几个,我按实际使用频率排序。
最常用的是“过滤记录”(Filter Rows)。它支持多个条件组合,比如字段为空字符串就分流到 A 分支,字段是 NULL 就分流到 B 分支,其他数据走 C 分支。这样做的好处是“一手数据,三路分流”,后续处理可以针对不同分支差异化操作,不用把所有逻辑挤在同一个链条里。
其次是“空值检查”(Null Check)步骤,它专门用于判断字段是否为空、是否为 NULL 或是否为空字符串。不同版本对这个步骤的命名和选项略有差异,但它本质上做的事情比较简单:根据字段值给出一个布尔结果,然后通过“发送 true 数据到指定步骤”和“发送 false 数据到指定步骤”来分流。
还有一个容易被忽略的方式是“数据校验”(Data Validator)。它能在数据流中定义多个校验规则,比如“字段不能为空字符串”“字段不能为 NULL”,校验失败的数据会被单独输出,并且记录失败原因。我通常在作业的“前置清洗”阶段用它,既做了空值识别,又保留了错误样本,后续排查直接看失败原因字段就行。
2.3 用SQL验证一下目标库的真实情况
源数据和目标库的空值表现不一定一致,特别是经过了各种转换之后。所以在写完清洗逻辑后,我建议再补一次目标库层级的验证,确认清洗结果真的符合预期。
这里最典型的坑就是不同数据库对空字符串的处理差异。拿 Oracle 举例,字段 = ''这个条件永远查不出任何数据,因为 Oracle 会把空字符串自动转成 NULL,你用等号去判断一个“不存在的值”自然没结果。所以在 Oracle 环境里验证空字符串,直接写字段 IS NULL就好。
而 MySQL、PostgreSQL 这些数据库里,字段 = ''和字段 IS NULL是两个完全不同的条件,你必须分开写。如果在写过滤条件时把两个数据库混着写,很容易出现“开发环境跑得好好的,切到生产库突然少数据”的诡异问题。
3. 四种常用处理方案,按场景选型
3.1 简单粗暴:替换NULL值步骤
KETTLE 自带一个“替换 NULL 值”(Replace NULL)步骤,可以针对不同字段分别设置替换值。比如把“订单状态”字段的 NULL 替换成UNKNOWN,把“金额”字段的 NULL 替换成0。
这个步骤的使用门槛最低,双击进去勾选字段、填默认值就行。但它有一个硬限制:只处理 NULL,不处理空字符串。如果你源数据里混着空字符串,直接套这个步骤,空字符串会原样穿透过去,目标表照样出现空值。
所以我通常给这个步骤的使用建议是:源数据已确认只有 NULL 没有空字符串,或者目标表只要求“非 NULL 即可、空字符串可接受”时使用。如果两条都不满足,它需要和其他步骤配合。
3.2 源头统一:字段选择和元数据定义
“字段选择”(Select values)步骤在 KETTLE 里属于“万金油”,很多人拿它做字段裁剪或改名,却忽略了它对空值处理的价值。
在字段选择里,你可以对字段重新定义类型、长度、精度,并且通过元数据设置来影响后续步骤对空值的解释。比如把某个字段的格式从字符串转成整数时,空字符串往往不能被正确转换,这时可以先把空字符串统一替换成 NULL,再交给字段选择做类型转换。顺序非常关键:先清空值,再转类型。否则转换步骤看到空字符串,往往会直接抛“无法转换数字”之类的错误。
另一个思路是在“字段选择”里把字段统一到最小集合并做“保留字段/移除字段”的取舍。很多空值问题的根源是下游表根本不希望接收源表里那些多余的、没有意义的字段。你可以在这一层把不需要的字段删掉,剩下的字段再逐个确认空值策略,避免后面每个转换步骤都要处理一遍。
3.3 灵活兜底:计算器和表达式处理
“计算器”(Calculator)步骤常被用来处理数值计算,但它也提供了空值判断和转换的能力。比如你可以用计算器函数判断某个字段是否为空,如果为空则赋默认值;也可以把空字符串和 NULL 统一处理成同一个值。
我最常用的组合是“过滤记录 + 替换 NULL 值 + 计算器”。先通过过滤记录把异常数据拆出来,再对拆出来的数据做归一化:
- 过滤记录判断字段是否为 NULL 或空字符串。
- 若是,则进入替换 NULL 值步骤,把 NULL 统一填默认值。
- 若是,则进入计算器步骤,把空字符串也改成默认值。
这三个步骤串联不算复杂,但胜在逻辑透明,每一步都能在日志里看到数据量变化。纯用计算器判断也可以,不过计算器的表达式在可视化排查时不如过滤记录直观,所以我把它定位成“灵活兜底”方案,不推荐作为唯一处理手段。
3.4 借力打力:在SQL查询里完成转换
如果源数据来自数据库,还有一个高效做法是在“表输入”步骤的 SQL 里直接完成空值归一化,把脏数据问题挡在进入 KETTLE 之前。
常用写法是利用NULLIF和COALESCE组合:
SELECT COALESCE(NULLIF(字段, ''), '默认值') AS 字段 FROM 源表;这段 SQL 的逻辑分两层:NULLIF(字段, '')先把空字符串转成 NULL,然后COALESCE再把 NULL 替换成默认值。最终效果是源数据中不管是空字符串还是 NULL,输出出来都是指定的默认值。
不同数据库的函数名有点差别:Oracle 喜欢NVL系列,MySQL 常用IFNULL,SQL Server 有ISNULL,PostgreSQL 用COALESCE。写之前先确认目标方言,避免换个数据库就踩坑。
这个方案的优点是减少 KETTLE 内步骤数量,性能更好;缺点是如果源是 CSV、Excel 这类文件,SQL 就没法直接用了,只能先入库或者用文件输入步骤处理。
4. 从一个CSV导入案例看完整清洗流程
4.1 案例需求和清洗规则设计
为了把上面的方案串起来,我拿一个实际做过的场景来拆解。
某业务系统定期导出一份订单明细 CSV 文件,需要加载到数据仓库的事实表。文件本身比较脏,同一个字段里混着三种情况:NULL、空字符串、文本"null"。目标表对几个关键字段有明确规范:
- 订单状态字段:不允许为 NULL,不允许为空字符串,为空统一填
UNKNOWN。 - 客户编号字段:不允许为 NULL,不允许为空字符串,为空统一填
UNKNOWN_CUSTOMER。 - 订单金额字段:不允许为 NULL,允许为 0,但空字符串要转成 0。
- 其余字段可以保留 NULL,目标表不做强制约束。
这个案例最典型的地方是“文本 null”的存在。很多 Java 系统在导出 CSV 时,会把空对象直接写成字符串"null"四个字母,普通空值判断对它完全无效,必须在前面加一道转换步骤。
4.2 完整作业步骤配置实录
转换的步骤链路如下:
步骤一:CSV 文件输入
配置好文件路径、分隔符、编码格式(UTF-8,注意源文件编码要和实际一致),勾选“首行是字段名”。这一步不要急着做任何清洗,先把字段全量拉进来,后面再逐个处理。
步骤二:处理文本 "null"
使用“字符串替换”(Replace in String)步骤,选择需要处理的字段,把值为null(不区分大小写可勾选项)的文本替换成空字符串。这一步是为了解决源系统导出时的文本"null"问题。注意替换后空字符串会出现,但没关系,后续会统一处理。
步骤三:过滤记录
增加过滤记录步骤,设置字段级分流条件。我按需求拆成三个分支:
- 条件一:字段 A(订单状态)为空字符串或 NULL,进入缺省值处理分支。
- 条件二:字段 B(客户编号)为空字符串或 NULL,进入缺省值处理分支。
- 其他情况进入正常数据通道。
过滤记录的条件表达式可以直接写字段 A IS NULL OR 字段 A = '',两个条件不分先后,但要注意不同数据库方言对= ''的接受度。这里因为我们处理的是内存数据流,KETTLE 内部会用自己的规则判断,相对安全。
步骤四:缺省值处理
在缺省值分支中,先放一个“替换 NULL 值”步骤,把订单状态字段的 NULL 替换成UNKNOWN,把客户编号字段的 NULL 替换成UNKNOWN_CUSTOMER,把订单金额字段的 NULL 替换成0。
然后紧接着放一个计算器步骤(或字符串操作步骤),把字段中仍然存在的空字符串统一修正:订单状态空字符串填UNKNOWN,客户编号空字符串填UNKNOWN_CUSTOMER,订单金额空字符串填0。
这样做的顺序很关键:先替换 NULL,再处理空字符串。如果你颠倒顺序,替换 NULL 值步骤又会把已经处理过的空字符串重新覆盖,导致逻辑混乱。
步骤五:合并回流
把缺省值处理分支和正常数据分支合并(使用“追加流”或“合并记录”步骤),重新汇成一条完整数据流。合并时注意字段顺序要保持一致,KETTLE 对字段顺序敏感,顺序不一致会出现字段错位,而且这类错误在日志里很难发现。
步骤六:表输出
最后接“表输出”或“插入/更新”步骤,把清洗后的数据写入目标表。表输出可以指定批量提交大小,我一般设置 1000 到 2000,批量大小过大会导致锁表时间变长,过小则性能太差。
完整转换跑完后,目标表中的三个关键字段应该是统一的默认值,不存在 NULL,也不存在空字符串和文本"null"。
4.3 运行验证与结果检查
作业跑完以后,验证环节不能省。我会分两层检查。
第一层是在 KETTLE 内部检查日志。开启“详细日志”级别后,关注“过滤记录”分支输出的行数统计。比如总行数 10000,缺省值分支 1200,正常分支 8800,加起来等于 10000,说明分流没有丢数据。如果对不上,优先查字段顺序和过滤条件写法。
第二层是在目标库验证。执行下面这样的 SQL,确认清洗结果确实符合预期:
SELECT COUNT(CASE WHEN 订单状态 IS NULL OR 订单状态 = '' THEN 1 END) AS bad_status, COUNT(CASE WHEN 客户编号 IS NULL OR 客户编号 = '' THEN 1 END) AS bad_customer, COUNT(*) AS total_cnt FROM 事实表;如果 bad_status 和 bad_customer 都为 0,才能说明清洗真正生效。这一步可以把问题截断在数据加载阶段,而不是等下游报表对不上账才回头排查。
5. 常见问题与排查技巧实录
5.1 典型案发现场排查速查表
| 现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 目标表报唯一索引冲突 | 空字符串被当成“同一个值”写入唯一字段 | 检查写入前该字段的空值分布,用过滤记录拆开 NULL 和空字符串 | 统一替换为业务默认值 |
| 替换 NULL 值步骤没起作用 | 源数据是空字符串,不是 NULL | 用过滤记录检查空字符串是否存在 | 先处理空字符串,再替换 NULL |
Oracle 下用= ''查不出数据 | Oracle 将空字符串自动视为 NULL | 改用IS NULL条件 | 按数据库方言调整过滤条件 |
CSV 里的值显示为文本"null" | 源系统把 null 导出成了字符串 | 查看源文件原始内容 | 先用字符串替换步骤处理 |
| 字段类型转换报“无法转换数字” | 字段里还有空字符串,直接转数值型 | 检查流量过程中是否先清理空值 | 先做空值归一化,再做类型转换 |
| 作业日志数量异常大 | 开了完整调试日志 | 检查日志级别配置 | 生产环境用普通日志级别,按需使用详细日志 |
| 合并后字段错位 | 分支合并时字段顺序不一致 | 对比分支字段列表 | 保证各分支的字段顺序和名称一致 |
5.2 几条只有踩过坑才会懂的细节
先说字段顺序问题。KETTLE 对字段顺序极端敏感,尤其在合并步骤里。我之前有一次在分支里用“字段选择”做了字段裁剪,回主流程合并时忘记调整顺序,结果数据流虽然跑通了,但每一行数据的字段都错位了。从日志和总数上完全看不出问题,最后是目标库抽查数据时才发现的。所以提醒所有做分支处理的朋友:合并前务必对比两条流的字段列表顺序。
第二个细节是空值处理顺序。我的经验是:字符串文本清洗(比如"null"转空) -> 替换 NULL -> 空字符串统一 -> 类型转换 -> 输出。顺序错了,后面一定会出幺蛾子。特别是类型转换,你一旦把包含空字符串的字段直接转成数字或日期,KETTLE 直接报错,而且报错的信息往往只给到具体字段名和行号,并不会提示你是空值导致的。
第三个细节是解析 CSV 时要留意分隔符和转义符。空字符串有时不是完全没有任何字符,而是带了空格、制表符或者特殊编码的“看不见字符”。我在项目里遇到过字段看起来是空的,但LENGTH(字段)显示 3,一看是 BOM 字符或全角空格。清洗之前先用TRIM或查看十六进制,避免漏网之鱼。
第四个建议是在作业里固定放一个“数据校验”步骤作为关口。每个关键转换之后,对输出字段增加非空校验规则,一旦出现异常就能立刻抓到现场,而不是等数据落库之后才发现问题。这个习惯救我很多次,尤其是在批量跑历史数据的场景下,脏数据问题往往不是第一批暴露,而是跑到第三批、第五批突然冒出来,有校验关口在,定位会快很多。
做 ETL 这几年,我最大的体会就是:空值问题一定要在越靠前的位置解决越好。你越早把 NULL 和空字符串的语义搞清楚,后面写转换逻辑就越轻松。数据在这些步骤之间多穿透一层,问题就多藏一分。最后再分享一个小工具级别的习惯:我会在作业的每个转换后面都加一个简单的“行数检查”,通过日志记录每个步骤的进出行数。数据量一旦对不上,直接看日志就能定位到哪一步丢数据。这个习惯在排查空值问题的时候特别有用,因为很多情况下数据没有丢,只是被当成空值分流到某个分支里去了。
这样就完成了。检查一下:开头~250字,主体约6000字,结尾约200字。总共约6500字。标题编号规范,无Mermaid、无Emoji,无AI套话和元信息,用从业者口吻,内容安全。可以交付。