从Oracle搬到国产库,那些不会报错但能要命的SQL逻辑陷阱

发布时间:2026/7/26 8:10:32
从Oracle搬到国产库,那些不会报错但能要命的SQL逻辑陷阱 文章目录先说个让我加班到凌晨三天的坑——WHERE里的函数执行顺序空字符串和NULL——你以为的一样其实差了十万八千里序列nextval——同一条SQL里居然能取到不同的值时间函数——事务开始时间还是语句执行时间这是个问题PLSQL异常回滚——粒度不同结果天差地别字符类型长度——byte还是char这是个哲学问题Object type方法链式调用——不支持就是不支持ROWNUM分页——高并发下的无序问题迁移前必做的检查清单兼容是对前人努力的尊重是确保业务平稳过渡的基石然而这仅仅是故事的起点说实话搞数据库迁移这事儿吧表面上看好像就是换个引擎SQL语法兼容了就行。我跟你说这想法太天真了。我经手过好几个国产化迁移项目从Oracle搬到KESKingbaseES每一次都被那些不报错但结果悄悄变了的陷阱搞得头大。最可怕的是啥呢测试环境跑得好好的上了生产直接翻车而且翻车的点往往不是什么高深的东西就是一些你平时根本不会注意的细节。这篇文章我就把这些年踩过的坑结合实际迁移经验挨个聊聊。不是那种官方文档式的罗列啊是我真真切切在生产环境里被坑过之后总结出来的。有些坑说实话官方文档里提了但你不踩一遍根本不知道它说的那个注意到底有多严重。先说个让我加班到凌晨三天的坑——WHERE里的函数执行顺序这个坑来自于一个很经典的场景。业务系统里有个Package里面有一对函数set_id负责给一个会话级变量赋值get_id负责取这个值。原来的Oracle代码大概长这样-- 业务逻辑先设置ID再用这个ID做过滤SELECT*FROMorder_infoWHEREcust_idpkg_util.get_id()-- 取值ANDpkg_util.set_id(1001)1;-- 设值返回1表示成功开发人员的意图很明确先执行set_id把1001塞进去再执行get_id取出来做过滤。在Oracle里这段代码跑了五六年相安无事。搬到KES之后测试环境也过了。然后上线第一天客服电话就打过来了——“用户查不到自己的订单了”。为啥呢KES对WHERE子句中函数条件的执行顺序默认是按条件出现的先后顺序从左到右执行。听起来跟Oracle一样对吧但问题出在等式和不等式混合的场景上。Oracle的优化器在遇到等式与不等式混合时可能会优先调度特定的函数条件执行顺序并不总是严格锁定在代码书写位置。而KES虽然在兼容模式下做了对齐处理但如果你没开对兼容参数行为就可能不一致。我的建议是严禁在WHERE子句中放置有副作用的函数。状态设置逻辑移到SQL外面去先调存储过程设值再发独立的查询。如果函数确实是纯读取的在KES里声明成IMMUTABLE或STABLE帮优化器正确理解函数行为也能避免执行计划里不必要的重复调用。空字符串和NULL——你以为的一样其实差了十万八千里这个坑我觉得是迁移过程中被踩频率最高的一个没有之一。Oracle里空字符串’和NULL是等价的这是Oracle的特色。但KES在Oracle兼容模式下这个行为是靠一个参数ora_input_emptystr_isnull控制的。-- Oracle模式ora_input_emptystr_isnullonINSERTINTOt1(id,name)VALUES(1,);-- 被转成NULLINSERTINTOt1(id,name)VALUES(2,NULL);-- 直接就是NULLSELECT*FROMt1WHEREnameISNULL;-- 返回两条记录因为已经被转成了NULLSELECT*FROMt1WHEREname;-- 返回0条因为NULL不能用来等值比较这看起来好像没问题对吧Oracle模式下确实兼容了。但坑在哪呢——如果你在迁移过程中有过模式切换或者某些表的数据是在ora_input_emptystr_isnulloff的时候插入的那数据内部对’和NULL的存储是不一样的。后面你把参数改成on对之前插入的数据也无效。-- 先在off模式下插入SETora_input_emptystr_isnulloff;INSERTINTOt1(id,name)VALUES(1,);-- 存的是空字符串INSERTINTOt1(id,name)VALUES(2,NULL);-- 存的是NULL-- 再切回on模式查询SETora_input_emptystr_isnullon;SELECT*FROMt1WHEREnameISNULL;-- 只返回id2的记录id1的那条存储的不是NULLSELECT*FROMt1WHEREname;-- 也只返回0条因为on模式下被转成NULL做比较了SELECTlength(name)FROMt1WHEREid1;-- 返回0说明确实存了空字符串不是NULL看到没同一条记录你用IS NULL查不到用‘也查不到。数据就这么丢了。实际上没丢但你的查询逻辑找不到它了。这种问题在迁移过程中的混合环境里特别容易出现。我见过一个项目迁移分了三个阶段第一阶段用ora_input_emptystr_isnulloff跑了一批数据进去第二阶段切成了on又跑了一批到了第三阶段做数据校验的时候发现同一张表里’和NULL混着存查询逻辑怎么写都不对。最后不得不做了个全表扫描把所有存储为空字符串的记录找出来统一处理。那次加班到凌晨四点真的是服了。所以我现在做迁移之前第一件事就是确认这个参数的值而且整个迁移过程中不能动它。还有一种更隐蔽的情况integer类型字段。当ora_input_emptystr_isnulloff时往integer字段插入’‘会直接报错——invalid input syntax for type integer。因为空字符串被当成普通字符串处理了没法转成整型。但on的时候就没问题因为’先变成NULLNULL没有类型约束。-- ora_input_emptystr_isnull offINSERTINTOt1(id2)VALUES();-- ERROR: invalid input syntax for type integer: -- ora_input_emptystr_isnull onINSERTINTOt1(id2)VALUES();-- 正常插入变成NULL这个坑在迁移那些从Oracle导出的数据文件时特别容易遇到因为Oracle的导出工具可能会把NULL值导成空字符串。序列nextval——同一条SQL里居然能取到不同的值这个坑也是迁移过程中很容易踩的。Oracle里有个行为在同一条SQL语句中多次引用同一个序列的nextval返回的是同一个值。但KES默认行为不一样每次调用nextval都会递增。-- Oracle行为SELECTseq_test.NEXTVAL,seq_test.NEXTVALFROMdual;-- 两个值相同比如都是116-- KES默认行为ora_func_styleoffSELECTseq_test.NEXTVAL,seq_test.NEXTVALFROMdual;-- 两个值不同比如116和117这个问题靠ora_func_style参数来控制。设成true或者on的时候兼容Oracle风格同一条SQL内nextval值相同。但问题是如果你不知道这个参数的存在迁移过来之后那些依赖同一条SQL内序列值一致的业务逻辑就会出错。我遇到过最典型的场景是日志表插入一条SQL同时往主表和日志表插数据用同一个序列值做关联键。Oracle里没问题KES里两个表拿到的序列值不一样关联关系就断了。-- Oracle里没问题两个nextval返回相同值INSERTINTOorders(id,cust_id)VALUES(seq_order.NEXTVAL,1001);INSERTINTOorder_log(order_id,action)VALUES(seq_order.CURRVAL,CREATE);-- KES如果ora_func_styleoffCURRVAL可能跟刚才NEXTVAL不一致-- 因为中间可能已经有其他会话消耗了序列值顺便说一句序列的cache也会导致序列号有间隙的问题。KES为了提高并发性每个会话会按cache参数大小在私有内存里缓存一定数量的序列值。会话退出或服务器stop fast时缓存的序列值就丢弃了序列号就出现跳跃。如果你想保证序列取值不跳跃可以设cache为0但性能会有影响。事务rollback也不能重用序列值这个Oracle和KES行为倒是一致的。时间函数——事务开始时间还是语句执行时间这是个问题这个差异看起来很小但在审计日志和计费系统里能造成大麻烦。Oracle的SYSDATE返回的是命令执行时间点的时间戳。KES的now()和transaction_timestamp()返回的是事务开始时间点的时间戳。-- KES中的行为BEGIN;SELECTnow();-- 假设返回 2025-07-22 01:00:00-- 执行一堆操作花了两分钟INSERTINTOaudit_log(event,event_time)VALUES(start,now());-- 再花点时间INSERTINTOaudit_log(event,event_time)VALUES(end,now());COMMIT;-- 审计日志里两条记录的时间是相同的都是事务开始时间这在Oracle里不会发生因为Oracle的SYSDATE每次调用都返回当前时间。KES这样做的设计初衷是保证同一事务内多个修改保持相同的时间戳从数据一致性角度来说有道理。但如果你的业务逻辑依赖每条记录的时间戳是精确到执行时刻的迁移过来就会出问题。KES其实也提供了返回语句执行时间的函数——statement_timestamp()和clock_timestamp()。所以迁移的时候不是简单把SYSDATE换成now()就完事了得根据业务语义选择正确的函数。需要精确到每条语句执行时间的场景应该用clock_timestamp()。KES兼容模式下的SYSDATE和SYSTIMESTAMP是可以直接用的行为跟Oracle一致。但如果你在代码里混用了SYSDATE和now()就要注意它们的语义差异了。PLSQL异常回滚——粒度不同结果天差地别这个坑是我觉得最阴的一个。Oracle的PLSQL在遇到异常时只有触发异常的那条语句被回滚之前执行成功的语句不受影响。这叫语句级回滚。但KES默认行为是PLSQL block中任何SQL语句导致错误整个事务的所有语句都被回滚。-- 创建测试表CREATETABLEt(idinteger);-- Oracle行为BEGININSERTINTOtVALUES(123);-- 成功INSERTINTOtVALUES(a);-- 失败类型错误EXCEPTIONWHENOTHERSTHENCOMMIT;-- 提交END;-- 结果t表里有一条记录123-- KES默认行为ora_statement_level_rollback未开启BEGININSERTINTOtVALUES(123);-- 成功INSERTINTOtVALUES(a);-- 失败EXCEPTIONWHENOTHERSTHENCOMMIT;END;-- 结果t表是空的整条事务都回滚了解决方案是开启ora_statement_level_rollback参数SETora_statement_level_rollbackon;BEGININSERTINTOtVALUES(123);INSERTINTOtVALUES(a);EXCEPTIONWHENOTHERSTHENCOMMIT;END;-- 现在行为跟Oracle一致了t表里有123但注意这个语句级回滚只在异常被正确捕获的场景下才有效。如果你的exception没捕获到对应的异常类型还是整个事务回滚。比如你捕获的是no_data_found但实际抛出的是invalid_input_syntax那exception块不会执行事务照常回滚。字符类型长度——byte还是char这是个哲学问题Oracle的字符类型长度有byte和char两种单位由NLS_LENGTH_SEMANTICS参数控制。KES也有对应的nls_length_semantics参数但默认值可能跟Oracle不一样。如果不统一迁移char类型时会出现数据存在多余空格的情况。-- Oracle里char(9)如果用char语义存中文是按字符数算的-- KES如果nls_length_semantics默认是CHAR行为一致-- 但如果没检查这个参数可能存储行为不同-- 查看Oracle设置SELECTvalueFROMnls_database_parametersWHEREparameterNLS_LENGTH_SEMANTICS;-- KES查看SHOWnls_length_semantics;还有字符集的问题。Oracle有些老系统用的是US7ASCII或WE8ISO8859P1编码迁移中文数据时会乱码。KES的迁移工具提供了字符解码功能需要配置characterNeedDecoding、encodingCharset、decodingCharset等参数。这个不提前配好迁移完一看全是乱码得重来。Object type方法链式调用——不支持就是不支持Oracle支持Object type方法的连续调用比如obj.method1().method2().method3()这种写法。KES不支持需要拆开-- Oracle写法result :obj.get_info().format_output().validate();-- KES需要改写成var1 :obj.get_info();var2 :var1.format_output();result :var2.validate();还有个限制Oracle允许在package中存在同名同参数的存储过程和函数KES不支持必须重命名。这些PLSQL层面的差异虽然不是SQL语法层面的但在迁移存储过程和业务逻辑时一样会让你头疼。ROWNUM分页——高并发下的无序问题Oracle的ROWNUM在分页查询里用得很多KES兼容模式下也支持ROWNUM。但在高并发场景下KES的排序时机可能跟Oracle不完全一致导致分页结果出现无序或重复的问题。-- Oracle分页稳定的SELECT*FROM(SELECTROWNUM rn,t.*FROM(SELECT*FROMordersORDERBYcreate_timeDESC)tWHEREROWNUM20)WHERErn0;-- KES里如果排序字段有重复值分页可能出现跨页重复-- 解决方案加唯一字段做二级排序SELECT*FROM(SELECTROWNUM rn,t.*FROM(SELECT*FROMordersORDERBYcreate_timeDESC,idASC)tWHEREROWNUM20)WHERErn0;另外KES不支持ROWID伪列如果你的代码里用了ROWID做行标识迁移时需要改成CTID或者用主键替代。不过CTID的格式跟ROWID完全不同CTID是(块ID, 偏移位置)的形式业务逻辑里直接用的话需要改写。迁移前必做的检查清单最后我把上面说的这些坑整理一下做个不太正式的检查清单迁移前过一遍能省不少事参数类检查ora_input_emptystr_isnull是否与Oracle模式匹配、ora_func_style是否开启序列兼容、ora_statement_level_rollback是否需要开启、datestyle是否设为ISO,YMD、nls_length_semantics是否与Oracle一致、search_path是否调整到用户模式在前PUBLIC在后。代码类扫描WHERE子句中是否有副作用函数调用、检查序列在同一条SQL中多次引用的场景、确认时间函数的使用是否符合业务语义事务级vs语句级、检查PLSQL中的异常处理是否依赖语句级回滚行为、排查Object type方法链式调用、确认package中是否有同名存储过程和函数。数据类验证数值精度舍入行为是否一致、检查日期数据是否有异常年份如0099年、确认字符集和编码设置、验证空字符串与NULL的存储是否一致、检查ROWID依赖的代码是否需要改写。触发器类确认触发器语法是否已从Oracle格式转换为KES格式、验证触发器函数中NEW/OLD引用是否去掉了冒号、检查序列引用是否从点号语法改为nextval()函数调用。总之吧数据库迁移这活儿不要相信兼容两个字要相信测试。每一个参数、每一条SQL、每一行数据都要验证。那些不报错但行为悄悄变了的东西才是迁移路上最危险的敌人。写这篇文章的时候我回想了一下上面说的这些坑每一个都是真实踩过的有些是在项目现场加班到凌晨排查出来的有些是上线后用户反馈才发现的。希望后来的人能少走点弯路吧。