1. 为什么从CSV到Oracle,我选了Kettle而不是一把梭
任务本身一句话就能说清:把一批csv文件导入到Oracle,也就是很多人挂在嘴边的“导入到pl/sql”。但这句话背后的麻烦,真正跑过一遍数据的人才有体会——编码不统一、日期格式参差、几十万行文件跑到一半报主键冲突,而且不能轻轻松松点了重来。如果只是用PL/SQL Developer自带的文本导入器点几下,文件小、格式干净的时候确实快;可文件一大、列一多、数据类型还乱七八糟,点完基本就开始后悔了。
1.1 这不是“导入不导入”的问题,而是数据到库之前要不要“过一遍”
PL/SQL Developer里有个“文本导入器”(Text Importer),用来导入csv看起来很方便:选择文件、预览、选择目标表、字段映射、点导入。数据量在几千行、字段都是简单字符、也没有复杂日期处理需求的时候,这条路是可行的,我早期也这么干过。但实际生产场景中,csv往往是从别的系统导出的,字段类型、长度、格式根本不是按你的目标表来设计的,会出现:中文字段已经乱码、日期既有2024/1/5 8:30又有2024-01-05 08:30:00、18位的订单号被当成数字后精度丢失、同一批数据里偶尔还混着空行和多个空格。用文本导入器处理这些,虽然也不是完全不行,但每导一次都要重新核对一遍映射关系,而且出错了没有日志、没有重试机制,只能从头再来。几万行数据也许还能忍受,几百万行的时候就非常痛苦了。
1.2 Kettle在整个场景里到底扮演了什么角色
Kettle(现在的官方名是Pentaho Data Integration,社区里习惯叫PDI)核心就是一个可视化的ETL工具。ETL三个字母对应到这次实际任务就是:Extract从csv文件里读取数据,Transform对读出来的数据做类型转换、去空格、统一日期等清洗动作,Load把处理好的数据写进Oracle目标表。这个链路最大的好处是可以保存成转换文件,下次换一个文件,只要把路径改一下,跑起来就行;整个过程中字段类型、格式规则、日志、错误处理都是可见、可控、可复现的,而不是一次性的“导入向导”。所以当手里有一份需要反复导入的csv,或者目标表字段结构相对固定时,我会优先考虑用Kettle搭一个转换流程,而不是直接开PL/SQL Developer的导入工具。这篇文章就把我实际搭建这个流程时用到的配置、思路和踩过的坑完整记录下来,给同样卡在csv入库这件事上的人做一个参考。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 开工前先把这几件事确认清楚:连接串、驱动、字符集
很多人在Kettle里半天连不上Oracle,问题往往不是Kettle本身,而是前置条件没有核对好。真正动手建转换之前,我习惯先确认三件事:驱动包是否就位、数据库连接类型是否选对、文件里到底是什么编码。这三件事里任何一件没搞定,后面所有操作都会白做。
2.1 pl/sql和PL/SQL Developer,目标到底是哪个
先把这个题目的常见歧义说清楚。很多人说“把csv导入pl/sql”,但pl/sql实际上是Oracle的编程语言,不是数据库,也不是能直接“导入数据”的地方。大家真正想表达的是:把csv文件导入到Oracle数据库的某个表里,然后通过PL/SQL Developer这个客户端工具去查看和使用数据。所以一开始我建议明确:你要导入的目标是一个已经存在的业务表,还是允许新创建一个表。如果是新表,建议先想好字段名、类型、主键,提前在数据库里把表结构建好,而不是让Kettle在运行中创建,因为Kettle自动生成的建表语句往往和业务预期有差距,基本不会生成合适的注释、约束和索引。
比如我这里规划一个常见的例子。目标表结构如下:
sql复制CREATE TABLE t_order (
order_id VARCHAR2(20) NOT NULL,
order_date DATE NOT NULL,
amount NUMBER(10,2),
customer_name VARCHAR2(100),
CONSTRAINT pk_t_order PRIMARY KEY (order_id)
);
order_id是20位以内的字符串,最典型的情况是里面存了20位以内的纯数字单号或者带前缀的编号;order_date是DATE类型;amount是金额。我手里的样例文件叫order_data.csv,表头是ORDER_ID,ORDER_DATE,AMOUNT,CUSTOMER_NAME,总行数大约5万,文件编码不确定,中文姓名在Windows记事本里打开正常。
2.2 驱动缺了就是连不上:ojdbc该放哪里才有效
Kettle连接Oracle不像连MySQL那样开箱即用。Kettle安装包默认并不自带完整的Oracle JDBC驱动,所以第一次新建数据库连接时,极有可能报找不到驱动类,或者在“测试”时提示无法加载驱动。解决办法很简单:从Oracle官方下载对应版本的ojdbc驱动jar包,放到Kettle安装目录下的lib文件夹里,然后重启Spoon。如果Kettle正在运行,放了驱动不重启是不会生效的。
驱动版本怎么选?我的建议是:不要一上来就找最新版,先看你连的Oracle是多少版本。Oracle 11g/12c用ojdbc8.jar基本都能兼容,Oracle 19c及以上可以试ojdbc8.jar或ojdbc11.jar,Kettle版本比较旧的时候,某些新驱动反而可能报不兼容。平时我的习惯是优先用且只用一个jar,避免多个版本混放引起类冲突。找不到jar包下载入口的话,可以去Oracle官方JDBC下载页面,或者看你本机Oracle客户端安装目录下是否有,比如$ORACLE_HOME/jdbc/lib/ojdbc8.jar。我自己在Windows的Oracle客户端目录里找到过,拷过来就能用。
还有一点值得注意:Kettle的lib目录是公共类加载目录,放进去的驱动对所有作业生效。如果你同时还要连Oracle以外其他数据库,尽量保证驱动之间没有同名类覆盖问题,出现莫名其妙的“ClassNotFoundException”时,优先怀疑lib目录里有多个版本的同类驱动在打架。
2.3 连接串细节:SID和Service Name最好不要凭感觉填
在Spoon中新建数据库连接,数据库类型选择“Oracle & Oracle XE”,连接方式有几种可选。实际用下来最关键的一个选择是:SID和Service Name不能填混。SID是一个数据库实例的唯一标识,形如ORCL;Service Name则是数据库对外提供的服务名,可能是orcl.example.com这种带域名的格式。Kettle里连接方式选“Native(JDBC)”后,如果下面类型选择的是SID,填写的“数据库名称”会被拼成类似:
text复制jdbc:oracle:thin:@192.168.1.100:1521:ORCL
如果连接方式选择的是服务名,则会拼成:
text复制jdbc:oracle:thin:@//192.168.1.100:1521/orcl.example.com
两者只差一个斜杠和写法,但填错之后报的错误不一样:SID填错常常报ORA-12505,Service Name填错报ORA-12514。看到这两个错误码时不用怀疑网络,第一件事先去确认你到底应该用SID还是服务名连接。一个快速的确认办法:在PL/SQL Developer里看已经保存的连接是怎么填的,或者直接问DBA要一条能用的JDBC连接串,照着填。
测试连接成功只是第一步,还有一个容易忽略的是用户名对应的schema权限。导入数据至少需要目标表的INSERT权限;如果要清空表再导入,还要DELETE或TRUNCATE权限;如果目标表不存在、要勾建表选项,还需要CREATE TABLE权限。我在测试时经常遇到连接能通,但跑转换时报“权限不足”,多半就是这个问题。
3. 一次完整导入:订单号还在、日期变成DATE、中文不乱码
前置条件搞定之后,就可以正式在Spoon里搭建转换了。我通常用这样一个结构,简单但足够应对大多数csv导入场景:
text复制CSV文件输入 -> 字段选择 -> 表输出
中间的“字段选择”不是必须的,但大多数场景下确实建议加。它的作用有两个:一是只保留目标表需要的字段,把csv里多余的列过滤掉;二是在这一步完成类型转换,比如把日期字符串转成真正的日期类型,避免直接把字符串丢给DATE列引起数据库隐式转换和潜在错误。第三步“表输出”负责把数据真正写入Oracle表。
3.1 CSV文件输入:输出字段类型不要直接信“自动”
在Spoon左侧“转换”里新建一个转换,从输入分类中找到“CSV文件输入”,拖到画布上,双击进入配置界面。界面里不同Kettle版本叫法略有差异,但几个关键配置项都一样:
- 文件路径:选择具体的csv文件。如果路径里包含中文,某些旧版Kettle会有编码问题,我一般会避免把文件放在中文目录下。
- 分隔符:csv最常见的是英文逗号,但我也见过用分号或制表符的。如果点击“获取字段”后解析出来只有一列,八成是分隔符没有选对。
- 文本限定符:默认是英文双引号。如果csv里某个字段本身包含逗号,比如“备注:你好,世界”,这个字段在文件里一般会被双引号包起来,此时文本限定符必须设为双引号,否则内容会被错误地拆成多列。
- 编码:这是后面会重点踩坑的地方。不确定文件编码时,先用记事本或Notepad++打开看一眼,看右下角显示的编码,再回来选UTF-8或GBK。Kettle默认不一定会猜对你文件的编码。
- Is a header present / 是否包含头部:如果csv第一行是列名,这里要选是。选错的话第一行数据会被当成表头丢掉。
- 获取字段:点击这个按钮,Kettle会读取文件头部来猜测字段名、类型、长度。这里自动猜出来的类型,尤其是数字和日期,不能全信,需要逐个检查。
以我的order_data.csv为例,“获取字段”之后Kettle通常会把ORDER_ID识别成Number,把ORDER_DATE识别成String或Date,把AMOUNT识别成Number,把CUSTOMER_NAME识别成String。如果你的订单号是纯数字且超过15位,这里如果维持Number类型,后面导入后订单号末尾就会悄悄变成0。所以我的习惯是:先在“获取字段”里看清楚字段名单,然后把所有看起来像业务编号的列都改成String类型;金额这种真正的数值再做为Number。调整方法就是在字段表格里直接改Type列,改完点“确定”。
还要提一个技巧:不想一次性导入整个大文件调试的时候,可以在CSV文件输入里设置“限制”或者叫“最大行数”,填100,第一次运行就只读前100行。确认字段映射、类型转换都没问题了,再把限制清空跑全量,能省不少来回排查时间。
3.2 用字段选择步骤把String日期转成真正的Date
我特意把日期单独拿出来讲,因为这是最容易报错、又最容易被新手忽略的地方。如果csv里的ORDER_DATE长这样:
text复制2024/1/5 8:30
2024-01-05 08:30:00
第一行是斜杠分隔且个位数不补零,第二行是横杠分隔且是标准时间格式。Kettle的CSV输入步骤在自动识别时,最多只能按一种默认格式去猜,一旦遇到第二种就不太稳定,要么报“不能解析”,要么读出来是空值。稳妥的做法是:在CSV文件输入里把ORDER_DATE的类型先设置成String,这样它原始是什么样就还是什么样,不会在读取阶段报错;然后经过“字段选择”步骤做格式化。
“字段选择”步骤拖到画布上,放在CSV输入和表输出之间,用连线把它们连起来。双击字段选择,在“选择”页签里保留需要的四个字段;在“元数据”页签里,把每一行的“字段名称”选中,再修改“类型”和“格式”。比如要转换ORDER_DATE,把字段选为ORDER_DATE,类型改成Date,格式写成Java日期模式:
text复制yyyy-MM-dd HH:mm:ss
如果CSV里的原始日期格式与这个pattern不完全一致,可以在解析后先通过“字符串操作”或“字段选择”统一清洗,把/替换成-。例如在CSV输入与字段选择之间加一个“字符串替换”或“替换”步骤,把占位符里的/替换成-,再进入日期转换。实战里格式乱到不行的情况也遇到过,这种时候我不会硬在Kettle里做复杂清洗,而是先把csv按原样全导入到一个临时表/staging表,所有列暂时都是VARCHAR2,再用SQL配合TO_DATE和CASE WHEN去统一处理。把麻烦留给SQL,Kettle只做搬运,这个思路在一些脏数据场景里效率更高。中间清洗后的数据可以通过“Preview rows”预览,确认日期确实变成了形如2024/01/05 08:30:00的Date类型值,再继续往下。
3.3 表输出:字段映射、提交大小和“清空表”选项
画布上再拖一个“表输出”步骤,双击进入配置。首先要新建或选择一个数据库连接,就是我们第2章里确认过能连通的Oracle连接。点击“浏览”找到目标表T_ORDER,这一步如果能在下拉列表里看到表,说明连接和权限基本没问题。
“表输出”上有几个关键配置:
- 提交记录数量大小(Commit size):默认一般是1000,对于几万行的小文件这个值够用。Kettle是按照这个数量分批提交的,比如5万行数据会分成50次提交,一次失败不会把所有已提交的数据回滚。追求速度可以调到5000,但如果目标表上有较多触发器或复杂约束,提交太大反而容易导致锁竞争或回滚段压力,我一般稳定设在1000左右。
- Truncate table(清空表):如果希望每次导入前先清空目标表,可以勾选这个选项。但它非常危险,尤其目标表是生产环境正在使用的表时,绝对不要轻易勾。勾了之后每次执行第一件事就是把表清空,清完再插入。一般来说,我更推荐在Kettle里用“执行SQL脚本”步骤明确执行
TRUNCATE TABLE t_order或者DELETE FROM t_order WHERE ...,这样这条危险操作在流程里是显式可见的,不容易被后面接手的人误开。 - 字段映射:点击“数据库字段”或“获取字段”按钮,Kettle会把目标表字段和上游流字段按照名称自动匹配。如果CSV字段名和目标表字段名一致,通常能自动对上。名称不一致时,左边是流字段,右边是目标表字段,手动拉一下对应关系即可。
做完配置后,执行前还要检查一下逻辑是否对齐:流里的ORDER_ID是String、目标表order_id是VARCHAR2,没问题;流里的ORDER_DATE已经通过字段选择转换成了Date、目标表order_date是DATE,没问题;AMOUNT是Number、目标表是NUMBER(10,2),没问题;CUSTOMER_NAME是String,目标表长度100,应该也没问题。如果csv里中文姓名长度超过100,插入时会报“值太大”,这种问题不太会在导入阶段暴露,而会在批量提交时报ORA-12899,需要在字段选择里对字段做长度检查或截断。
3.4 运行验证:不要只在Spoon里看到绿色勾就算完
配置完成后点击“运行”,Kettle会开始执行转换。界面上可以看到每个步骤的行数流动情况,日志里会显示每一步读取了多少行、写出多少行。看到绿色对勾只代表该步骤执行成功,不代表数据一定按预期进了表。我会习惯性做一个双重校验:第一,在Spoon日志里记录的目标表输出行数,与CSV文件总行数做一次减法;第二,去PL/SQL Developer里执行一次COUNT:
sql复制SELECT COUNT(*) FROM t_order;
两个数字对上了,才敢说这次导入真正成功了。如果数量不一致,差多少行,再看日志里是否有被跳过的错误记录。Kettle的表输出在有脏数据时会报错并中断,但更隐蔽的是某些步骤在限定条件下会“跳过”个别行,所以COUNT核对这一步不能省。
4. 实测踩坑复盘:中文乱码、BOM头、长数字精度是怎么一步一步找出来的
说几个我在真实项目里遇到的坑,每一个都导致过返工。把它们单独拿出来复盘,是因为如果只看正确配置,永远不知道为什么要那样配。
4.1 中文乱码:不是目标库的问题,是文件编码没设对
第一个项目里,我从一个老业务系统导出csv,导入后PL/SQL Developer里打开表,CUSTOMER_NAME全是问号。一开始我怀疑数据库字符集问题,但查了目标库是支持中文的,其他渠道写进去的中文也正常,说明问题出在Kettle读文件这一侧。
排查过程是这样的:先用Notepad++打开源文件,看右下角显示编码是“ANSI as GBK”。也就是说,csv文件本身是用GBK编码保存的。Windows平台很多老系统的csv导出默认都是这个编码。同时Kettle在CSV文件输入里如果不手动指定编码,可能默认按UTF-8去解码,UTF-8去解码GBK的中文字节流,自然就变成乱码或问号。把CSV文件输入配置里的编码改成GBK之后,再预览数据,中文恢复正常。这里有个判断技巧:如果导入后中文乱码,但英文字段正常,基本就是读取编码没对上;如果连英文表头都断行、多列错位,那要优先怀疑分隔符和文本限定符的问题。UTF-8和GBK这两种编码在Kettle里都可以直接写,但要注意Linux服务器上运行时,系统默认编码可能和Windows不一样,同一个转换文件在Windows上正常换到Linux上却乱码了,优先检查有没有在转换里显式指定文件编码,而不是依赖系统环境。
4.2 列名变成 \ufeffORDER_ID:UTF-8 BOM引发的隐蔽故障
另一个很隐蔽的问题是BOM头。有时候文件本身是UTF-8编码,但用Windows记事本“另存为UTF-8”保存时,文件开头会带一个不可见的BOM字符(字节顺序标记)。这个字符在文本编辑器里看不见,但Kettle读进来后,第一列的列名会变成\ufeffORDER_ID而不是ORDER_ID。
这个故障的典型表现是:数据导入目标表时,第一列始终映射不上,日志里提示找不到字段,或者目标表第一列全部为空。我第一次遇到时花了不少时间排查,最后是CSV输入步骤里“获取字段”得到的字段列表,第一个字段名前面有乱码字符,才意识到是BOM问题。解决方案有两个:一是在源头上让文件变成“无BOM的UTF-8”。用Notepad++的“转换为UTF-8编码”而不是“以UTF-8编码保存”会比较稳妥;VSCode里可以在右下角点击编码,选择“通过编码保存”,再选UTF-8。二是在Kettle里不处理BOM,直接把CSV输入读取后的第一列字段名改对,比如在字段表格里手动把\ufeffORDER_ID改成ORDER_ID。这个办法省事,但要求每次源文件格式都稳定,如果换一个文件BOM不见了,列名反而对不上,所以从源头去掉BOM更好。
4.3 身份证号、订单号尾数变000:长数字一定不能用Number去读
还有一个典型问题出现在“订单号是20位以内的纯数字”这种场景。CSV输入“获取字段”时,Kettle看到一列全是数字,会默认识别成Number类型。问题在于Java的基础数值类型对超过15位以上的数字会丢失精度。比如18位订单号123456789012345678,很可能读进来就变成了123456789012345680,最后两位变成0。这种错误在导入阶段不会报错,因为类型、长度都合法,只有到了业务侧发现单号匹配不上时才会暴露,属于极难排查的隐性故障。
我现在的处理方式很明确:凡是订单号、身份证号、手机号、编号这类看起来是数字但不需要参与计算的字段,一律在CSV输入里手动把类型改成String。如果源数据已经被识别成了Number,就在字段选择里把类型改回来,同时确认目标表里对应列的类型是VARCHAR2或CHAR,而不是NUMBER。如果业务系统里历史遗留的目标表本身就是NUMBER列,csv里又是长数字,那就需要先跟业务确认精度需求和转换规则,不要自己扛着往里灌。用Kettle读取几十万行这种长数字列的性能很好,前提是不要把解析精度错付给数值类型。
4.4 主键冲突中断:失败后先清理“半截”数据
如果再导入过程中因为主键冲突或字段超长导致中断,表里很可能已经提交了一部分数据。Kettle的提交是按照Commit size分批提交的,中断时最后一批可能还没提交、也可能已经提交了一部分,数据库里就处于“导了一半”的状态。遇到这种情况,我一般会先执行一次:
sql复制SELECT COUNT(*), MAX(order_id) FROM t_order;
看一下导进去了多少、到哪条为止,然后把这一批次的数据清理掉,再回头修数据重新导。如果你想做的是全量覆盖,最干脆的姿势是先TRUNCATE TABLE t_order再重跑;如果目标是增量追加,要搞清楚断点在哪,避免同一批数据重复插入。实际排错时我还发现,有一些主键冲突不是源文件重复,而是长数字精度丢失之后,多条不同数据在库里变成了同一个值,这种时候光删掉重复数据再导是不够的,必须回到字段类型上解决。
5. 从手动导入到可维护的数据流程
搭好一条能跑的导入转换只是开始。真实业务里不会永远在Spoon界面里点运行,通常还需要把这个转换纳入到一个可以被定时调用、可以传不同文件路径、能够增量更新的流程里。最后这部分讲几点我个人常用的扩展方法。
5.1 参数化了才算可用:文件路径不写死
在CSV文件输入里,文件路径可以直接写死,但这样每换一次文件都要打开Spoon改一遍路径,容易出错。更好的方式是使用参数或变量。以文件名参数为例,把CSV文件输入的文件路径写成:
text复制${inputFile}
然后在运行转换时,通过Spoon左下角的“变量”或执行窗口填入实际路径,例如:
text复制inputFile=D:/data/order_data.csv
命令行模式下则可以用Pan传参。Kettle里参数变量是一个很实用的机制,文件路径、日期条件、数据库连接字符串都可以参数化。配好参数之后,同一个转换既能跑这个月的csv,也能跑下个月的csv,只要传不同的参数进去就行。另外,可以更进一步把“获取文件列表”和“循环执行”组合起来,批量处理一个目录下的多个csv文件。这里不写死路径带来的收益,在后期维护时最明显。
5.2 把转换包进作业:START、转换、成功分支
Kettle里的“转换”和“作业”是两种不同层次的东西。上面做的只是一个转换,负责单次数据处理;但如果想实现“按顺序跑多个转换、失败时发通知、成功时发邮件”,就需要建立一个作业(Job)。在作业画布上,最常用的结构就是:
text复制START -> 转换
Spoon左侧切换到“作业”,新建一个作业,从“通用”或“作业控制”里拖一个START开始节点,再拖一个“转换”作业项到画布上,双击指定要执行的.ktr转换文件。这样配置之后,作业运行时会自动去执行那个转换。如果后续要加导出结果文件、发送通知邮件,也都是在作业这一层做。这种设计的好处是把业务步骤和单个ETL步骤解耦,清理数据、导数据、发邮件各干各的,比把所有逻辑塞进一个转换里更容易维护。
5.3 定时执行:命令行跑起来才是王道
手动打开Spoon去点运行,在服务器上不现实;更规范的做法是用Kettle自带的命令行工具执行作业。Kettle安装目录下有两个常用脚本:执行转换用Pan,执行作业用Kitchen。Windows下对应的分别是Pan.bat和Kitchen.bat。比如我执行一个作业文件:
bash复制Kitchen.bat -file=D:/etl/jobs/import_order.kjb -level=Basic -logfile=D:/etl/logs/import_order.log
参数-level=Basic表示日志级别,-logfile可以把日志写到文件里,方便排查历史问题。这样配置后,就可以用Windows计划任务或者Linux上的cron定时执行,让导入任务在无人值守时自动跑。如果转换里使用了变量,也可以在命令行加-param:inputFile=D:/data/order_data.csv传参数。我的建议是:不要把日志级别设成Error级别然后就不管了,日常建议Basic或Detailed,至少能看到每个步骤处理的行数;生产环境中定期检查日志大小,避免日志文件无限增大把磁盘打满。
5.4 增量同步思路:不要每次都全量覆盖
如果每次数据都是全量快照,那一套“先清空再导入”的流程没有任何问题。但业务经常是每天产生新数据,这时候再用全量覆盖就不合适了。一个常见的增量思路是利用目标表的更新时间字段:先查询目标表最新的数据时间,把它作为本次导入的起始过滤条件,只把csv里晚于这个时间的数据插入。另一个更简单粗暴但有效的做法是使用“插入/更新”步骤替换“表输出”。插入/更新通过主键判断一行记录在目标表中是否存在:存在就更新,不存在就插入。实际操作中,我会先把csv数据导入一个中间临时表,再用SQL的MERGE INTO把临时表数据合并到正式表。这样做的优势是把增量判断放到数据库里,执行效率和事务控制都更好,Kettle里只需要做“从csv写临时表”和“执行SQL”两步。
不过这里想提个醒:增量同步的前提是数据文件本身有可靠的主键或唯一键。如果csv文件里的唯一标识缺失、重复,那不管用Kettle还是SQL,都无法真正实现增量,只能每次全量比对。所以在搭建同步流程之前,多花一点时间分析数据质量,比之后反复处理脏数据要划算得多。
回过头来,这条链路走完以后,从csv到Oracle的导入不再是一个一次次手工点击的临时动作,而是一套可以反复执行、接受参数、可查日志、可接入定时调度的标准流程。最开始拿到那份让人头疼的csv文件时,我焦头烂额地想了半天应该用什么工具去“导入”;现在再看,问题早就不是“怎么导进去”,而是“每次导的时候,怎么保证质量、效率和可维护性”。如果让我给第一次跑这个场景的人一句实在话:先别急着点运行,花十分钟确认文件编码、字段类型、目标表约束这三件事,后面能少踩一半的坑。
