使用完整性约束强制实施“子集”关系的最佳方法是什么

时间:2011-04-02 09:20:22

标签: mysql sql-server oracle database-design postgresql

例如,给出3个表:

  • 蜗牛
  • 蛞蝓

并假设我们要强制执行

  1. '腹足动物'中的每一行都有'蜗牛'或'slu'中的一行(但不是两者)
  2. 'slug'中的每一行在'gastropod'中都有一行对应的行
  3. 'snail'中的每一行在'gastropod'
  4. 中只有一行

    设置架构以强制执行这些约束的最佳方法是什么?

    我为postgres提供了一个可能的答案,我对postgres和Oracle的解决方案特别感兴趣,但也有兴趣看到其他RDBMS的解​​决方案

    EDIT
    作为参考,来自以下答案/评论的SO问题解决了类似的问题:

12 个答案:

答案 0 :(得分:3)

我自己的postgres解决方案(但我不知道这是否是最佳方式):

枚举:

create type gastropod_type as enum ('slug', 'snail');

表和约束:

create table gastropod(
  gastropod_id serial unique,
  gastropod_type gastropod_type,
  slug_gastropod_id integer,
  snail_gastropod_id integer,
  average_length numeric,
  primary key(gastropod_id, gastropod_type),
  check( (case when slug_gastropod_id is null then 0 else 1 end)+
         (case when snail_gastropod_id is null then 0 else 1 end)=1) );

create table slug(
  gastropod_id integer unique,
  gastropod_type gastropod_type check (gastropod_type='slug'),
  is_mantle_visible boolean,
  primary key(gastropod_id, gastropod_type),
  foreign key(gastropod_id, gastropod_type) 
    references gastropod deferrable initially deferred );

create table snail(
  gastropod_id integer unique,
  gastropod_type gastropod_type check (gastropod_type='snail'),
  average_shell_volume numeric,
  primary key(gastropod_id, gastropod_type),
  foreign key(gastropod_id, gastropod_type)
    references gastropod deferrable initially deferred );

alter table gastropod 
add foreign key(slug_gastropod_id, gastropod_type) 
references slug deferrable initially deferred;

alter table gastropod 
add foreign key(snail_gastropod_id, gastropod_type) 
references snail deferrable initially deferred;

试验:

insert into gastropod(gastropod_type, slug_gastropod_id, average_length)
values ('slug', currval('gastropod_gastropod_id_seq'), 100);

insert into slug(gastropod_id, gastropod_type, is_mantle_visible)
values (currval('gastropod_gastropod_id_seq'), 'slug', true);

select gastropod_id, gastropod_type, average_length, is_mantle_visible
from gastropod left outer join slug using(gastropod_id, gastropod_type) 
               left outer join snail using(gastropod_id, gastropod_type);

 gastropod_id | gastropod_type | average_length | is_mantle_visible
--------------+----------------+----------------+-------------------
            1 | slug           |            100 | t                 
(1 row)

答案 1 :(得分:1)

查看此主题:Maintaining subclass integrity in a relational database

该线程为SQL Server实现提供了多个建议,如果这些想法也不能应用于Oracle,我会感到惊讶。

答案 2 :(得分:1)

SQL的一个问题是它对完整性约束的支持程度很低,尤其是引用约束。

出于所有实际目的,除非在要向表中插入行时禁用约束,否则无法使用SQL约束解决问题。原因是SQL要求一次更新一个表,因此每当插入新行时都必须违反约束。这是SQL的一个基本限制,所有主要的DBMS都受其影响。

有一些解决方法,但没有一个是完美的。如果您的DBMS具有DEFERRABLE约束(例如Oracle),则可以使用DEFERRABLE约束。 DEFERRABLE约束实际上只是一种禁用约束的简单方法。或者您可以使用触发器,这意味着规则是在程序上强制执行而不是通过适当的数据库约束。

答案 3 :(得分:1)

我知道这个问题是超类型/子类型问题。我已经多次在SO上写过这篇文章了。在this post中,它被解决为员工,客户和供应商的问题。但this post在理论基础和约束如何运作背后有最广泛的讨论。它是用在线出版物写的。

答案 4 :(得分:1)

这种情况下,使用触发器是有价值的,具有强制执行的复杂约束。

答案 5 :(得分:1)

“并假设我们想强制执行 (1)'腹足动物'中的每一行在'蜗牛'或'slu''中只有一行(但不是两者) (2)'slug'中的每一行在'腹足动物'中只有一行相应的行 (3)'蜗牛'中的每一行在“腹足动物”中都有一行对应的行

(1)是'GASTROPOD'和定义为SLUG UNION SNAIL的虚拟relvar(又名“视图”)之间的包含依赖(也称为“外键依赖”)。 (2)和(3)是'SLUG'(/'SNAIL')和'GASTROPOD'之间的包含依赖关系。

所有这些合在一起意味着你在'GASTROPOD'和'SLUG UNION SNAIL'之间存在“平等依赖”(至少就标识符而言)。

请注意,为了能够更新受此类限制的数据库,您可能需要一个支持这个名为“Multiple Assignment”的DBMS引擎,或者另一个支持“Deferred Constraint Checking”的数据库。

“数据库专业人员应用数学”一书的第11章深入探讨了如何在SQL环境中强制执行此类约束(实际上,只是任何约束,无论多么复杂)。你的问题的答案几乎就是那一章的全部内容,我希望你不要指望我用几句话来概括一下(答案的实质是“触发器” - 正如StarShip3000也指出的那样)。 / p>

答案 6 :(得分:1)

我会去

DROP TABLE GASTROPOD PURGE;
DROP TABLE SNAIL PURGE;

CREATE TABLE GASTROPOD
  (GASTROPOD_ID NUMBER,
  GASTROPOD_TYPE VARCHAR2(5),
  SNAIL_ID NUMBER,
  SLUG_ID NUMBER,
  CONSTRAINT GASTROPOD_PK PRIMARY KEY (GASTROPOD_ID),
  CONSTRAINT GASTROPOD_TYPE_CK CHECK (GASTROPOD_TYPE IN ('SLUG','SNAIL')),
  CONSTRAINT GASTROPOD_SLUG_CK CHECK 
     (SNAIL_ID IS NOT NULL OR SLUG_ID IS NOT NULL),
  CONSTRAINT GASTROPOD_SNAIL_CK1 CHECK 
     (GASTROPOD_TYPE = 'SNAIL' OR SLUG_ID IS NULL),
  CONSTRAINT GASTROPOD_SLUG_CK1 CHECK 
     (GASTROPOD_TYPE = 'SLUG' OR SNAIL_ID IS NULL),
  CONSTRAINT GASTROPOD_SNAIL_CK2 CHECK (SNAIL_ID = GASTROPOD_ID),
  CONSTRAINT GASTROPOD_SLUG_CK2 CHECK (SLUG_ID = GASTROPOD_ID),
  CONSTRAINT GASTROPOD_SNAIL_UK UNIQUE (SNAIL_ID),
  CONSTRAINT GASTROPOD_SLUG_UK UNIQUE (SLUG_ID)
  );

所以你检查一个腹足动物是蜗牛还是slu and,并设置了slug_id或snail_id。 如果它是一个蜗牛,那么slug_id必须为null,对于一个slug,那么snail_id必须为null。 确保slug和snail id是唯一的(我已添加检查以将它们与gastropod_id匹配)。

CREATE TABLE SNAIL
  (SNAIL_ID NUMBER,
   CONSTRAINT SNAIL_PK PRIMARY KEY (SNAIL_ID),
   CONSTRAINT SNAIL_FK FOREIGN KEY (SNAIL_ID)
     REFERENCES GASTROPOD (SNAIL_ID));

蜗牛必须指向腹足动物中的一行,其中snail_id不为空,它也是主键(因此也是唯一的)

ALTER TABLE GASTROPOD ADD CONSTRAINT SNAIL_GS_FK FOREIGN KEY (SNAIL_ID)
     REFERENCES SNAIL (SNAIL_ID) DEFERRABLE INITIALLY DEFERRED;

具有snail_id集的腹足动物也必须在蜗牛中有相应的行。我已经把这个方向推迟了,否则你永远不会得到任何新数据。

答案 7 :(得分:0)

外键引用来自slug和snail的腹足与外键列上的唯一索引强制执行规则2和3.规则1虽然棘手但是: - (

我知道强制执行规则1的唯一方法是编写一些数据库代码,检查蜗牛和slug是否存在行。

顺便说一下 - 你打算如何插入数据?无论你做什么顺序,你都会违反规则。

答案 8 :(得分:0)

“@ Erwin我更喜欢那些不涉及触发器的解决方案 - 我对它们有一种病态的厌恶。”

很抱歉有新的回答,无权向其添加评论。

据我所知,由于您希望强加的约束的性质,您可能能够在特定情况下“仅使用延迟约束”。如果它适合你,你感到满意,那么一切都好,不是吗?

我的主要观点是,约束(如:“任何可以想象的业务规则,你可能会遇到数据库设计者”)可能会变得任意复杂。想想一个家谱数据库,在这个数据库中,你想强制执行“任何人都不能成为他自己的祖先,无论在多大程度上”的规则(这是我最喜欢的例子,因为它最终涉及传递闭包和/或递归)。没有办法,你可以让SQL DBMS在不使用触发器的情况下强制实施这些规则(顺便说一句,也不使用触发器中的递归SQL)。

你的DBMS,我和其他任何熟悉关系理论的人都不会关心你曾经遇到的任何病症的弗洛伊德屎。但也许是因为你提到的这些病症,如果你使用我自己开发的DBMS(它确实支持类似触发器),可能有趣的是观察到你可以做所有你想要的东西,而不必定义任何触发器。事情,但你不需要诉诸它们来强制执行数据完整性。)

答案 9 :(得分:0)

所有这些例子都有一个非常复杂的程度,如下所示:

create table gastropod(
    average_length numeric
);
create table slug(
    like gastropod,
    id          serial  primary key,
    is_mantle_visible boolean
);
create table snail(
    like gastropod,
    id          serial  primary key,
    average_shell_volume numeric
);   
\d snail;

        Column        |  Type   |                     Modifiers                      
----------------------+---------+----------------------------------------------------
 average_length       | numeric | 
 id                   | integer | not null default nextval('snail_id_seq'::regclass)
 average_shell_volume | numeric | 
Indexes:
    "snail_pkey" PRIMARY KEY, btree (id)

在您说这不是答案之前,请考虑一下这些要求。

  1. '腹足动物'中的每一行都有'蜗牛'或'slu'中的一行(但不是两者)
  2. 'slug'中的每一行在'gastropod'中都有一行对应的行
  3. 'snail'中的每一行在'gastropod'
  4. 中只有一行

    表中的列是数据完整性的等价物而没有所有废话。

    注意:DDL 中的LIKE可以将所有列(甚至9.0中的约束和索引)复制到新表中。所以你可以做出假的继承。

答案 10 :(得分:0)

这里有两个问题:

  • 状态:如果没有至少一个子行,则不能有父行。
  • 排他性:不能有包含多个子行的父行。

在支持延迟约束的DBMS(包括PostgreSQL和Oracle)上,这两个目标都可以通过声明方式实现:

enter image description here

gastropod.snail_idsnail.snail_id之间以及gastropod.slug_idslug.slug_id之间存在循环外键。还有一个CHECK可以确保中只有一个匹配gastropod.gastropod_id(另一个是NULL)。

要在插入新数据时打破鸡蛋问题,请推迟一个外键方向。

这是如何在PostgreSQL中实现的:

CREATE TABLE gastropod (
    gastropod_id int PRIMARY KEY,
    snail_id int UNIQUE,
    slug_id int UNIQUE,
    CHECK (
        (slug_id IS NULL AND snail_id IS NOT NULL AND snail_id = gastropod_id)
        OR (snail_id IS NULL AND slug_id IS NOT NULL AND slug_id = gastropod_id)
    )    
);

CREATE TABLE snail (
    snail_id int PRIMARY KEY,
    FOREIGN KEY (snail_id) REFERENCES gastropod (snail_id) ON DELETE CASCADE
);

CREATE TABLE slug (
    slug_id int PRIMARY KEY,
    FOREIGN KEY (slug_id) REFERENCES gastropod (slug_id) ON DELETE CASCADE
); 

ALTER TABLE gastropod ADD FOREIGN KEY (snail_id) REFERENCES snail (snail_id)
    DEFERRABLE INITIALLY DEFERRED;

ALTER TABLE gastropod ADD FOREIGN KEY (slug_id) REFERENCES slug (slug_id)
    DEFERRABLE INITIALLY DEFERRED;

新数据插入如下:

START TRANSACTION;
INSERT INTO gastropod (gastropod_id, snail_id) VALUES (1, 1);
INSERT INTO snail (snail_id) VALUES (1);
COMMIT;

但是,尝试仅插入父级而不是子级会失败:

START TRANSACTION;
INSERT INTO gastropod (gastropod_id, snail_id) VALUES (2, 2);
COMMIT; -- FK violation.

插入错误的孩子会失败:

START TRANSACTION;
INSERT INTO gastropod (gastropod_id, snail_id) VALUES (2, 2);
INSERT INTO slug (slug_id) VALUES (2); -- FK violation.
COMMIT;

在父级中插入设置太少,太多或不匹配的字段也会失败:

INSERT INTO gastropod (gastropod_id) VALUES (2); -- CHECK violation.
...
INSERT INTO gastropod (gastropod_id, snail_id, slug_id) VALUES (2, 2, 2); -- CHECK violation.
...
INSERT INTO gastropod (gastropod_id, snail_id) VALUES (1, 2); -- CHECK violation.

在不支持延迟约束的DBMS上,可以声明性地强制执行排他性(但不存在):

enter image description here

在支持计算字段(例如Oracle 11虚拟列)的DBMS下,类型鉴别符type不需要物理存储在子表的级别(仅父表)。 / p>

对于不支持FK引用密钥的超集的DBMS,可能需要唯一约束U1(据我所知,几乎全部都是这样),所以我们人工制作这套超级套装。


是否所有这些实际上都应该在实践中完成是另一回事。这是在应用程序级别强制执行某些数据完整性方面的情况之一,可以通过减少开销和复杂性来证明这一点。

答案 11 :(得分:0)

理想情况下,我会制作一张餐桌"腹足类动物"使用"类型"田野,然后有观点"腹足类动物" (选择除&#34之外的所有字段;键入",没有"其中"子句),"蜗牛" (使用"其中"子句限制为蜗牛类型)," slug" (使用" where"子句限制类型slug)。如果两种类型中的一种更小并且有许多字段仅与较小的类型相关,则可能存在例外情况,但是大多数情况下使单个表中的不同视图将确保正确的完整性约束。