空值的问题

空值的问题1

首先看看空值的含义。空值(NULL)在关系模型中可以有两种定义:1、空值表示可以给该列赋值,但是目前还没有赋值;2、空值表示该列不可以被赋值。RDBMS支持的一般都是第一种类型,SQL标准也是这样定义的。

NULL引起的EXISTS子句错误

1989年9月,C.J.Date在“Database Programming & Design”上发表了题为“Beware of SQL EXISTS!”的文章,用例子说明了SQL标准定义的EXISTS语句存在问题(大家不妨用这个例子来检查一下目前的数据库系统是否还存在如此的问题)。该例子用到了供应商-供应零件表sp,表中只有三条记录:

sno pno qty
--- --- ----
S1 P1 NULL
S2 P1 200
S3 P1 1000

这个表的意思很明白,就是供应商(SNO)对每种零件(PNO)提供了多少(QTY)。记录(S1, P1, NULL)表明供应商S1提供了零件P1,但是具体的数目不清楚。

DATE给出了下面的查询:查找供应了零件P1但是供应数不包括1000的供应商。很明显,正确的查询结果应该是一条记录S2,只有供应商S2满足条件。对应的SQL语句可以是:

SELECT DISTINCT spx.sno
FROM sp spx
WHERE spx.pno = 'P1' AND
  1000 NOT IN
    ( SELECT spy.qty
      FROM sp spy
      WHERE spy.sno = spx.sno AND
        spy.pno = 'P1');

注:假如表sp以(sno, pno)作为主键,则用不着上面的DISTINCT,SQL语句如果写成:

SELECT sno
FROM sp
WHERE pno = 'P1' AND qty != 1000;

返回的结果应该是(S1, S2),当然可以将上述的SQL语句修改成:

WHERE sno = 'P1' AND qty != 1000 AND qty IS NOT NULL ;   

此时返回结果只有S2。这个例子太特别,不知道有没有更具实践意义的好例子。

上述语句可以返回正确的结果S2,但是如果用EXISTS子句改写成“等价”形式:

SELECT DISTINCT spx.sno
FROM sp spx
WHERE spx.pno = 'P1' AND
NOT EXISTS
( SELECT spy.qty
  FROM sp spy
  WHERE spy.sno = spx.sno AND
    spy.pno = 'P1' AND
    spy.qty = 1000);

根据ANSI SQL的定义,结果应该是S1、S2。请看DATE的分析:假如主查询中spx=(S1, P1, NULL),则子查询的EXISTS子句逻辑上等价于:

(SELECT spy.qty
 FROM sp spy
 WHERE spy.sno = 'S1' AND
   spy.pno = 'P1' AND
   spy.qty = 1000)

这个子查询的结果是个空集,因此整个查询相当于:

SELECT DISTINCT spx.sno
FROM sp spx
WHERE spx.pno = 'P1'
  /* TRUE for spx = (S1,P1,1000) */
  AND TRUE

于是供应商S1也出现在了查询结果中,从而导致了错误。回顾一下上面的分析过程,容易发现EXISTS子句并不是产生问题的根源,错误源于WHERE子句对(spy.qty = 1000)的判断。存在NULL的逻辑判断应该采用三值逻辑,而ANSI SQL只定义了WHERE子句的二值逻辑,即WHERE子句只可能产生TRUE或者FALSE。在上述的例子中,WHERE子句对记录spy=(S1,P1,NULL)的判断结果应该是“未知”,而不是FALSE。既然NULL参与的比较是问题的源头,RDBMS可能导致的问题就远远不止DATE提出的那些了,比如:

1、不能正确的处理子查询

前面用了IN子句的查询可以产生正确的结果,但也可以将其改成一种错误的形式:

SELECT DISTINCT sno 
FROM sp
WHERE pno = 'P1' AND sno NOT IN 
  (SELECT sno
   FROM sp
   WHERE pno = 'P1' AND qty = 1000)

这个查询和用了EXISTS子句的查询存在同样的问题。因此用了子查询的SQL语句,只要子查询中表的某一列存在空值,都可能导致类似的问题。

2、即使用户给出了正确的SQL语句,试图避免上述问题,DBMS系统也可能产生错误结果。

因为DBMS系统一般要对用户给出的查询作出改写和优化,DATE给出的那个使用了IN子句的查询可能被优化器改写成带EXISTS子句的查询,这样又会产生错误。

3、ANSI SQL对HAVING子句的定义存在同样的问题。

同WHERE子句类似,HAVING子句采用的也是二值逻辑,只能取值TRUE或FALSE。而很多DBMS允许HAVING子句出现在子查询或视图定义中,这样也会导致类似的错误。

要解决上述的问题理论上看来并不是很复杂,只要对SQL的处理采用三值逻辑即可,但是这在工程实践中是需要认真对待的,并不是想象的那么简单。

NULL引起的争论

既然NULL会引起比较严重的问题,C.J.Date等人就认为数据库中应该取消NULL,而代以某个确切的值。反对者包括E.F.Codd等,他们认为NULL是数据库中必不可少的特性,它很好地反映了现实。这方面是不是有点象Dijkstra发起的编程语言中有关GOTO语句的争论?[2]

数据库中确实存在未知信息:在实际工作中存在很多情况,不能获得某些信息。例如单位的职工统计表中可能会存在某个职工(比如说是foo)的生日未知,在报表中可以用空白、短横线或者直接写上“未知”代替。在数据库系统中可能用空格等代替,这样应用程序就需要根据实际采用的替代值进行判断,首先这样做增加了程序的复杂性,其次还不能保证系统提供统一的解决方案。关系数据库解决这种问题的方法是用“NULL”表示未知值。即便如此,用SQL语句在数据库中查询foo的生日是否为1900年10月10日,返回的是“FALSE”而不是“未知”。

为了彻底解决上述的问题,关系模型引入了三值逻辑。一般的数据库条件操作结果只可能是“TRUE”或“FALSE”,但是对于存在NULL值的问题只能回答“未知”,而不是给出确切的回答。这种解决方案让人觉得有些不舒服,于是C.J.Date和David McGoveran等就建议用缺省值代替空值,但和三值逻辑相比,这种方法的表达能力还是欠佳。而且实际系统还没有哪个实现了他们的方案。倒是FirstSQL正确地实现了三值逻辑(这是FirstSQL自己的陈述,我没有证实此事,但也倾向于采用三值逻辑)。别的很多DBMS对NULL的实现,甚至ANSI SQL标准本身对NULL的定义都存在缺陷,对于具体的系统都应该实际测试验证一下。

参考文献

  1. http://www.firstsql.com
  2. http://www.dbdebunk.com
  3. C. J. Date, An Introduction to Database (Eighth Edition), Addison-Wesley
[ SQL ]
Written on March 4, 2003