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

What is the best way to enforce a 'subset' relationship with integrity constraints

例如,给出3个表:

  • 腹足纲
  • 蜗牛
  • 蛞蝓

假设我们要强制执行

  • "腹足类"中的每一行在"蜗牛"或"鼻涕虫"中都有一个对应的行(但不是两者都有)。
  • "slug"中的每一行在"gastopod"中只有一个对应的行。
  • "蜗牛"中的每一行都有一个对应的"腹足动物"行。
  • 设置模式以强制执行这些约束的最佳方法是什么?

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

    编辑以下回答/评论中的问题可供参考,以解决类似问题:

    • 在关系数据库中维护子类完整性
    • 数据库设计-文章、博客文章、照片、故事
    • 来自数据库中不同实体的相同数据-最佳实践-电话号码示例


    我自己的Postgres解决方案(但我不知道这是否是最好的方法):

    枚举:

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

    表和约束:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    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;

    测试:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    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)
      );

    所以你要检查腹足动物是蜗牛还是鼻涕虫,鼻涕虫和蜗牛都是固定的。如果它是蜗牛,那么"蜗牛ID"必须为空,而对于"蜗牛ID"则必须为空。确保鼻涕虫和蜗牛的ID是唯一的(我已经添加了与腹足类匹配的检查)。

    1
    2
    3
    4
    5
    CREATE TABLE SNAIL
      (SNAIL_ID NUMBER,
       CONSTRAINT SNAIL_PK PRIMARY KEY (SNAIL_ID),
       CONSTRAINT SNAIL_FK FOREIGN KEY (SNAIL_ID)
         REFERENCES GASTROPOD (SNAIL_ID));

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

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

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


    "假设我们要强制执行(1)"腹足类"中的每一行在"蜗牛"或"鼻涕虫"中只有一个对应的行(但不是两者都有)。(2)"slug"中的每一行在"gastopod"中只有一个对应的行。(3)"蜗牛"中的每一行在"腹足类"中只有一个对应的行。"

    (1)是"腹足类"和虚拟relvar(又名"视图")之间的包含依赖关系(又名"外键依赖关系"),定义为slug union蜗牛。(2)和(3)是"鼻涕虫"(/"蜗牛")和"腹足类"之间相同的包涵体依赖性。

    所有这些加在一起意味着"腹足类"和"鼻涕虫结合蜗牛"(至少就标识符而言)之间存在"平等依赖性"。

    请注意,为了能够更新受此类约束的数据库,您可能需要一个支持称为"多重分配"的DBMS引擎,或者一个支持"延迟约束检查"的引擎。

    《数据库专业人员应用数学》一书的第11章深入探讨了如何在SQL环境中实施这些约束(事实上,只是任何约束,不管多么复杂)。你的问题的答案几乎是那一章的全部内容,我希望你不要指望我用几句话来概括这一切(答案的本质是"触发器"——正如StarShip3000所指出的那样)。


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


    我知道这个问题是一个超类型/子类型的问题。我已经写过好几次了。在本文中,它是针对员工、客户和供应商问题的解决方案。但这篇文章在理论基础和约束是如何工作的背后进行了最广泛的讨论。它是根据在线出版物写的。


    SQL的一个问题是它对完整性约束,特别是引用约束的支持水平很差。

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

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


    检查这个线程:在关系数据库中维护子类的完整性

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


    理想情况下,我会制作一个带有"类型"字段的单表"腹足类",然后查看"腹足类"(选择除"类型"之外的所有字段,不带"Where"子句)、"Snail"(使用"Where"子句限制为Snail类型)和"Slug"(使用"Where"子句限制为Slug类型)。如果两种类型中的一种小得多,并且有许多字段只与较小的类型相关,则可能存在异常,但在大多数情况下,使其与单个表的视图不同将确保适当的完整性约束。


    这里有两个问题:

    • 存在:父行不能至少有一个子行。
    • 排他性:父行不能有多个子行。

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

    enter image description here

    gastropod.snail_idsnail.snail_id之间以及gastropod.slug_idslug.slug_id之间有一个循环外键。还有一个检查确保其中一个与gastropod.gastropod_id完全匹配(另一个为空)。

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

    以下是在PostgreSQL中实现的方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    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;

    按如下方式插入新数据:

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

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

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

    插入错误类型的子项失败:

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

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

    1
    2
    3
    4
    5
    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上,独占性(但不存在)可以这样声明性地强制执行:

    氧化镁

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

    对于不支持FK引用超键集(据我所知,几乎所有超键集)的DBMS,可能需要唯一的约束U1,因此我们人为地创建了这个超集。

    这一切是否真的应该在实践中完成是另一回事。这是其中一种情况,通过减少开销和复杂性,在应用程序级别强制实现数据完整性的某些方面可能是合理的。


    所有这些例子都有一个极其复杂的层次,对于一些如此简单的事情来说:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    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)

    在你说这不是一个答案之前,先考虑一下需求。

  • "腹足类"中的每一行在"蜗牛"或"鼻涕虫"中都有一个对应的行(但不是两者都有)。
  • "slug"中的每一行在"gastopod"中只有一个对应的行。
  • "蜗牛"中的每一行都有一个对应的"腹足动物"行。
  • 将列放在表中是数据完整性的等价物,没有任何胡说八道。

    注意:DDL中的LIKE可以将所有列(甚至9.0中的约束和索引)复制到新表中。所以你可以伪造遗产。


    "@erwin我更喜欢不涉及触发器的解决方案——我对它们有病态的厌恶。"

    很抱歉有新的答案,未授权对此添加评论。

    据我所见,在您的特定情况下,由于您希望施加的约束的性质,您可能能够摆脱"只是使用延迟约束"。如果它对你有效,你也很满意,那么一切都是好的,不是吗?

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

    无论你的数据库管理系统,我还是任何一个精通关系理论的人,都不会关心你身上的任何病状。但是,也许正是由于你提到的这些病状,如果你使用我自己开发的DBMS(它确实支持类似触发器的东西,但是你不需要借助它们来增强数据完整性),那么观察你可以做你想要的所有事情,而不必定义任何触发器,这可能会很有趣。


    外键引用来自slug和snail的腹足类,在外键列上有一个唯一的索引,执行规则2和3。但是规则1更复杂:-(

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

    顺便问一下-您打算如何插入数据?不管你按什么顺序做,你都会违反规则。