查询反模式 - 正视NULL值
一、提出问题
不可避免地,我们都数据库总有一些字段是没有值的。不管是插入一个不完整的行,还是有些列可以合法地拥有一些无效值。SQL 支持一个特殊的空值,就是NULL。
在很多时候,NULL值导致我们的程序出现报错的现象,于是很多人就开始拒绝NULL值,想各种各样的方法来避免使用NULL值,但是很遗憾,NULL值恰恰就是满足我们的需要用于表示空值的。
空值经常存在于我们的数据库当中:
- 例如一个在职员工的离职时间。
- 例如一辆电力驱动的车的燃油消耗比。
二、反模式
很多人对于NULL值感觉到恐惧,原因在于不知道什么时候就会因为一个NULL冒出一个报错。实际上都是由于对NULL值的理解有误引起的。很多人将NULL值当做一个0、False、空字符串来理解。实际上SQL将NULL当做一个特殊的值,并不同于0、false、空字符串。
实际上对NULL值最好的理解是“不知道”。用“不知道”可以正确理解与NULL值的运算,如"+","-",AND,OR,NOT等。
我们来看看容易出错的地方。
首先,我们建一张表如下:
在里面添加几条数据:
注意里面的NULL值。
我们,来看看如下SQL语句的结果:
以上可以理解为:不知道+10=不知道
留意到上面的数据少了一条,Age为NULL的那一条,原因在于NOT (NULL)的值并不为True也并不为False而是NULL。
以上的例子还有很多,于是就出现了很多人引用一个普通的值来代替NULL值,例如"无效"、"未知"或者-1。假设我们使用的是-1,虽然我们在使用
SELECT * FROM Person WHERE Age <> -1
作为查询条件,看起来没什么问题,但是这时候当我们使用SUM、AVG等聚合函数的时候就会导致计算结果出错。
使用NULL并不是反模式,反模式是将NULL作为一个普通值处理或者使用一个普通的值来取代NULL的作用。
三、解决方案 - 将NULL视为特殊值
下面先列举一些程序员运用NULL运算时期望得到的结果与实际结果。
1、在标量表达式中使用NULL
表达式 | 期望值 | 实际值 | 原因 |
NULL=0 | TRUE | NULL | NULL不是0 |
NULL=12345 | FALSE | NULL | 未指定值不知道是否等于所给值 |
NULL<>12345 | TRUE | NULL | 未指定值也不知道一定不等于所给值 |
NULL+12345 | 12345 | NULL | NULL不是0 |
NULL||'string' | 'string' | NULL | NULL不是空字符串 |
NULL=NULL | TRUE | NULL | 两个都不知道,鬼知道你等不等 |
NULL!=NULL | FALSE | NULL | 两个都不知道,贵知道你等不等 |
2、在布尔表达式中使用NULL
表达式 | 期望值 | 实际值 | 原因 |
NULL AND TRUE | FALSE | NULL | NULL不是FALSE |
NULL AND FALSE | FALSE | FALSE | FALSE AND 什么都是FALSE |
NULL OR FALSE | FALSE | NULL | NULL不是FALSE |
NULL OR TRUE | TRUE | TRUE | TRUE OR 什么都是TRUE |
NOT (NULL) | TRUE | NULL | NULL 不是FALSE |
3、检索NULL值
由于=NULL或者不等于NULL操作在对NULL进行比较时都是返回NULL,因此在检索NULL的时候,要用写特别操作:
IS NULL 和 IS NOT NULL
对于上面的例子,如果我们希望检索NULL,可以这样写:
4、声明NOT NULL列
如果NULL会破坏程序结构或者NULL本身就是毫无意义的,那么最好就在定义列时加上NOT NULL约束。让数据库来帮你确保约束的实行比自己写代码可靠得多。
有人建议为每一列都定义一个DEFAULT值,这样一来当在执行插入操作时,即使省略了某一列,也能获得一个非NULL值。这样的建议也并不是通用的。就假设有一个年龄列,设置了一个Default值为0,那么使用AVG聚合函数的时候结算的是错误的结果。NULL值是不被纳入AVG的计算范畴之内的,而0会被计算。
如对于上表:
而,如果我们将最后两行设置为0,那么AVG的结果将是:
5、动态默认值
动态默认值这个东西的意思是,当我们的查询碰到一个NULL值的时候,我们希望它返回一个非NULL的默认值,已不至于计算出错。
比如 姓 + NULL + 名返回的是NULL。
因此,我们不希望中间返回NULL,而是''空字符串。这样计算才正常。
大部分数据库都提供了一个COALESCE函数实现这个功能,来看SQLServer中的示例:
我们将第一行的姓名置NULL