从DB模型中消除NULLable列的选项(为了避免SQL的三值逻辑)?

时间:2010-06-20 16:01:22

标签: sql null relational-database three-valued-logic

不久前,我一直在阅读这本书SQL and Relational Theory by C. J. Date。作者因批评SQL的三值逻辑(3VL)而闻名。 1)

作者强调了为什么在SQL中应该避免使用3VL,但是他没有概述如果不允许可以为空的列,数据库模型会是什么样子。我已经考虑了一点,并提出了以下解决方案。如果我错过了其他设计方案,我想听听他们的意见!

1) 日期对SQL 3VL的批评反过来也受到了批评:见this paper by Claude Rubinson(包括CJ Date的原始批评)。


示例表

例如,请使用下表,我们有一个可为空的列(DateOfBirth):

#  +-------------------------------------------+
#  |                   People                  |
#  +------------+--------------+---------------+
#  |  PersonID  |  Name        |  DateOfBirth  |
#  +============+--------------+---------------+
#  |  1         |  Banana Man  |  NULL         |
#  +------------+--------------+---------------+

选项1:通过标记和默认值模拟NULL

不是使列可为空,而是指定任何默认值(例如1900-01-01)。其他BOOLEAN列将指定是否应忽略DateOfBirth中的值或是否实际包含数据。

#  +------------------------------------------------------------------+
#  |                              People'                             |
#  +------------+--------------+----------------------+---------------+
#  |  PersonID  |  Name        |  IsDateOfBirthKnown  |  DateOfBirth  |
#  +============+--------------+----------------------+---------------+
#  |  1         |  Banana Man  |  FALSE               |  1900-01-01   |
#  +------------+--------------+----------------------+---------------+

选项2:将可为空的列转换为单独的表:

可以为空的列替换为新表(DatesOfBirth)。如果记录没有该列的数据,则新表中将不会有记录:

#  +---------------------------+ 1    0..1 +----------------------------+
#  |         People'           | <-------> |         DatesOfBirth       |
#  +------------+--------------+           +------------+---------------+
#  |  PersonID  |  Name        |           |  PersonID  |  DateOfBirth  |
#  +============+--------------+           +============+---------------+
#  |  1         |  Banana Man  |
#  +------------+--------------+

虽然这似乎是更好的解决方案,但这可能会导致需要为单个查询加入许多表。由于不允许使用OUTER JOIN(因为它们会将NULL引入结果集中),因此可能不再像以前那样只使用一个查询来获取所有必需的数据。


问题: 是否有其他选项可以消除NULL(如果是,它们是什么)?

7 个答案:

答案 0 :(得分:4)

我看到Date的同事Hugh Darwen在一篇精彩的演讲“如何处理不使用NULL的信息”中讨论了这个问题,这可以在the Third Manifesto website上找到。

他的解决方案是第二种方法的变体。它是第六种常规形式,其中包含用于保存出生日期和未知标识符的表格:

#  +-----------------------------+ 1    0..1 +----------------------------+
#  |         People'             | <-------> |         DatesOfBirth       |
#  +------------+----------------+           +------------+---------------+
#  |  PersonID  |  Name          |           |  PersonID  |  DateOfBirth  |
#  +============+----------------+           +============+---------------+
#  |  1         |  Banana Man    |           ! 2          | 20-MAY-1991   |
#  |  2         |  Satsuma Girl  |           +------------+---------------+
#  +------------+----------------+
#                                  1    0..1 +------------+
#                                  <-------> | DobUnknown |
#                                            +------------+
#                                            |  PersonID  |
#                                            +============+
#                                            | 1          |
#                                            +------------+

从人物中选择然后需要加入所有三个表格,包括样板表示未知的出生日期。

当然,这在某种程度上是理论上的。这些天的SQL状态仍然不够先进,无法处理所有这些。休的介绍涵盖了这些缺点。他提到的一件事并不完全正确:SQL的某些版本确实支持多个赋值 - 例如Oracle's INSERT ALL syntax

答案 1 :(得分:2)

我建议你选择2.我非常肯定Chris Date也会因为你正在做的事情完全正常化6NF,这是Date was jointly responsible for introducing最高的正常形式。 我推荐Darwen's paper处理缺失信息。

  

由于不允许OUTER JOIN(因为它们会引入NULL)   进入结果集),可能不再需要所有必要的数据   像以前一样只用一个查询获取。

......事实并非如此,但我同意Darwen文件中没有明确提到外连接的问题;这是让我想要的一件事。明确的答案可以在Date的另一本书中找到......

首先,请注意Date和Darwen自己的真正关系语言Tutorial D只有一种连接类型是自然连接。理由是实际上只需要一种连接类型。

我提到的日期书是优秀的SQL and Relational Theory: How to Write Accurate SQL Code

  

4.6:关于外部联接的评论:“从关系上讲,[外部联接]是a   一种猎枪婚姻:它迫使桌子成为一种联盟 - 是的,我   表示联盟,而不是加入 - 即使有问题的表没有   符合工会的通常要求......它是这样做的   在执行之前,通过使用空值填充一个或两个表来实现效果   结合,从而使它们符合那些通常的要求   毕竟。但是没有理由不应该填充填充   使用适当的值而不是空值

使用您的示例和默认值“1900-01-01”作为“填充”,外部联接的替代方法可能如下所示:

SELECT p.PersonID, p.Name, b.DateOfBirth
  FROM Person AS p
       INNER JOIN BirthDate AS b
          ON p.PersonID = b.PersonID
UNION
SELECT p.PersonID, p.Name, '1900-01-01' AS DateOfBirth
  FROM Person AS p
 WHERE NOT EXISTS (
                   SELECT * 
                     FROM BirthDate AS b
                    WHERE p.PersonID = b.PersonID
                  );

Darwen的论文提出了两个明确的表格,比如BirthDateBirthDateKnown,但SQL不会有太大不同,例如:半连接到BirthDateKnown代替上面BirthDate的半差异。

请注意,上述内容仅使用JOININNER JOIN,因为标准SQL-92 NATURAL JOINUNION CORRESPONDING并未在现实生活中的SQL产品中广泛实现(无法找到引用但是IIRC Darwen主要负责后两者进入标准。

进一步注意,上面的语法看起来很啰嗦,因为SQL一般都是啰嗦。在纯关系代数中,它更像是(伪代码):

Person JOIN BirthDate UNION Person NOT MATCHING BirthDate ADD '1900-01-01' AS DateOfBirth;

答案 2 :(得分:1)

我还没看过,但有一篇名为如何使用S-by-C处理丢失信息的文章在由Hugh Darwen和CJ Date运营的Third Manifesto网站上。 这不是由C.J.Date写的,但我认为因为它是该网站上的一篇文章,它可能与他的观点相似。

答案 3 :(得分:0)

一种替代方案可能是entity-attribute-value模型:

 entity  attribute    value
 1       name         Banana Man
 1       birthdate    1968-06-20

如果生日是未知的,你只需省略它的行。

答案 4 :(得分:0)

选项3:记录作者的负责人:

CREATE TABLE Person
(
  PersonId int PRIMARY KEY IDENTITY(1,1),
  Name nvarchar(100) NOT NULL,
  DateOfBirth datetime NOT NULL
)

为什么要扭曲模型以在您的目标是消除它们时允许空表示?

答案 5 :(得分:0)

您可以使用COALESCE删除输出中的null

SELECT personid  /*primary key, will never be null here*/
       , COALESCE(name, 'no name') as name
       , COALESCE(birthdate,'no date') as birthdate
FROM people

并非所有数据库都支持COALESCE,但几乎所有数据库都有一个名为
的后备选项 IFNULL(arg1, arg2)或类似的东西,它们将执行相同的(但仅限于2个参数)

答案 6 :(得分:0)

一种选择是使用显式option types,类似于Haskell的Maybe仿函数。

不幸的是,很多现有的SQL实现对用户定义的代数数据类型的支持很少,甚至对用户定义的类型构造函数的支持也更差,你真的需要干净利落地完成这些工作。

这只为那些明确要求它的属性恢复了一种“null”,但没有null愚蠢的三值逻辑。 Nothing == NothingTrue,不是unknownnull

当有几个缺少信息的原因时,对用户定义的代数类型的支持也会有所帮助,例如,等效于以下Haskell类型的数据库对于明显的应用程序来说是一个很好的解决方案:

data EmploymentStatus = Employed EmployerID | Unemployed | Unknown

(当然,支持这一点的数据库也需要支持随之而来的更复杂的外键约束。)

如果没有这个,我同意APConedaywhen关于6NF的答案。