我接手过不少带LONG类型的Oracle老库,说实话,每次看到这种表我都得先深吸一口气。倒不是LONG类型完全不能用,而是它带来的限制实在太多,多到会让你在写SQL、做分页、搞迁移的时候怀疑人生。这篇文章就专门聊聊LONG类型和CLOB类型的比较与转换,把我实际踩过的坑、验证过的方法、以及转换后容易忽略的细节一次性说清楚。
1. LONG类型为什么成了历史包袱
1.1 那个年代的设计逻辑
LONG类型是Oracle早期版本就有的设计,初衷很简单:在关系型数据库里存"大字段"。当时的字符型数据最常用的是VARCHAR2,但VARCHAR2在很长一段时间里被限制在4000字节以内,存不下长篇文本、完整HTML内容或者大段的JSON字符串,于是Oracle提供了LONG来填补这个空缺。
可以把它理解为"临时救场"的方案,Oracle官方后续也不再对LONG做功能增强,反而推了CLOB来全面替代它。在9i、10g这些版本里,LONG还能勉强混日子,但到了12c、19c、21c,LONG类型在某些场景下已经成了寸步难行的老古董。
1.2 LONG的七个致命限制
我在实际开发中总结过LONG类型最让人头疼的限制,遇到任何一个都足以让你回头改表结构:
- 一张表只能有一个LONG列,多一个都不行,报错信息ORA-01754你早晚会见一次。
- LONG列不能直接建索引。想在LONG字段上做搜索、排序、分组?这条路直接被堵死。
- LONG不允许出现在WHERE条件、GROUP BY、ORDER BY、CONNECT BY、DISTINCT等子句中,一旦使用就报ORA-00997。
- 不支持通过SQL函数直接操作,比如SUBSTR、INSTR对LONG的某些用法会直接报"非法使用LONG数据类型"。
- 不能做主键、外键、唯一约束的一部分。
- 在SQL*Plus或某些客户端工具里查询LONG时,显示方式非常不友好,常常只能看到前80字节,后面的内容直接被截断。
- 数据迁移(EXP/IMP、数据泵、物化视图、远程查询)都会绕开或限制LONG,处理起来要额外想办法。
说白了,LONG就像一条单行道的乡间小路,能走,但处处限宽。CLOB则是升级后的高速公路,容量更大、操作接口更丰富、对SQL语义的支持也更完整。
2. LONG与CLOB的底层机制差异
2.1 存储与行迁移的本质区别
LONG类型在底层的存储方式和VARCHAR2类似,属于行内(inline)存储,数据直接存在数据块里。当数据量大了之后,行迁移和行链接现象会变得明显,导致查询性能的不可控。而CLOB走的是LOB存储机制:一个CLOB列在逻辑上存储的是LOB定位器,真正的数据存放在独立的LOB segment和LOB index里。
这带来几个直接后果:
- CLOB列本身占的空间很小(约几十字节),即使数据很大,行迁移的压力也远小于LONG。
- CLOB数据支持分块(chunk)读取和按偏移量访问,这对应用层做"只取前100个字符做摘要"这类需求非常友好。
- CLOB在表结构里可以容纳多个,不再受"一表一LONG"的束缚。
2.2 操作接口的完全不同
LONG类型几乎没什么精细操作接口,你只能把它作为一个整体去查、去更新,想取出中间某一段都非常费劲。CLOB则配合DBMS_LOB包给开发者提供了完整工具箱:
- DBMS_LOB.SUBSTR:按偏移量取子串。
- DBMS_LOB.INSTR:查找子串位置。
- DBMS_LOB.GETLENGTH:获取长度。
- DBMS_LOB.COMPARE:比较两个CLOB内容。
- DBMS_LOB.APPEND:拼接大文本。
这种差异在做全文搜索、内容匹配、字段截取、数据比对时尤为明显。举一个实际场景:老系统里有个备注字段是LONG类型,前端页面只想展示前200字,LONG时期只能调文件接口慢慢拼,改成CLOB之后一行DBMS_LOB.SUBSTR就解决了。
2.3 长度上限与扩展性
LONG类型的最大长度是2GB-1字节,CLOB的最大长度也取决于数据库块大小配置,通常也能到2GB以上,而且因为CLOB支持多表空间存储和表空间管理,你可以把大字段单独放到一个独立的表空间,这在维护和管理上比LONG灵活太多。
生活化的类比:LONG像是老式磁带,内容长一点就要整盘倒带,想跳转到中间还得自己估算时间;CLOB则像U盘,文件再大也能直接定位、分块读写。
3. 转换实操:从ALTER TABLE到数据校验
3.1 最快路径:一条ALTER语句搞定
如果表结构允许,最简单直接的方案就是用ALTER TABLE语句把LONG列改成CLOB。
ALTER TABLE t_old_notes MODIFY (note_content CLOB);但这里有几个前提:
- 当前LONG列必须允许为空。如果列是NOT NULL,直接ALTER会报错,需要先取消NOT NULL约束,转换后再加回约束。
- 表数据量不能太大。ALTER TABLE对LONG列做转换时,Oracle后台会做全表扫描并根据需要重建行,数据量大的时候锁表时间会很长。
- 确认没有依赖该列的触发器、视图、存储过程或物化视图。这些对象在列类型变更后可能需要重新编译。
实测下来,百万级以下的小表用这种方案最舒服,几秒钟到几十秒就能完成。千万级以上的表建议走在线重定义(DBMS_REDEFINITION),后面细说。
3.2 新列+TO_LOB+改名方案
如果ALTER TABLE MODIFY受限,比如列不能直接改、或者表里还有别的复杂依赖,可以采用"新建并列迁移"的方式:
-- 1. 新增CLOB列 ALTER TABLE t_old_notes ADD (note_content_clob CLOB); -- 2. 用TO_LOB把LONG数据迁移到新列 UPDATE t_old_notes SET note_content_clob = TO_LOB(note_content); COMMIT; -- 3. 修改原列名(或用新列替代原列) ALTER TABLE t_old_notes DROP COLUMN note_content; ALTER TABLE t_old_notes RENAME COLUMN note_content_clob TO note_content; -- 4. 为必要的约束重新命名这里面TO_LOB是Oracle专门用于把LONG/LONG RAW转换成LOB的函数,在LONG转CLOB这个场景下极其好用。需要注意:
- UPDATE期间会产生大量undo和redo,如果原表数据极大,建议分批提交,例如按主键范围循环更新。
- 如果表上存在依赖原列的对象,DROP COLUMN之后记得重新编译这些对象。
3.3 大表场景:在线重定义
千万行、上亿行的表,直接ALTER TABLE MODIFY或UPDATE都不太现实,因为锁表时间不可控。Oracle为此提供了DBMS_REDEFINITION包,可以在线完成表结构变更,期间原表仍然可读可写。
基本步骤可以概括为:
- 检查表能否在线重定义。
- 创建一张结构相同但类型为CLOB的新表。
- 启动重定义过程,Oracle内部会完成数据同步。
- 结束重定义,让新表替换旧表。
简化示例:
-- 检查是否可以重定义 EXEC DBMS_REDEFINITION.CAN_REDEF_TABLE('SCOTT', 'T_OLD_NOTES'); -- 创建中间表(结构上把LONG改成CLOB) CREATE TABLE scott.t_old_notes_temp AS SELECT * FROM scott.t_old_notes WHERE 1=0; ALTER TABLE scott.t_old_notes_temp MODIFY (note_content CLOB); -- 启动重定义 BEGIN DBMS_REDEFINITION.START_REDEF_TABLE( uname => 'SCOTT', orig_table => 'T_OLD_NOTES', int_table => 'T_OLD_NOTES_TEMP' ); END; / -- 将原表上的约束、索引同步到中间表 BEGIN DBMS_REDEFINITION.COPY_TABLE_DEPENDENTS( uname => 'SCOTT', orig_table => 'T_OLD_NOTES', int_table => 'T_OLD_NOTES_TEMP' ); END; / -- 结束重定义 BEGIN DBMS_REDEFINITION.FINISH_REDEF_TABLE( uname => 'SCOTT', orig_table => 'T_OLD_NOTES', int_table => 'T_OLD_NOTES_TEMP' ); END; /这里要特别提醒,在线重定义虽然叫"在线",但结束重定义的前后几秒内仍然会有轻微的锁表窗口,生产环境需要在业务低峰期操作,并且提前做好回滚方案。
3.4 转换后的数据校验清单
类型转换最怕的就是数据丢失或内容被截断。我的习惯是转换后做一套完整校验,一条都不能少:
| 校验项 | SQL示例 | 预期结果 |
|---|---|---|
| 总行数一致 | SELECT COUNT(*) FROM t_old_notes; | 转换前后保持一致 |
| LOB非空数量一致 | SELECT COUNT(*) FROM t_old_notes WHERE note_content IS NOT NULL; | 转换前LONG非空数=转换后CLOB非空数 |
| 最大长度抽样 | SELECT MAX(DBMS_LOB.GETLENGTH(note_content)) FROM t_old_notes; | 和转换前LONG最大值对比 |
| 内容哈希比对 | 对主键抽样,比较ORA_HASH(TO_CHAR(note_content))或标准哈希 | 抽样记录哈希一致 |
尤其是内容哈希比对,最好别省。因为有些场景下LONG转CLOB之后会引入字符集转换的差异,肉眼看不出来,但程序比对会出问题。用上ORA_HASH能做到批量速筛。
4. 转换中比较容易翻车的几个场景
4.1 TO_LOB不是万能的
TO_LOB确实能把LONG数据搬到CLOB,但它只支持一次性把LONG列的数据完整搬到LOB列。如果源LONG列长度超过CLOB容量、或者存在畸形编码数据,TO_LOB会直接报错中断。我的经验是:对超大表执行TO_LOB迁移时,不要一股脑UPDATE,建议分批提交,且每批尽量按主键顺序处理。
例如:
DECLARE v_start NUMBER := 0; v_batch_size NUMBER := 10000; BEGIN LOOP UPDATE t_old_notes SET note_content_clob = TO_LOB(note_content) WHERE id > v_start AND ROWNUM <= v_batch_size; EXIT WHEN SQL%ROWCOUNT = 0; v_start := v_start + v_batch_size; COMMIT; END LOOP; END; /4.2 依赖LONG的SQL写法会失效
转换前,某些SQL虽然奇怪但能跑。比如:
SELECT id, note_content FROM t_old_notes WHERE note_content LIKE 'abc%';在LONG时代,这种写法是不允许的(ORA-00997),很多人会绕道。但转换后,CLOB在某些条件下可以支持LIKE操作,然而处理方式又和普通字符串不完全一样。如果你原有代码里用了DBMS_LOB、或者自定义函数来规避LONG限制,转换后这些代码的调用方式可能不再最优,甚至报错。
另一个容易翻车的场景是在临时表和中间表使用LONG列做分析运算。早年间有人拿着LONG列的数据往临时表里灌,再配合分页查询。转换后这类逻辑要重构,否则临时表的CLOB字段在排序、去重、分组时一样会受限,只是报错方式不同。
4.3 传统迁移工具的隐藏问题
用EXP/IMP或数据泵迁移表数据时,LONG和CLOB的处理路径差别极大。EXP对LONG列会做特殊处理,导入时可能需要额外参数或触发字段截断。CLOB在数据泵里则是标准化支持,几乎不会因为类型问题导致失败的。
如果你所在的旧系统还在用EXP做逻辑备份,转换前先做一个只含LONG表的导出测试,确认导出的dmp文件大小和导入行为都符合预期。不要等到正式迁移了才发现数据被截断。
4.4 执行计划变化引发的连锁反应
LONG转CLOB之后,表的行长度会显著变小(因为LOB数据被移出了行),这听起来是好事,但会导致已有的索引、分区、统计信息全部失效或失真。如果你转换完没有立刻收集统计信息,可能第二天就出现SQL性能下降的投诉。
EXEC DBMS_STATS.GATHER_TABLE_STATS('SCOTT', 'T_OLD_NOTES', cascade => TRUE);这一步几乎等于必须项。CLOB列本身虽然不能建索引,但它所在的表其他列的执行计划严重依赖统计信息的准确性,别忽视。
5. 藏得很深的兼容性坑
5.1 应用端代码的适配点
JAVA程序、Python脚本、PL/SQL包,凡是直接读取LONG列的应用,都要做一层适配。
在JDBC里,读取LONG列通常用getString或getBytes,数据量大的时候容易内存溢出;读取CLOB列则要用getClob再把流式内容读出来,处理方式完全不同。转换后如果应用层没改,往往会出现奇怪的字符截断或类型转换异常。
PL/SQL里也有个经典坑:
DECLARE v_text VARCHAR2(4000); BEGIN SELECT note_content INTO v_text FROM t_old_notes WHERE id = 1; END; /在LONG时期,这个写法一旦超过4000字节就可能报"值过大"。改成CLOB后,看似应该更包容,但如果仍声明为VARCHAR2(4000),依旧会因溢出报错。正确的做法是用CLOB类型的变量去承接,需要时再用DBMS_LOB.SUBSTR截取。
5.2 SQL*Plus和客户端工具显示问题
CLOB在SQL*Plus里默认显示同样受限,只显示一部分内容。虽然比LONG的80字节友好一点,但如果需要完整看到内容,记得设置:
SET LONG 2000000000 SET LONGCHUNKSIZE 200000否则你在调试数据时会被"只看得到一部分内容"误导,误以为转换丢数据了。
5.3 字符集转换的隐蔽风险
LONG类型和CLOB在特定字符集组合下,转换时可能出现字符集转换偏差。我处理过一个案例:LONG列里存储的是历史遗留的ZHS16GBK数据,数据库字符集升级为AL32UTF8后,LONG列的部分字符显示为乱码,转换后乱码问题依然存在,根本原因在于转换前就没有做好字符集清洗。
所以在换库或数据迁移时,先确认LONG列里的数据是否存在非法字符集编码。可以用UTL_RAW.CAST_TO_VARCHAR2或者IS HASH比较等方式做抽样检查。
6. 技术选型和性能对比表
很多朋友关心"到底要不要把LONG全部换成CLOB",答案很明确:迟早要换。Oracle官方对LONG的支持策略就是"能用但别再依赖",未来的功能演进全部集中在LOB体系里。
我在选型时会参考这样一张表:
| 维度 | LONG类型 | CLOB类型 |
|---|---|---|
| 表内列数限制 | 每表仅1个LONG | 多列CLOB无此限制 |
| 建索引 | 不支持 | 不支持(可建函数索引或全文索引) |
| 查询条件中的应用 | WHERE/GROUP BY/ORDER BY等受限 | 支持部分场景,配合DBMS_LOB更强大 |
| 数据访问方式 | 整块读写 | LOB定位器+分块访问 |
| PL/SQL操作支持度 | 功能极少 | DBMS_LOB全套接口 |
| 数据泵迁移支持 | 支持但不灵活 | 原生支持良好 |
| 官方演进方向 | 已停止增强 | 持续演进 |
从这张表能得出的结论是:LONG只在"老代码没空改"这个前提下有存在价值,但凡涉及新功能开发、性能优化、数据迁移,都应该把LONG列为优先改造对象。
7. 我的一些实操心得
最后分享几个我总结的细节,都是实际项目中沉淀出来的:
转换前先查依赖。别只盯着表结构,要把USER_DEPENDENCIES、USER_TRIGGERS、USER_VIEWS、USER_PROCEDURES全扫一遍。我遇到过一次,表只有几十万数据,但存储过程里对LONG列做了隐式拼接,转换后过程直接失效,回滚脚本又没准备好,当时差点要走数据恢复流程。
不要迷信"一条ALTER语句搞定"。ALTER TABLE MODIFY虽然方便,但它对undo和redo的消耗是隐性的。大表在执行期间会把整个表的镜像写进undo,如果undo表空间不够,会直接导致转换失败。建议大表先估算数据量,再决定使用在线重定义还是分批迁移。
分页查询的坑。LONG类型不允许直接在子查询中做DISTINCT或ORDER BY,但转换成CLOB后也并不意味着完全顺畅。我曾经在一个分页查询里对CLOB列做排序,执行计划直接走了全表排序,性能掉得厉害。解决办法是把CLOB的截断值(比如DBMS_LOB.SUBSTR(note_content, 100))作为一个独立字段排序。
还有一点,数据库12c以上引入了更大的VARCHAR2长度(32767字节),不少人问"既然VARCHAR2能到32K,还要不要用CLOB"。我只能说:如果你确定数据长度不会超过32767字节,用VARCHAR2当然可以;但现实中大文本字段的长度很难做硬约束,稍不留神就超限,CLOB依然是更稳妥的选择。
我处理过一个1000万行级别的旧系统,里面三张核心表的备注字段都是LONG,迁移前每次做增量报表都要绕道,改完CLOB后很多原来的限制都消失了,应用层再配合DBMS_LOB做截取,整体维护成本降了一个量级。这个过程不算复杂,但需要足够的耐心把依赖和边界条件确认清楚,希望这篇文章能帮你少走一些弯路。