Sql Oracle-部分可空的复合外键

Sql Oracle-部分可空的复合外键,sql,oracle,foreign-keys,Sql,Oracle,Foreign Keys,我有一个遗留的Oracle数据库,它有一个我想理解的奇怪的怪癖。它有一个复合外键,某些列可以为空。对我来说,这听起来像是一个粗心的开发人员的糟糕设计,但我想征求意见。当然,最初的开发团队早已不复存在 表中的列要大得多,但我认为我能够在下面的示例中提取问题: create table quadrant ( region number(9) not null, area number(9) not null, caption varchar2(20), primary key (re

我有一个遗留的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!
ID区域\u ID区域\u ID名称
--  ---------  -------  ------

5 10农场1关于使用空表示某事,存在很多争论。有些人认为null表示该值未知或表示无效,其他人则认为它本身就是一个实际值。我怀疑在这种情况下,它代表未知。假设您记录了100年前一个县的农场位置。使用一些当地的历史书籍,你已经绘制出了那个时期70%的现有农场及其确切边界(或大约),但对于剩下的30%,有些已经知道了地区,有些只知道存在。在这种情况下,我肯定会说空外键是有意义的。这仅仅是未知信息。

对于您的“功能”有一些猜测:也许是区域字段仅适用于某些农场?示例:指定区域的农场需要支付一些附加费或税费(在这里猜测,因为我不知道您的数据)?在这种情况下,NULL表示某种东西(不需要支付)。也许在实施“区域”之前就存在农场,因此从未分配过农场?在这种情况下,NULL实际上意味着NULL,因为该区域从未存在过,因此是未知的。

我不知道它对您的数据模型是否有意义,但对于部分NULL外键有明确的用例

考虑一个简单的固定资产表(计算机、汽车、建筑等——会计会折旧的东西)。假设他们想知道资产在哪里使用,所以他们有两列:
company\u id
department\u id

有些资产,如建筑物,可能在各个部门之间共享,因此我希望使用外键,如
(123,null)
。我还希望在
COMPANY\u ID
上的
COMPANY
表中有一个单独的外键

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

更新 我不知道你为什么认为甲骨文不能做我描述的事情。下面是一个简单的测试:

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

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

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

部分空外键的含义

正如@agiles231所指出的,关于这意味着什么,有很多争论
NULL
可能意味着:

  • 值是未知的
  • 其他人说这意味着该值是无效的
  • 其他人说,
    NULL
    本身就是一个真正的值
简而言之,到目前为止,它的含义还没有明确的答案

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

部分空外键的实现

定义了(第4.10.2节)将复合外键与可空值匹配的三种不同方式:

  • MatchSIMPLE:如果复合外键的任何列为null,则接受、存储外键,但不根据引用的表进行验证。这通常是数据库提供的默认模式。在SQL-92标准中,描述了此模式,但未命名

  • MatchPARTIAL:如果复合外键的任何列为null,则将每个非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在创建表时接受,但被忽略

简言之:

  • 默认情况下,所有已测试的数据库的行为方式相同:它们默认匹配简单数据库

  • 我测试的任何数据库都不支持部分匹配。我想这是有道理的,因为我个人觉得它没什么用处。此外,在不创建引用表上所有可能的索引组合的情况下,对单独的外键列执行部分验证可能会变得非常昂贵

  • PostgreSQL实现了完全匹配和Sybase ASE。这是个好消息!令人惊讶的是,HyperSQL(这个小小的数据库)也是如此

实施完全匹配的变通方法

好消息是,如果您碰巧需要,可以在任何测试过的数据库中实现完全匹配,这是一个相当简单的解决方法。只需添加一个表约束,该约束允许所有空列或所有非空列。比如:

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
它工作得很好

最后,作为一个非常个人的观点,我会