复合主键中的可空列有什么问题?

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

同时,大多数系统也允许在可为空的列上使用 独一无二约束。

为什么唯一约束可以有 NULL,而主键不能?这是否有一个基本的逻辑原因,或者这更多的是一个技术上的限制?

127555 次浏览

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

根据定义,NULL 不能成为成功比较的一部分。甚至与自身的比较(NULL = NULL)也会失败。这意味着包含 NULL 的密钥将无法工作。

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


(*) 提醒一句: 拥有可为空的外键并不是干净的关系数据库设计。

如果有两个实体 AB,其中 A可以选择性地与 B相关联,那么干净的解决方案是创建一个分辨率表(比如说 AB)。该表将 AB链接: 如果有 一个关系,那么它将包含一个记录,如果有 不是那么它不会。

一个主键为表中的 每个行定义了一个唯一标识符: 当一个表有一个主键时,你可以保证从中选择任何一行。

唯一约束不一定标识每一行; 它只是指定 如果行的列中有值,那么行的值必须是唯一的。这不足以惟一地标识 每个行,这是主键必须做的事情。

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

因此,即使使用带有实际值的附加列,也不能使用 NULL 值检索任何关系。

从根本上说,在多列主键中使用 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”或者“香草”或者“ beta”,或者任何其他 字段不足以满足的人可能会梦想的扩展)。如果一个包没有扩展名,那么在上面的模型中它是 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));

在这个版本中,tuple 的“ ext”部分不是 NULL,而是默认为空字符串——这在语义上(实际上)不同于 NULL。NULL 是一个未知数,而空字符串是对“某些不存在的东西”的刻意记录。换句话说,“空”和“空”是不同的东西。它是“我在这里没有值”和“我不知道这里的值是什么”之间的区别

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

顺便说一句,这两种方式在 Postgres 中都是有效的(因为我们讨论的是“企业”RDBMS) ,但是当你在混合中加入一个 NULL 时,比较结果可能会有很大的不同——因为 NULL = = “不知道”,所以所有涉及 NULL 的比较结果都会变成 NULL,因为你不可能知道某些未知的东西。仔细考虑一下: 这意味着通过一系列的比较,NULL 比较得到 繁殖。在排序、比较等时,这可能是一个微妙错误的来源。

Postgres 假设你是一个成年人,可以自己做这个决定。Oracle 和 DB2假设您没有意识到自己在做一些愚蠢的事情,并抛出一个错误。这是 通常正确的做法,但并不总是正确的——在某些情况下,事实上可能不知道,并且有一个 NULL,因此留下一行未知的元素,不可能进行有意义的比较,这是正确的行为。

在任何情况下,您都应该努力消除整个模式中允许的 NULL 字段的数量,当涉及到属于主键的字段时,应该加倍努力消除这些字段的数量。在大多数情况下,NULL 列的存在表明模式设计是非规范化的(而不是故意的非规范化) ,在被接受之前应该仔细考虑。

[ * 注意: 可以创建一个自定义类型,该类型是整数的并集,而“ bottom”类型在语义上意味着“空”,而不是“未知”。不幸的是,这在比较操作中引入了一点复杂性,而且通常真正的类型正确并不值得在实践中付出努力,因为首先不应该允许使用很多 ABC0值。也就是说,如果 RDBMS 除了 ABC0之外还包含一个默认的 BOTTOM类型,那就太好了,这样可以防止随意地将“无值”语义与“未知值”语义混为一谈的习惯。]

托尼•安德鲁斯(Tony Andrews)给出了一个不错的答案。但是真正的答案是,这已经成为关系数据库社区的惯例,并不是必须的。也许这是一个很好的会议,也许不是。

将任何内容与 NULL 进行比较将得到 UNKNOWN (第3个真值)。 因此,所有关于平等的传统智慧都被抛弃了。乍一看就是这样。

但是我不认为这是必然的,甚至 SQL 数据库也不认为 NULL 破坏了所有比较的可能性。

在数据库中运行查询 从值中选择 * (空) 工会 从值中选择 * (空)

您看到的只是一个元组,其中一个属性的值为 NULL。 所以联合在这里将两个 NULL 值识别为相等。

当比较具有3个组件的复合键与具有3个属性(1,3,NULL) = (1,3,NULL) < = > 1 = 1 AND 3 = 3 AND NULL = NULL 的元组时 结果是未知的。

但是我们可以定义一种新的比较运算符,例如 = = 。 X = = Y < = > X = Y OR (X 为空,Y 为空)

使用这种相等操作符可以使包含 null 组件的复合键或包含 null 值的非复合键不成问题。

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