复合主键中的可为空的列怎么了?


149

ORACLE不允许在任何包含主键的列中使用NULL值。似乎大多数其他“企业级”系统也是如此。

同时,大多数系统还允许在可为空的列上使用唯一约束。

为什么唯一约束可以具有NULL但主键不能具有NULL?是否有根本的逻辑原因,还是更多的技术限制?


Answers:


216

主键用于唯一标识行。这是通过将键的所有部分与输入进行比较来完成的。

根据定义,NULL不能成为成功比较的一部分。甚至与自己进行比较(NULL = NULL)也会失败。这意味着包含NULL的键将不起作用。

另外,外键中允许使用NULL来标记可选关系。(*)在PK中也允许它会破坏这一点。


(*)注意:具有可空的外键并不是干净的关系数据库设计。

如果有两个实体AB并且A可以选择与关联B,那么干净的解决方案是创建一个解析表(假设AB)。该表将连接AB:如果一个关系那么它将包含一个记录,如果不是那就不是。


5
我已经更改了对此答案的接受范围。从票数来看,这个答案对更多人来说是最清楚的。我仍然感到,托尼·安德鲁斯(Tony Andrews)的回答更好地说明了该设计的意图。还要检查一下!
罗曼·斯塔科夫

2
问:什么时候需要NULL FK而不是缺少行?答:仅在为优化而规范化的架构版本中。在非平凡的模式中,每当需要新功能时,诸如此类的非标准化问题都可能导致问题。otoh,网页设计的人群不在乎。我至少对此要加一个注意事项,而不是使其听起来像是一个好的设计思想。
zxq9

3
“具有可为空的外键并不干净的关系数据库设计。” -无空数据库设计(第六种标准形式)总是增加复杂性,实现这些收益所需的额外程序员工作通常不超过所节省的空间。

1
如果它是ABC分辨率表怎么办?带有可选的C
Bart Calixto

1
我试图避免写“因为该标准禁止使用它”,因为这实际上没有解释。
托玛拉克

62

主键为每个定义一个唯一的标识符表中的行:当表具有主键时,您可以采用保证的方式从表中选择任何行。

唯一约束不一定标识每行;它只是指定如果一行的列中有值,它们必须是唯一的。这不足以唯一地标识每一行,这是主键必须执行的操作。


10
在Sql Server中,具有可空列的唯一约束仅允许该列中的值“空”一次(给该约束的其他列赋予相同的值)。因此,这种唯一约束的行为本质上类似于带有可空列的pk。
杰拉德(Gerard)

我对甲骨文(11.2)确认相同
亚历山大·马拉霍夫

2
在Oracle(我不了解SQL Server)中,表可以包含许多行,其中唯一约束中的所有列均为空。但是,如果唯一性约束中的某些列不为空,而某些列为空,则将强制执行唯一性。
托尼·安德鲁斯

这如何适用于复合UNIQUE?
Dims 2014年

1
@Dims与SQL数据库中的几乎所有其他内容一样,“取决于实现”。在大多数数据库中,“主键”实际上是下面的唯一约束。“主键”的概念实际上并不比UNIQUE的概念特别特殊或强大。真正的区别在于,如果表有两个独立的方面可以保证是唯一的,那么按定义就没有标准化的数据库(您将两种类型的数据存储在同一表中)。
zxq9

46

从根本上说,多列主键中的NULL没什么不对。但是,具有影响力的设计人员可能没有想到,这就是为什么许多系统在尝试这种方法时都会引发错误。

考虑存储为一系列字段的模块/软件包版本的情况:

CREATE TABLE module
  (name        varchar(20) PRIMARY KEY,
   description text DEFAULT '' NOT NULL);

CREATE TABLE version
  (module      varchar(20) REFERENCES module,
   major       integer NOT NULL,
   minor       integer DEFAULT 0 NOT NULL,
   patch       integer DEFAULT 0 NOT NULL,
   release     integer DEFAULT 1 NOT NULL,
   ext         varchar(20),
   notes       text DEFAULT '' NOT NULL,
   PRIMARY KEY (module, major, minor, patch, release, ext));

主键的前5个元素是发行版本中定期定义的部分,但是某些软件包具有定制的扩展名,通常不是整数(例如“ rc-foo”或“ vanilla”或“ beta”或其他用于谁四个场是不够的可能梦想)。如果软件包没有扩展名,则在上述模型中为NULL,以这种方式放置东西不会造成任何伤害。

但是什么 NULL?它应该表示缺乏信息,一个未知数。也就是说,也许这更有意义:

CREATE TABLE version
  (module      varchar(20) REFERENCES module,
   major       integer NOT NULL,
   minor       integer DEFAULT 0 NOT NULL,
   patch       integer DEFAULT 0 NOT NULL,
   release     integer DEFAULT 1 NOT NULL,
   ext         varchar(20) DEFAULT '' NOT NULL,
   notes       text DEFAULT '' NOT NULL,
   PRIMARY KEY (module, major, minor, patch, release, ext));

在此版本中,元组的“ ext”部分不是NOT NULL,但默认为空字符串-在语义(和实际上)上与NULL不同。NULL是未知数,而空字符串是“不存在的内容”的故意记录。换句话说,“空”和“空”是不同的东西。它与“我在这里没有价值”和“我不知道这里的价值”之间的区别。

当您注册缺少版本扩展名的软件包时,您会知道它缺少扩展名,因此,空字符串实际上是正确的值。仅当您不知道它是否具有扩展名,或者您知道它具有扩展名但不知道它是什么时,NULL才是正确的。在以字符串值为标准的系统中,这种情况更容易处理,因为除了插入0或1之外,没有其他方法可以表示“空整数”,这将在以后进行的任何比较中汇总(本身的含义)*。

顺便说一句,这两种方法在Postgres中都是有效的(因为我们正在讨论“企业” RDMBS),但是当您将NULL放入混合中时,比较结果可能会有很大不同-因为NULL ==“不知道”,所以所有涉及NULL的比较结果最终为NULL,因为您无法了解未知的内容。危险!请仔细考虑:这意味着NULL比较结果通过一系列比较传播。排序,比较等时,这可能是一些细微错误的来源。

Postgres假设您已经成年,可以自己做出决定。Oracle和DB2假定您没有意识到自己在做愚蠢的事情并抛出错误。通常这是正确的事情,但并非总是如此- 在某些情况下,您可能实际上并不知道并且为NULL,因此,将行与未知元素放在同一行中是不可能的,这是正确的行为。

无论如何,您都应该努力消除整个模式中允许的NULL字段的数量,而且要加倍处理涉及主键一部分的字段。在绝大多数情况下,NULL列的存在表示未规范化(与故意取消规范化相反)的架构设计,在接受之前应进行认真思考。

[* 注意:可以创建一个自定义类型,该类型是整数的结合,而一个“底部”类型的语义上表示“空”,而不是“未知”。不幸的是,这在比较操作中引入了一些复杂性,通常,真正地正确键入类型在实践中不值得付出努力,因为一开始根本不应该允许您使用很多NULL值。话虽如此,RDBMS BOTTOM除了要NULL包含习惯性地将“无值”的语义与“未知值”的语义混为一谈之外,如果还包括默认类型,那就太好了。]


5
这是一个非常不错的答案,并解释了很多有关NULL值的信息,以及它在许多情况下的含义。先生,您现在有我的尊敬!甚至在大学时,我对数据库中的NULL值也没有很好的解释。谢谢!

我支持此答案的主要思想。但是这样写:“应该表示缺少信息,一个未知数”,“在语义上(实际上)不同于NULL”,“一个NULL是未知数”,“一个空字符串是故意记录着“不存在某物” “','NULL ==”不知道“',等是含糊和误导性的&实际上只是针对缺少语句的助记符,它们说明了如何使用NULL或任何值或将要使用或打算使用任何值(其余的内容) 。(包括启发SQL NULL功能的(坏的)设计。)它们没有任何理由或解释。他们应该被解释和揭穿。
philipxy

21

NULL == NULL-> false(至少在DBMS中)

因此,即使具有实际值的其他列也无法使用NULL值检索任何关系。


1
这听起来是最好的答案,但是我仍然不明白为什么在创建主键时禁止这样做。如果这只是一个检索问题,则可以使用where pk_1 = 'a' and pk_2 = 'b'普通值,并where pk_1 is null and pk_2 = 'b'在存在空值时切换到该值。
EoghanM

甚至更可靠,where (a.pk1 = b.pk1 or (a.pk1 is null and b.pk1 is null)) and (a.pk2 = b.pk2 or (a.pk2 is null and b.pk2 is null))/
Jordan Rieger

8
错误的答案。NULL == NULL->未知。不假。要注意的是,如果测试结果为UNKNOWN,则不认为违反约束。这通常使SEEM看起来好像比较结果为假,但实际上并非如此。
Erwin Smout,2015年

4

托尼·安德鲁斯(Tony Andrews)的回答很不错。但是真正的答案是,这已经是关系数据库社区所使用的约定,并不是必须的。也许这是一个很好的约定,也许不是。

将任何内容与NULL进行比较都会得出UNKNOWN(第三个真值)。因此,正如已经被废除的那样,所有关于平等的传统智慧都被排除在外了。乍一看就是这样。

但是我认为并非一定如此,甚至SQL数据库也认为NULL不会破坏所有进行比较的可能性。

在数据库中运行查询SELECT * FROM VALUES(NULL)UNION SELECT * FROM VALUES(NULL)

您看到的只是一个具有一个值为NULL的属性的元组。因此,联合在这里将两个NULL值视为相等。

将具有3个组成部分的组合键与具有3个属性(1、3,NULL)=(1、3,NULL)的元组进行比较时== 1 = 1 AND 3 = 3 AND NULL = NULL结果为UNKNOWN 。

但是我们可以定义一种新型的比较运算符,例如。==。X == Y <=> X = Y或(X是NULL并且Y是NULL)

拥有这种相等运算符将使具有空成分的复合键或具有空值的非复合键变得毫无问题。


1
不,UNION已将两个NULL识别为不明显的。与“等于”不是同一回事。尝试使用UNION ALL,您将获得两行。至于“新型比较运算符”,SQL已经有了。没有区别。但这本身还不够。在SQL结构(例如NATURAL JOIN或外键的REFERENCES子句)中使用此函数将需要这些结构上的其他选项。
Erwin Smout,2015年

啊哈,Erwin Smout。在这个论坛上也很高兴认识您!我不知道SQL的“ IS NOT DISTINCT FROM”。很有意思!但这似乎恰好是我的虚构==运算符的含义。您能否解释一下为什么这么说:“仅凭这一点是不够的”?
拉米·奥加雷斯

根据定义,REFERENCES子句基于相等性。根据对应的属性值不是DISTINCT而不是(更严格的)EQUAL,将子元组/行与父元组/行匹配的一种参考将需要能够指定此选项,但是语法不需要允许它。同理自然加入。
Erwin Smout,2015年

为了使外键起作用,所引用的对象必须是唯一的(即,所有值都必须是唯一的)。这意味着它可以有一个空值。如果将使用NOT DISTINCT运算符定义REFERENCES,则所有null值都可以引用单个null。我认为这样会更好(从某种意义上来说更有用)。使用JOIN(外部和内部),我认为严格的等值更好,因为当左侧的空值与右侧的所有空值匹配时,“ NULL MATCHES”将相乘。
拉米·奥加雷斯

1

我仍然相信这是技术性带来的基本/功能缺陷。如果您具有一个可选字段来标识客户,那么现在您必须在其中输入虚拟值,仅仅是因为NULL!= NULL,不是特别优雅,但这是“行业标准”

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.