我有一个遗留的Oracle数据库,该数据库有一个我想理解的奇怪问题。它具有一个复合外键,其中一些列可以为空。对我来说,这闻起来像是粗心的开发人员的糟糕设计,但我想征求意见。当然,最初的开发团队早已消失。
该表的列数要大得多,但我认为我可以在下面的示例中提取问题:
create table quadrant (
region number(9) not null,
area number(9) not null,
caption varchar2(20),
primary key (region, area)
);
insert into quadrant (region, area, caption) values (10, 123, 'Chicago');
insert into quadrant (region, area, caption) values (10, 125, 'Wisconsin');
create table farm (
id number(9),
region_id number(9) not null,
area_id number(9),
name varchar2(50),
constraint fk_region_area foreign key (region_id, area_id)
references quadrant (region, area)
);
insert into farm (id, region_id, area_id, name) values (5, 10, null, 'farm 1');
insert into farm (id, region_id, area_id, name) values (6, 11, null, 'farm 2');
select * from farm;
结果:
ID REGION_ID AREA_ID NAME
-- --------- ------- ------
5 10 <null> farm 1 <-- Does it point to anything?
6 11 <null> farm 2 <-- Region 11 doesn't even exist!
如果外键的部分为空,那么它的含义是什么?
REGION_ID, AREA_ID = (10, null)
指向任何东西,还是只是无用的信息?REGION_ID, AREA_ID = (11, null)
指向任何东西吗?我不这么认为。我很想添加一个约束,以强制外键上的空值执行ALL或NONE。有道理吗?
但是最重要的是,此“功能”的用例是什么?
答案 0 :(得分:2)
感谢所有答案和评论。这个问题使我不得不学习一些新知识,这是一件好事。 @philipxy给了我一个大提示。我想回顾一下我学到的东西,因为它可能对其他人有用,这是记录它的好地方。
此问题有两个方面:首先,什么是部分为空的外键 是什么,其次是如何实现。
部分为空的外键的含义
关于这是什么有很多争论-@ agiles231指出。 NULL
的意思是:
NULL
本身就是一个真诚的价值。 简而言之,到目前为止,其含义尚无明确答案。
我猜取决于人们如何解释空值,然后在外键中使用它们(并验证它们)的策略可能会有所不同。
部分为空的外键的实现
SQL-92 Standard定义(第4.10.2节)将复合外键与可空值匹配的三种不同方式:
匹配 SIMPLE :如果组合外键的任何列为空,则该外键将被接受,存储,但不会针对引用的表进行验证。这通常是数据库提供的默认模式。在SQL-92标准中,描述了此模式,但未命名。
匹配 PARTIAL :如果复合外键的任何列为null,则将每个非null列与引用表进行匹配,以检查至少有一行包含该值存在。我还没有数据库实现此模式。
匹配 Full :不接受部分为空的外键。外键完全为空或完全不为空。如果为null,则不会针对引用的表进行验证。不为null时,将针对引用的表进行完全验证。这是我期望的默认行为(在我的无知中)。
好吧,我检查了10个不同的数据库是如何实现这些模式的,这就是我的发现:
Database Engine Match SIMPLE Match PARTIAL Match FULL
--------------- ------------ ------------- ----------
Oracle 12c1 YES*1 NO NO
DB2 10.5 YES*1 NO NO
PostgreSQL 10 YES*1 NO YES
SQL Server 2014 YES*1 NO NO
MariaDB 10.3 YES*1 NO*2 NO*2
MySQL 8.0 YES*1 NO*2 NO*2
Sybase ASE 16 YES*1 NO YES
H2 1.4 YES*1 NO NO
Derby 10.13 YES*1 NO NO
HyperSQL 2.3 YES*1 NO YES
* 1这是默认模式。
* 2创建表时接受,但被忽略。
简而言之:
默认情况下,所有经过测试的数据库的行为均相同:它们默认为Match SIMPLE。
我测试过的数据库均不支持Match PARTIAL。我想这很有意义,因为我个人对此几乎没有用。而且,对单独的外键列执行部分验证而又不在引用表上创建所有可能的索引组合的情况下,代价可能会非常高昂。
PostgreSQL实现了Match Full和Sybase ASE。这真是个好消息!令人惊讶的是HyperSQL(这个小型数据库)也是如此。
实施完全匹配的解决方法
好消息是,如果您碰巧需要在任何经过测试的数据库中实施Match FULL,则有一个相当简单的解决方法。只需添加一个表约束,该约束将允许所有空列或所有非空列。像这样:
create table farm (
id int,
region_id int,
area_id int,
name varchar(50),
constraint fk_region_area foreign key (region_id, area_id)
references quadrant (region, area),
constraint fkfull_region_area check ( -- here's the workaround
region_id is null and area_id is null or
region_id is not null and area_id is not null)
);
insert into farm (id, region_id, area_id, name) values (5, 10, null, 'farm 1'); -- fails
insert into farm (id, region_id, area_id, name) values (6, 11, null, 'farm 2'); -- fails
insert into farm (id, region_id, area_id, name) values (7, 10, 125, 'farm 3'); -- succeeds
insert into farm (id, region_id, area_id, name) values (8, null, null, 'farm 4'); -- succeeds
效果很好。
最后,作为一个非常我个人的看法,我期望Match FULL是默认的匹配策略。也许对我而言,(默认情况下)允许不指向其他行的外键会导致使用该数据库的应用程序出错。
与SIMPLE相比,我认为大多数开发人员将很容易理解FULL。而且PARTIAL更复杂,并且容易出错。只是我的意见。
答案 1 :(得分:1)
关于使用null表示某些东西的争论很多。有些人会认为null意味着该值是未知的或表示无效,而另一些人则认为这是实际值本身。我怀疑在这种情况下,它代表未知。假设您要记录100年前的一个县的农场位置。使用一些本地历史书籍,您已绘制出该时期70%的现有农场及其确切边界(或附近),但对于其余30%,则有一些已知区域,而某些仅已知存在。在这种情况下,我肯定会说空外键是有意义的。这只是未知信息。
答案 2 :(得分:1)
对于您的“功能”,有些猜测是这样的:也许面积区域仅适用于某些农场?示例:指定区域的农场需要支付一些附加费或税费(此处猜测,因为我不知道您的数据)?在这种情况下,NULL表示某种东西(不需要付款)。也许有些农场在实施“区域”之前就已经存在,因此从未被分配?在这种情况下,NULL实际上表示NULL,因为该区域从不存在,因此未知。