如何在我的数据库中避免 NULL,同时还表示丢失的数据?
Posted
技术标签:
【中文标题】如何在我的数据库中避免 NULL,同时还表示丢失的数据?【英文标题】:How can I avoid NULLs in my database, while also representing missing data? 【发布时间】:2011-05-19 05:05:12 【问题描述】:在SQL and Relational Theory(C.J. Date, 2009)第 4 章中提倡避免重复行,同时也避免在我们存储的数据中使用NULL
属性。虽然避免重复行没有问题,但我正在努力了解如何在不使用NULL
的情况下对数据进行建模。以以下为例 - 这有点工作。
我们有一个artist
表,其中除其他列外,还有一个gender
列。这是gender
表的外键。然而,对于一些艺术家,我们不知道他们的性别——例如,我们得到了一份新音乐列表,其中没有艺术家的描述。如果不使用NULL
,如何代表这些数据?我看到的唯一解决方案是在gender
表中添加一个新的性别“未知”。
虽然我非常喜欢这本书,但当本章结束时我真的很失望:
当然,如果禁止使用空值,则必须通过其他方式处理丢失的信息。不幸的是,这些其他方法过于复杂,无法在此详细讨论。
这真是令人遗憾 - 因为这是我等待阅读的解决方案!有一个参考阅读附录,其中有很多出版物可供阅读,但在我深入阅读这些之前,我希望能得到更多的实际总结。
我得到一些人的评论,他们不明白为什么我希望避免“NULL”,所以我将再次引用这本书。进行以下查询:
SELECT s.sno, p.pno
FROM s, p
WHERE s.city <> p.city
OR p.city <> 'Paris'
现在,以 s.city 为伦敦,p.city 为巴黎为例。在本例中,伦敦 巴黎,所以查询为真。现在假设 p.city 不是巴黎,实际上是 xyz。在这种情况下,(London xyz) OR (xyz Paris) 也是 True。所以,给定任何数据——这个查询是真的。但是,如果 xyz 为“NULL”,则情况会发生变化。在这种情况下,这两个表达式都不是 True 也不是 False,它们实际上是 Unknown。在这种情况下,由于结果未知,您将不会返回任何行。
从 2 值逻辑到 3 值逻辑的转变很容易引入这样的错误。事实上,我刚刚在工作中介绍了一个激发这篇文章的动机。我想要type != 0
所在的所有行但是,这实际上最终匹配type == 0 OR type IS NULL
- 令人困惑的行为。
未来我是否使用NULL
对我的数据进行建模尚不清楚,但我很好奇其他解决方案是什么。 (我也一直认为如果你不知道,你应该使用NULL
)。
【问题讨论】:
NULL 和 unknown 是不同的。 NULL 表示不填写;未知表示您目前无法确定。 “理论上,理论和实践之间没有区别。在实践中,有” - Jeff Atwood 空值总是会导致不正确的结果 - 即与它们应该代表的任何事物的现实和逻辑不匹配的结果。由于没有它们,所有信息都可以准确表示,因此仅在特殊情况下才应使用空值。 性别表有多少行? 男/女/文件未找到 【参考方案1】:你很好,消除了 Null。我从来没有在我的任何数据库中允许 Null。
当然,如果禁止使用空值,那么丢失的信息将不得不通过其他方式处理。不幸的是,这些其他方法过于复杂,无法在此详细讨论。
其实一点也不难。有三种选择。
这是 H Darwen 撰写的关于 How To Handle Missing Information Without Using NULL 的论文,它可能有助于解决这个问题。
1.1。第六范式就是答案。但是您不必将您的整个数据库标准化为 6NF。对于可选的每一列,您需要一个主表之外的子表,只有 PK,这也是 FK,因为它是 1::0-1 关系。除了 PK,唯一的列是可选列。
看看这个Data Model;第4页的AssetSerial
是一个经典案例:不是所有Assets
都有SerialNumbers
;但是当他们这样做时,我希望他们存储它们;更重要的是,我想确保它们是唯一的。
(顺便提一下,对于那些面向 OO 的人来说,这是一个关系表示法的三层类图,一个“具体表继承”,没什么大不了的,我们已经有 30 年了。)
1.2。对于每个这样的表,使用视图来提供表的 5NF 形式。当然,使用 Null(或适用于该列的任何值)来识别任何行的列不存在。但不要通过视图更新。
1.3 不要使用直连接来抓取 6NF 列。也不要使用外连接(并让服务器为缺失的行填充 Null)。使用子查询来填充列,并指定要为缺失值返回的值(除非您有 Oracle,因为它的子查询处理甚至比它的集合处理更差)。例如。只是一个例子。您可以将数字列转换为字符串,并对缺失的行使用“Missing”。
如果您不想走那么远 (6NF),您还有两个选择。
您可以使用 Null 替换。我将 CHAR(0) 用于字符列,将 0 用于数字。但我不允许 FK 这样做。显然,您需要一个超出正常数据范围的值。这不允许三值逻辑。
除了 (2) 之外,对于每个 Nullable 列,您还需要一个布尔指示符。对于Sex
列的示例,指标类似于SexIsMissing
或SexLess
(抱歉)。这允许非常严格的三值逻辑。这 5% 中的许多人都喜欢它,因为 db 保持在 5NF(以及更少的表);缺少信息的列加载了从未使用过的值;它们仅在指标为假时使用。如果您有企业数据库,则可以将其包装在函数中,并始终使用 UDF,而不是原始列。
当然,在所有情况下,您都无法避免编写处理丢失信息所需的代码。无论是ISNULL()
,还是 6NF 列的子查询,还是使用值前要检查的 Indicator,还是 UDF。
如果 Null 有特定含义... 那么它就不是 Null!根据定义,Null 是未知值。
【讨论】:
+1 以获得 H Darwen 论文的链接。然而,在我看来,他从一个表的案例研究开始,在这个讨论中,没有人会首先允许 NULL。如果有一个所有意见都存在分歧的例子就好了,比如说 50-50。 感谢您的回答!是的,可悲的是,很多人似乎并没有真正理解我的观点,只是假设我没有“得到”NULL——这根本不是真的;我只是不认为任何事情是最终的方法,并且喜欢保持我的选择开放。再次感谢! 这种“解决方案”总是让我觉得被骗的地方是:“指定你想要为缺失值返回的值”。如果你打算有一个“哨兵值”,那么你要么必须在某种类型中保留这样的值(例如0
,''
),要么你需要将你的类型与不属于领域。这就是null
首先的用途。所以你似乎回到了你开始的地方。
@IMSoP。呃没有。我没有null,所以我没有问题。你有空,和问题。你习惯了兜圈子,一遍遍地看起点,所以你认为别人也是这样。我不。无论如何,我不是在回答你的问题。不要用我的解决方案,用你的“解决方案”,不要担心另一半的生活。如果你猜测它们,你肯定会崩溃,并欺骗自己。如果您真的不明白,请提出问题,而不是发表声明。
@PerformanceDBA 我不确定你为什么要亲自处理这个问题。如果我说的话以任何方式冒犯了你,我很抱歉。我并不是说我“感觉被你 欺骗了”,而是声称“NULL 是不必要的”,而实际上它似乎归结为“NULL 可以更好地实现”。我的问题是:当您引入哨兵值时(例如在第 1.3 点或第 2 点中),在某些情况下是否不需要重新实现 3VL,例如回答“工资 > 5000 美元?”的问题。对于带有“salary = 'Unknown'”的项目?如果是这样,这与谨慎使用 NULL 有何不同?【参考方案2】:
那么你如何设计没有NULLS?那是最初的问题。
其实很简单。您的设计使得每当您必须丢失一些数据时,您可以通过丢失整行来做到这一点。如果一行不存在,则它不是充满 NULL 的行。它根本不存在。
因此,在“DateOfDeath”的情况下,我们有一个包含两列的表,即 PersonId 和 DateOfDeath。 PersonId 引用 Persons 表中的 Id。如果没有要存储的 DateOfDeath,我们不存储该行。讨论完毕。
如果您在此表和 Persons 表之间进行 OUTER JOIN,那么在没有行的地方,您将得到 DateOfDeath 的 NULL。如果你在 where 子句中使用它,你会得到关于 3 值逻辑的通常令人困惑的行为。如果您执行 INNER JOIN,则没有 DateOfDeath 的行将从连接中消失。
允许每列强制执行 NOT NULL 的设计被称为第六范式。
说了这么多,我经常允许在非关键列中使用 NULL。而且我没有简洁的方式告诉你我如何确定一个列是关键的。
【讨论】:
简洁、非宗教、实用的建议结束。很好的答案! 6NF 与空值无关。【参考方案3】:很简单,只存储已知信息——换句话说,封闭世界假设。目标是至少处于 Boyce Codd / 第五范式,这样你就不会出错。
【讨论】:
你有什么关于封闭世界假设的推荐读物吗?我似乎也需要去阅读 3NF 之后的额外阅读。谢谢!【参考方案4】:null
s 是理论与现实相遇的结果,必须进行调整才能使用。在我看来,试图避免所有 null
值最终会导致比仅在适当的情况下使用 null
更难看且更难维护的代码。
【讨论】:
我同意。理论是实用的,但这个真理有其局限性。从理论上讲,理论与实践没有区别。在实践中,有。问题通常来自于数据库文档中没有明确说明,给定的 NULL 实际意味着什么,除了“这里没有数据”。null
是一个有效值。数据库对此没有任何意义。应用程序文档应记录超出“无数据”的含义。【参考方案5】:
NULL 是必需的 - 无需替换它们
NULL 的全部定义是它的未知 - 简单地用任意类型替换它就是做同样的事情,那为什么呢?
对于下面的cmets:
刚刚试过 - 两者都不是真的:
declare @x char
set @x = null
if @x = @x
begin
select 'true'
end
if @x <> @x
begin
select 'false'
end
我只能认为这是因为 null 是未知的,所以不能说它等于或不等于 - 因此两个陈述都是错误的
【讨论】:
同意。无法理解为什么 OP 想跳过NULL
空值表示未知的误解很常见,但显然是不正确的(至少在 SQL 中)。 SQL 不会那样使用空值。想想表达式 x=x。如果 x 未知,则该表达式的结果将为 TRUE。但是,如果 x 为 null,则情况并非如此。因此 null 并不意味着“未知”。
@ m.edmondson :您的示例说明了问题,但您的结论肯定是错误的。在数学、逻辑和现实世界中,如果 x 未知,则 x = x 为 TRUE。如果不是,那么代数和大多数科学都是不可能的。 Null 与“未知”的东西不同,如果你假装它是,那么你肯定会从你的 SQL 中得到错误的结果——比如你在这里演示的那个。
aCiD2 - 这根本不是真的。如果我有两种水果但没有告诉你是哪一种,你就不能确定它们是相同的还是不同的,因为你有一个未知的
x=x 为真的断言背后有两个假设。第一个假设是“x”的两个实例指的是同一个东西。第二个假设是,虽然 x 的值未知,但仍然可以断言 x 具有值。这些都是合理的假设,但如果要使用 NULL 来传达消息,则值得以某种方式明确说明它们。我更喜欢将 NULL 视为消息的非通信。这与某种默认消息的通信不同。【参考方案6】:
NULL
可以/应该使用,只要:
A) 您有商业原因。例如,在付款表中,NULL
付款值表示从未付款。 0.00
支付价值意味着我们故意不支付任何费用。对于医疗图表,血压读数的NULL
值表示您没有测量血压,0
值表示患者已经死亡。这是一个显着的区别,在某些应用中是必需的。
B) 您的查询说明了这一点。如果您了解NULL
对IN
、EXISTS
、不等式运算符(如您在OP 中指定)等的影响,那么这应该不是问题。如果您的表中现在有 NULL
并且不希望某些应用程序的值,您可以使用视图和 COALESCE
或 ISNULL
如果源表有 NULL
来填充不同的值。
编辑:
使用NULL
解决 OP 关于“现实世界”不等式/等式的问题,这是我有时使用的一个很好的例子。
您正在和另外 3 个人参加聚会。您知道一个人叫“John”,但不认识其他人。
逻辑上,“有多少人叫乔”的答案是未知的或NULL
。在 SQL 中,这类似于
SELECT name FROM party where NAME = 'Joe'
你不会得到任何行,因为你不知道他们的名字。他们可能是也可能不是乔。
你的不等式是:
SELECT name from party where NAME <> 'Joe'
你只会得到“John”的返回值,因为你只知道 John 的名字。其他人可能是也可能不是乔,但你无法知道。
【讨论】:
@dportas :如果您知道没有付款,使用NULL
比添加“未付款”列容易得多。
给 NULLS 赋予意义就像给狗在夜间吠叫失败赋予意义一样。它可能非常聪明,但也可能非常冒险。例如,死亡日期中的 NULL 可能表示此人还活着,但也可能表示在死亡日期留空的情况下输入了死亡证明。
@Valentin -- 不说付款与说没有付款是两回事。
@JNK。哇。您将大量字符信息打包到数字列中。或者你所有的列都是VARCHARS。您谈论业务的方式是关于人,而不是关于适当的业务决策,基于已知事实。
@JNK:当然。阅读我的答案,以及人们留下的 cmets。【参考方案7】:
我不同意作者的观点,并声称 NULL 实际上是处理可选字段缺失数据的正确方法。事实上,这就是NULL存在的原因......
对于您关于性别的具体问题:
您确定需要一个性别表并为每个查询产生额外的连接成本吗?对于简单的枚举类型,将字段设为 int 并定义 1=male, 2=female, NULL=unknown 并非不合理。【讨论】:
你不同意 Codd & Date,我喜欢它!!!您已经编写了关系模型的继任者,是吗? ENUM 不是 ISO/IEC/ANSI 标准 SQL;它是城镇免费软件的非标准扩展。连接没什么好害怕的。【参考方案8】:尽可能不要将列定义为 NULL。对我来说,它与您希望 NULL 表示它与磁盘 I\O 的业务规则无关。
在 SQL Server 中,一个可为空的列,比如一个字符 10,当为空时将占用位图中的一位,而当不可为空时将占用 10 个字节。那么空值如何损害磁盘 I/O。它伤害的方式是当一个值被插入到曾经是空值的列中时。由于 SQL 没有保留空间,因此行中没有空间来放置值,因此 SQL Server 必须移动数据以腾出空间。如果这是一个 HEAP,页面拆分、碎片、更新 RID 等都会损害磁盘 I/O。
顺便说一句,如果有性别表,我们可以为“无法确定个人的真实性起源或状态”添加另一行。
【讨论】:
假设你有一台“完美的电脑”——无限的内存(内存+磁盘),速度超快。你还会这样设计吗?如果是,那就这样吧。但是,如果不是,那么您是围绕硬件限制进行设计,而不是围绕业务需求进行设计。在过去约 15 年中,普通 PC 的磁盘空间增加了 1000 多倍,内存至少增加了 250 倍。 @Damir Sudarevic :即使您拥有一台“完美的计算机”,您当然也应该设计没有空值的数据库。原因是为了保证正确性,而不是为了优化硬件。空值会导致不正确的结果,并且不能准确地模拟现实,因此通常应该避免它们。以上是关于如何在我的数据库中避免 NULL,同时还表示丢失的数据?的主要内容,如果未能解决你的问题,请参考以下文章
Mongoose:我如何避免回调地狱,同时允许对不返回承诺的 mongoose 方法进行存根?