Oracle - 部分可以为空的复合外键

问题描述 投票:3回答:5

我有一个遗留的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)指向什么吗?我不这么认为。

我很想添加一个约束来强制执行A​​LL或NONE的外键值空值。是否有意义?

但最重要的是,这个“功能”的用例是什么?

sql oracle foreign-keys
5个回答
2
投票

感谢所有的答案和评论。这个问题我迫使我学习新东西,这是件好事。 @philipxy给了我很大的线索。我想回顾一下我学到的东西,因为它可能对其他人有用,而且这是一个记录它的好地方。

这个问题有两个方面:第一,部分无效的外键意味着什么,第二,它是如何实现的。

部分空外键的含义

关于这是什么意思存在很多争论 - 正如@ agiles231指出的那样。 NULL可能意味着:

  • 价值未知。
  • 其他人说这意味着价值无效。
  • 其他人说NULL本身就是一个真正的价值。

简而言之,目前还没有明确的答案。

我想根据人们如何解释空值,然后在外键中使用它们(并验证它们)的策略可能会有所不同。

部分空外键的实现

SQL-92 Standard定义了(第4.10.2节)三种不同的方法来匹配复合外键和可空值:

  • 匹配SIMPLE:如果复合外键的任何列为空,则接受,存储外键,但不对引用的表进行验证。这通常是默认模式数据库提供的。在SQL-92标准中,描述了此模式但未命名。
  • Match PARTIAL:如果复合外键的任何列为null,则每个非空列与引用的表匹配,以检查是否存在至少存在该值的行。我见过没有数据库实现这种模式。
  • 匹配已满:不接受部分为空的外键。外键完全为空或完全不为空。如果为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的解决方法

好消息是,在任何测试数据库中,如果您碰巧需要它,那么实现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更复杂,并且可能容易出错。只是我的观点。


1
投票

关于使用空值来表示某些东西存在很多争论。有些人认为null意味着价值未知或代表无效,其他人会认为它本身就是实际价值。我怀疑在这种情况下,它代表未知。假设您在100年之前记录了一个县的农场的位置。使用一些本地历史书籍,您已经绘制了70%的现有农场及其确切的边界(或其左右),但是对于剩下的30%,有些已知区域,有些只知道存在。在这种情况下,我肯定会说null外键是有道理的。这只是未知的信息。


1
投票

一些猜测是你的“特征”的情况:也许可能是区域只适用于某些农场?示例:具有指定区域的农场需要支付一些附加费或税费(此处猜测,因为我不知道您的数据)?在这种情况下,NULL表示某些内容(不需要付费)。也许在实施“区域”之前存在农场,因此从未分配过一个农场?在这种情况下,NULL实际上意味着NULL,因为该区域从未存在,因此是未知的。


1
投票

我不知道你的数据模型是否有意义,但有部分NULL外键的确定用例。

考虑一个简单的固定资产表(计算机,汽车,建筑物等 - 会计师将​​贬值的东西)。假设他们想知道资产在哪里使用,所以他们有两列:company_iddepartment_id

有些资产,比如建筑物,可能会跨部门共享,所以我希望像(123, null)这样的外键。我还希望在COMPANY上只有COMPANY_ID表的单独外键。

这种设置的含义是company_id必须是已知值,并且公司/部门组合(如果存在)必须是已知组合。

UPDATE

我不确定为什么你认为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

1
投票

马修的答案告诉你如何解决这个问题。至于为什么会这样,请记住:

约束仅拒绝条件为false的行。

比较任何东西= null =>未知。因此,除非您专门测试它们,否则null值会传递约束。这导致您演示的孤立行。

正如the docs所说:

如果复合外键的任何列为null,则键的非null部分不必匹配父键的任何相应部分

在大多数情况下,这是原始设计师的错误或疏忽。

© www.soinside.com 2019 - 2024. All rights reserved.