引言
做国产化迁移这几年,我见过太多团队,把精力全放在语法兼容、函数替换、数据类型匹配上,然后就忽略了空值这个最不起眼、也最容易出大事的细节。空值问题比较隐蔽------测试数据干净的时候,你永远测不出来;等生产数据脏了、边界场景出现了之后才出来问题。
今天这篇文章,博主把迁库踩过的空值坑给大家说一下。从空字符串和NULL的是有啥区别,到怎么正确的判断空值,再到金仓里影响空值行为的关键参数、兼容模式下的表现,最后再给大家一套空值处理的规范。

一、最容易被忽略的迁移杀手:空值不一致
在正式讲坑之前,我先跟大家聊聊,为什么空值这么一个小小的东西,能闹出这么大的动静。
1.1 空值问题为什么这么难查
空值问题的排查难度,远超你的想象。
第一,它不会报错。语法没问题,执行没问题,就是结果不对。你连个错误日志都抓不到,只能靠人肉比对数据。
第二,它的影响是隐性的。不是所有数据都有问题,只有特定条件、特定字段、特定值的时候才会出问题。大部分数据都对,少部分不对,你很难发现。
第三,它的后果可能很严重。财务系统差一分钱都不行,人事系统少一个人都不行,政务系统漏一条记录都不行。空值问题带来的,往往就是这种"差一点"的错误,而恰恰是这一点,可能就是整个项目的生死线。
我常跟团队里的人说,迁移项目里,最危险的不是那些一上来就报错的大问题,而是那些看起来一切正常、实则暗藏玄机的小细节。空值,就是这些小细节里,杀伤力最大的一个。
1.2 为什么金仓和传统数据库的空值行为不一样
很多人会问,都是SQL数据库,怎么空值处理还能不一样?
这事儿说起来也简单。不同的数据库,发展的历史背景不一样,设计理念不一样,对SQL标准的实现程度也不一样。
有的数据库,历史悠久,早期为了降低使用门槛,做了很多宽松化的处理,空串和NULL混着来,用户怎么方便怎么来;有的数据库,设计的时候严格遵循SQL标准,把NULL和空值分得清清楚楚,是就是,不是就不是。
电科金仓作为新一代自主研发的闭源商用数据库,在设计上更严谨,也更贴近SQL标准。它对空值的处理逻辑非常清晰:NULL就是NULL,空字符串就是空字符串,两者泾渭分明,不会混为一谈。
但这也意味着,从那些处理逻辑比较宽松的传统数据库迁过来的时候,很容易出现水土不服------以前能跑通的逻辑,到这儿结果不一样了;以前能查出数据的条件,到这儿查不出来了。
不是金仓不对,是以前的数据库太"惯着"大家了。
二、第一个大坑:空字符串''和NULL,到底是不是一回事
这是空值问题里最核心、也最容易踩的一个坑。我敢说,做过Oracle迁移的人,十个里有八个栽在这上面过。
2.1 现场还原:一条WHERE条件引发的血案
就是我开头说的那个财务系统项目。
出问题的SQL,是一张辅助账表的统计查询,逻辑不复杂:查询备注为空的凭证,统计它们的金额合计。
开发写的条件是WHERE remark = ''。
在Oracle里,这条SQL跑得好好的,能查出三百多条数据,金额也对得上;
迁到金仓之后,同样的SQL,一条数据都查不出来,合计金额自然就是0,整张报表直接差了四十七万。
开发当时特别困惑,说"我在Oracle里用了五六年了,一直这么写啊,怎么到你这儿就不行了?"
我当时也纳闷,按理说空字符串就是空字符串,怎么会查不出来呢?直到我把那三百多条数据的remark字段查出来一看------
全是NULL。
2.2 金仓里的世界:空串是空串,NULL是NULL
而金仓不一样。金仓严格遵循SQL标准,把空字符串和NULL分得清清楚楚:
-
空字符串
'':是一个真实存在的字符串值,只不过长度为0,它是"有值的"; -
NULL:表示未知、不存在、未赋值,它是"没有值的"。
两者完全是两码事。
所以你写WHERE remark = '',金仓只会查出remark字段确实是空字符串的那些行;如果字段是NULL,是查不出来的。
反过来,你写WHERE remark IS NULL,也只能查出真正是NULL的行,空字符串的行查不出来。
这就是为什么那条SQL在Oracle里能查出三百多条,在金仓里一条都查不到------因为那些数据存的根本就是NULL,不是空字符串。Oracle帮你做了隐式转换,你感觉不到;金仓严格按标准来,问题就暴露了。
2.3 用测试表直观感受一下差异
我建两张sys_前缀的测试表,分别模拟两边的行为,大家一看就明白了。
SQL
-- 测试表:凭证表(sys规范前缀)
CREATE TABLE sys_voucher (
vch_id INT PRIMARY KEY,
vch_no VARCHAR(32),
remark VARCHAR(200),
amount NUMERIC(12,2)
);
-- 插入测试数据:一条NULL,一条空串,一条正常值
INSERT INTO sys_voucher VALUES (1, 'V001', NULL, 1000.00);
INSERT INTO sys_voucher VALUES (2, 'V002', '', 2000.00);
INSERT INTO sys_voucher VALUES (3, 'V003', '正常备注', 3000.00);
现在我们分别执行几个查询,看看金仓里的结果:
查询1:查remark等于空串的
SQL
SELECT * FROM sys_voucher WHERE remark = '';
金仓返回结果:只有第2条,金额2000。
查询2:查remark是NULL的
SQL
SELECT * FROM sys_voucher WHERE remark IS NULL;
金仓返回结果:只有第1条,金额1000。
查询3:查remark不为空的
SQL
SELECT * FROM sys_voucher WHERE remark IS NOT NULL;
金仓返回结果:第2条和第3条,空串也算"不为空"。
看到没有?在金仓里,空串和NULL是完全分开的。空串有值,所以IS NOT NULL能查到;NULL没值,所以IS NULL才能查到。
而在Oracle里,第一条和第二条都会被当成NULL处理,WHERE remark = ''能查出两条,WHERE remark IS NULL也能查出两条。
这就是差异的根源。
三、第二个大坑:判断空值别用=,要用IS NULL
讲完了空串和NULL的区别,我们再来讲第二个高频坑:判断一个值是不是空,到底该怎么写。
3.1 新手最常犯的错:用= NULL判断空值
我见过太多开发,写判断空值的SQL,随手就写WHERE field = NULL。
写完还觉得挺对,等于空嘛,多直观。
结果一执行,一条数据都查不出来。
然后就懵了:表里明明有NULL值啊,怎么查不出来?是不是数据库有bug?
还真不是数据库的问题,是SQL的三值逻辑在起作用。
3.2 什么是SQL的三值逻辑
大多数人熟悉的逻辑,是二值逻辑:真和假,是与非。
但SQL里不是这样。SQL的布尔值有三种:真(TRUE)、假(FALSE)、未知(UNKNOWN)。
为什么要有第三种?因为有NULL的存在。
NULL代表未知、不存在。你拿一个已知的值去跟未知的值比较,结果自然也是未知的。
比如:
-
1 = 1→ 真 -
1 = 2→ 假 -
1 = NULL→ 未知 -
NULL = NULL→ 未知(两个未知的东西,你不知道它们等不等)
而WHERE条件的规则是:只有结果为"真"的行,才会被保留。假和未知,都会被过滤掉。
所以你写WHERE field = NULL,不管field是不是NULL,比较的结果都是未知,所有行都会被过滤掉,自然一条都查不出来。
这不是金仓独有的规则,这是SQL标准规定的,所有遵循标准的数据库都是这样。
那为什么有些人在某些数据库里写= NULL好像能查到数据?要么是那个数据库做了特殊兼容,要么是你记错了,其实写的是空串。
3.3 正确姿势:用IS NULL / IS NOT NULL
判断一个值是不是NULL,正确的写法只有一种:
SQL
WHERE field IS NULL;
判断不是NULL,就写:
SQL
WHERE field IS NOT NULL;
没有第二种写法。
不要用= NULL,不要用<> NULL,不要用!= NULL,这些写法的结果都是未知,永远查不出你想要的数据。
这是SQL最基础的知识点,但也是最容易被忽略的知识点。我面试过很多工作三四年的开发,连这个都搞不清楚,你说写出来的SQL能不出问题吗?
3.4 延伸:空值参与的运算,结果全是NULL
三值逻辑带来的另一个坑,就是NULL参与运算的问题。
任何数值和NULL做加减乘除,结果都是NULL;
任何字符串和NULL做拼接,结果都是NULL;
任何函数,只要参数里有NULL,结果大概率也是NULL。
举个例子:
SQL
SELECT 100 + NULL; -- 结果是NULL,不是100
SELECT 'hello' || NULL; -- 结果是NULL,不是'hello'
这个坑在统计计算里特别常见。比如你算员工的总收入,工资加奖金,要是奖金字段是NULL,那算出来的总收入就是NULL,而不是工资本身。
财务报表里要是有这么一个计算,那数就全乱了。
所以做计算、做拼接的时候,遇到可能为NULL的字段,一定要先用空值处理函数把它转成默认值。后面我会专门讲用什么函数。
四、第三个大坑:兼容模式下的空字符串问题
接下来这个坑,知道的人就更少了。很多人用了好几年金仓,都不知道还有这么个参数、这么种模式。
4.1 场景引入:为什么有的金仓环境里,空串和NULL是一回事?
我之前遇到过一个挺有意思的事。同一个项目,测试环境和生产环境都是金仓,版本也一样,可同一条SQL,两边执行结果不一样。
测试环境里,WHERE remark = ''能查出NULL值的数据;生产环境里,查不出来。
开发当时都快疯了,说"一样的数据库,怎么结果还能不一样?"
我去查了两边的参数配置,很快就找到了原因:测试环境开了一个兼容参数,生产环境没开。
这个参数就是ora_input_emptystr_isnull。
4.2 什么是ora_input_emptystr_isnull
这是金仓专门为了兼容Oracle迁移场景提供的一个参数,名字里的ora就是Oracle的缩写。
它的作用很简单:开启之后,往字符类型字段里插入空字符串 **''**的时候,数据库会自动把它转换成NULL再存进去。
也就是说,开启这个参数之后,你插进去的是空串,存到库里的是NULL,行为跟Oracle一模一样。
这样一来,从Oracle迁过来的数据和SQL,行为就基本一致了,不用大改。
但这个参数也带来了很多迷惑性。很多人不知道有这个参数,不同环境配置不一样,就会出现"同库不同命"的诡异现象。
4.3 参数的两种取值与行为对比
这个参数有两个值:on和off,默认是off。
当参数值为off时(默认状态):
-
插入空字符串,存进去的就是空字符串;
-
空串和NULL是两个完全不同的值;
-
WHERE field = ''只能查出空串的行,查不出NULL的行; -
WHERE field IS NULL只能查出NULL的行,查不出空串的行; -
LENGTH('')返回0。
当参数值为on时(Oracle兼容模式):
-
插入空字符串,数据库自动转换成NULL存储;
-
库里实际上不存在真正的空字符串,所有空串都变成了NULL;
-
WHERE field = ''等价于WHERE field IS NULL,能查出NULL的行; -
LENGTH('')返回NULL。
你看,就这一个参数,整个空值的行为逻辑全变了。
4.4 怎么查看和设置这个参数
查看当前参数值:
SQL
SHOW ora_input_emptystr_isnull;
会话级临时设置(当前连接生效):
SQL
SET ora_input_emptystr_isnull = on;
SET ora_input_emptystr_isnull = off;
全局永久设置:
修改金仓的核心配置文件sys_kingbase.conf,添加或修改这一行:
Properties
ora_input_emptystr_isnull = on
修改完之后,需要重启数据库服务才能生效。
五、第四个大坑:空值排序、分组、聚合的各种坑
空值的坑远不止查询判断,在排序、分组、聚合这些场景里,一样有很多容易踩的雷。
5.1 排序时的NULL位置:到底在前还是在后
这也是迁移时高频出现的问题。同样的ORDER BY排序,两边结果顺序不一样,分页的时候就会出现数据重复、遗漏,用户体验特别差。
为什么会这样?因为SQL标准里只规定了排序的规则,但没有规定NULL值应该排在最前面还是最后面。这个是数据库厂商自己决定的。
Oracle里,升序排序的时候,NULL默认排在最后面;
金仓里,升序排序的时候,NULL默认排在最前面。
两边默认行为刚好反过来。
如果你写SQL的时候依赖默认排序,不指定NULL的位置,迁过来之后顺序肯定不一样。
解决方案也很简单:显式指定NULL的位置。
金仓支持标准的NULLS FIRST和NULLS LAST语法:
SQL
-- 升序,NULL放最后(对齐Oracle行为)
SELECT * FROM sys_employee ORDER BY entry_time ASC NULLS LAST;
-- 升序,NULL放最前
SELECT * FROM sys_employee ORDER BY entry_time ASC NULLS FIRST;
显式写出来,不管什么数据库、什么版本,排序结果都一致。这也是分页查询的最佳实践。
5.2 分组聚合时的NULL处理
GROUP BY分组的时候,所有NULL值会被分到同一组。
这个两边行为是一致的,一般不会出问题。
但聚合函数就不一样了。
所有聚合函数(SUM、AVG、COUNT、MAX、MIN),计算的时候都会忽略NULL值。
比如SUM一个字段,NULL值的行不会被算进去;AVG也是一样,只算有值的行的平均值。
这个行为大多数数据库都是一致的,但还是有一些细节差异。
比如COUNT(*)和COUNT(字段)的区别:
-
COUNT(*):统计所有行数,包括NULL的行;
-
COUNT(field):统计字段不为NULL的行数,NULL的行不算。
很多人搞不清这两个的区别,统计出来的数永远对不上。
这里再强调一遍:要统计总行数,就用COUNT(*);要统计某个字段有值的行数,就用COUNT(字段)。别混着用。
5.3 空值拼接:整个字符串都没了
字符串拼接也是重灾区。
按照SQL标准,任何字符串和NULL拼接,结果都是NULL。
比如:
SQL
SELECT '姓名:' || NULL; -- 结果是NULL
但很多传统数据库不是这样的。有的数据库把NULL当空串处理,拼接出来就是正常的字符串;有的数据库用CONCAT函数的时候会自动忽略NULL。
迁到金仓之后,按标准来,一拼就是NULL,报表里一片空白,业务部门以为数据丢了。
解决方案:拼接前先用COALESCE把NULL转成空串。
SQL
SELECT '姓名:' || COALESCE(emp_name, '');
COALESCE函数的作用是:如果第一个参数是NULL,就返回第二个参数;否则返回第一个参数。
把NULL都转成空字符串,再拼接就不会出问题了。
六、迁移时空值问题的完整处理方案
讲完了所有的坑,我们来讲讲怎么系统性地解决空值问题。我结合自己的项目经验,整理了一套完整的处理方案,从迁移前到迁移后,全流程覆盖。
6.1 迁移前:先搞清楚两边的差异,定好统一策略
不要上来就导数据、改SQL。先做两件事:
第一件事:评估源库的空值现状
-
源库是空串和NULL等价,还是分开的?
-
业务库里,有多少字段存了空串?有多少字段存了NULL?
-
哪些SQL里用了
= ''的写法?哪些用了IS NULL?
先摸底,搞清楚现状,才知道后面要改多少东西。
第二件事:定好目标库的空值策略
金仓提供了两种模式,你得选一种:
方案A:开启ora_input_emptystr_isnull,对齐Oracle行为
适合场景:Oracle迁移项目,历史SQL太多,改不过来,项目时间紧。
优点:改动小,快速上线,兼容旧系统。
缺点:不是金仓原生行为,依赖兼容参数;以后换环境、换版本可能又出问题。
方案B:保持默认关闭,严格区分空串和NULL
适合场景:新项目、愿意规范代码的团队。
优点:符合SQL标准,逻辑清晰,不依赖兼容参数,长期更稳定。
缺点:需要改一批历史SQL和代码。
我的建议是:如果是老系统迁移、时间紧,可以先开参数过渡;但长期来看,还是建议逐步整改,回归标准写法,彻底摆脱对兼容参数的依赖。
6.2 迁移中:数据清洗+SQL整改
策略定好了,就开始落地。
数据层面:统一空值格式
-
如果选了Oracle兼容模式:把库里已有的空字符串全部更新成NULL,统一格式,避免既有空串又有NULL的混乱状态。
SQLUPDATE sys_table SET field = NULL WHERE field = ''; -
如果选了标准模式:把库里的NULL值,根据业务含义,该转空串的转空串,该转默认值的转默认值,统一规范。
SQL层面:整改不规范写法
-
所有
= ''的判断,根据业务逻辑,改成IS NULL或者保留= ''; -
所有
= NULL的写法,全部改成IS NULL; -
所有字符串拼接、数值计算,涉及可能为NULL的字段,都加上空值处理函数;
-
所有排序语句,显式加上NULLS FIRST/LAST,不依赖默认行为。
6.3 迁移后:全量结果比对+持续巡检
迁移完不是就完事了,一定要做验证。
第一,做全量结果比对
核心业务SQL,分别在源库和目标库执行,比对结果集的行数、字段值、聚合结果。重点查那些涉及空值判断、空值计算的SQL,确保两边一致。
第二,建立空值巡检机制
定期巡检库里有没有新插入的空串、有没有不规范的空值判断SQL,发现问题及时整改。
不要等出了事故再去查,主动巡检,把问题消灭在萌芽状态。
七、金仓常用空值处理函数大全
最后,给大家整理几个金仓里最常用的空值处理函数,都是日常开发必备的,建议收藏。
7.1 COALESCE:最通用的空值替换函数
这是我最推荐的一个函数,标准SQL函数,所有数据库都支持,可移植性最好。
SQL
COALESCE(value, default_value)
作用:如果value是NULL,就返回default_value;否则返回value本身。
可以传多个参数,返回第一个非NULL的值:
SQL
COALESCE(a, b, c, 0)
从左到右找,找到第一个不是NULL的就返回。
常用场景:
-
数值计算前把NULL转成0:
COALESCE(amount, 0) -
字符串拼接前把NULL转成空串:
COALESCE(name, '') -
多个字段取优先值:
COALESCE(mobile, phone, tel)
7.2 NVL:Oracle风格的空值函数
SQL
NVL(value, default_value)
作用和COALESCE两个参数的时候一样,都是空值替换。
区别是NVL是Oracle的传统函数,COALESCE是标准SQL函数。
金仓两个都支持,建议优先用COALESCE,更标准。
7.3 NULLIF:两个值相等就返回NULL
SQL
NULLIF(value1, value2)
作用:如果value1等于value2,就返回NULL;否则返回value1。
这个函数用得少,但某些场景特别好用。比如你想把空字符串转成NULL:
SQL
SELECT NULLIF(remark, '') FROM sys_voucher;
如果remark是空串,就返回NULL,否则返回原值。
正好可以用来处理那些空串转NULL的场景。
7.4 空值判断函数
还有一些判断空值的函数,比如:
-
ISNULL(value):如果是NULL返回true,否则返回false(注意这是布尔值) -
ISNOTNULL(value):如果不是NULL返回true,否则返回false
这些函数可以用,但我还是建议用标准的IS NULL和IS NOT NULL语法,更通用,可读性也更好。
八、总结
很多团队做迁移,注意力全放在大的地方:架构怎么搭、数据怎么导、性能怎么调。这些当然重要,但真正能让项目翻车的,往往都是一些不起眼的小细节------一个空值、一个排序、一个隐式转换。
这些东西太小了,小到没人会专门去评估;可它们的影响又太大了,大到能让整个项目延期、返工,甚至验收不过。空值就是这些小细节里最典型的一个,基础到大家都觉得这有什么好讲的;可它又太容易出问题了,稍微不注意就是一个大坑。电科金仓在空值处理上的设计是严谨的,也是符合SQL标准的。它没有为了兼容而妥协,而是坚持标准,把选择权交给用户------你要兼容,我给你提供参数;你要标准,默认就是标准。这是一种很成熟的设计思路,但作为使用者真正理解了底层逻辑,才能写出稳定、可靠、可维护的代码。