我有一个遗留的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的外键值空值。是否有意义?
但最重要的是,这个“功能”的用例是什么?
感谢所有的答案和评论。这个问题我迫使我学习新东西,这是件好事。 @philipxy给了我很大的线索。我想回顾一下我学到的东西,因为它可能对其他人有用,而且这是一个记录它的好地方。
这个问题有两个方面:第一,部分无效的外键意味着什么,第二,它是如何实现的。
部分空外键的含义
关于这是什么意思存在很多争论 - 正如@ agiles231指出的那样。 NULL
可能意味着:
NULL
本身就是一个真正的价值。简而言之,目前还没有明确的答案。
我想根据人们如何解释空值,然后在外键中使用它们(并验证它们)的策略可能会有所不同。
部分空外键的实现
SQL-92 Standard定义了(第4.10.2节)三种不同的方法来匹配复合外键和可空值:
好吧,我检查了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 FULL的解决方法
好消息是,在任何测试数据库中,如果您碰巧需要它,那么实现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相比,大多数开发人员都会很容易理解。 PARTIAL更复杂,并且可能容易出错。只是我的观点。
关于使用空值来表示某些东西存在很多争论。有些人认为null意味着价值未知或代表无效,其他人会认为它本身就是实际价值。我怀疑在这种情况下,它代表未知。假设您在100年之前记录了一个县的农场的位置。使用一些本地历史书籍,您已经绘制了70%的现有农场及其确切的边界(或其左右),但是对于剩下的30%,有些已知区域,有些只知道存在。在这种情况下,我肯定会说null外键是有道理的。这只是未知的信息。
一些猜测是你的“特征”的情况:也许可能是区域只适用于某些农场?示例:具有指定区域的农场需要支付一些附加费或税费(此处猜测,因为我不知道您的数据)?在这种情况下,NULL表示某些内容(不需要付费)。也许在实施“区域”之前存在农场,因此从未分配过一个农场?在这种情况下,NULL实际上意味着NULL,因为该区域从未存在,因此是未知的。
我不知道你的数据模型是否有意义,但有部分NULL外键的确定用例。
考虑一个简单的固定资产表(计算机,汽车,建筑物等 - 会计师将贬值的东西)。假设他们想知道资产在哪里使用,所以他们有两列:company_id
和department_id
。
有些资产,比如建筑物,可能会跨部门共享,所以我希望像(123, null)
这样的外键。我还希望在COMPANY
上只有COMPANY_ID
表的单独外键。
这种设置的含义是company_id必须是已知值,并且公司/部门组合(如果存在)必须是已知组合。
我不确定为什么你认为Oracle不能做我所描述的。这是一个简单的测试:
CREATE TABLE tst_company
( company_id NUMBER NOT NULL PRIMARY KEY );
CREATE TABLE tst_department
( company_id NUMBER NOT NULL,
department_id NUMBER NOT NULL,
CONSTRAINT tst_department_pk PRIMARY KEY ( company_id, department_id ),
CONSTRAINT tst_department_f1 FOREIGN KEY ( company_id ) REFERENCES tst_company ( company_id ) );
CREATE TABLE tst_asset
( asset_id NUMBER NOT NULL PRIMARY KEY,
company_id NUMBER NOT NULL,
department_id NUMBER,
CONSTRAINT tst_asset_f1 FOREIGN KEY ( company_id ) REFERENCES tst_company ( company_id ),
CONSTRAINT tst_asset_f2 FOREIGN KEY ( company_id, department_id ) REFERENCES tst_department ( company_id, department_id ) );
INSERT INTO tst_company ( company_id ) VALUES (1);
INSERT INTO tst_department ( company_id, department_id ) VALUES (1, 10);
INSERT INTO tst_asset ( asset_id, company_id, department_id ) VALUES (1001, 1, 10); -- Department specific asset
INSERT INTO tst_asset ( asset_id, company_id, department_id ) VALUES (1002, 1, NULL); -- Non-department specific asset
INSERT INTO tst_asset ( asset_id, company_id, department_id ) VALUES (1003, 2, NULL); -- Bad company - fails
INSERT INTO tst_asset ( asset_id, company_id, department_id ) VALUES (1004, 1, 11); -- Bad department - fails
INSERT INTO tst_asset ( asset_id, company_id, department_id ) VALUES (1005, 2, 11); -- Bad company AND department - fails
马修的答案告诉你如何解决这个问题。至于为什么会这样,请记住:
约束仅拒绝条件为false的行。
比较任何东西= null =>未知。因此,除非您专门测试它们,否则null值会传递约束。这导致您演示的孤立行。
正如the docs所说:
如果复合外键的任何列为null,则键的非null部分不必匹配父键的任何相应部分
在大多数情况下,这是原始设计师的错误或疏忽。