在具有互斥子类的类型/子类型设计模式中实现子类型的子类型

Alw*_*uff 21 database-design sql-server sql-server-2012 subtypes

介绍

为了让这个问题对未来的读者有用,我将使用通用数据模型来说明我面临的问题。

我们的数据模型由 3 个实体组成,它们应标记为ABC。为了简单起见,它们的所有属性都将是int类型。

实体A具有以下属性:D,EX;

实体B具有以下属性:D,EY;

实体C具有以下属性:DZ

由于所有实体共享公共属性D,我决定应用类型/子类型设计。

重要提示:实体是互斥的!这意味着实体是 A 或 B 或 C。

问题:

实体AB还有另一个共同的属性E,但是这个属性并不存在于实体中C

题:

如果可能的话,我想使用上述特性来进一步优化我的设计。

老实说,我不知道如何做到这一点,也不知道从哪里开始尝试,因此这篇文章。

Rem*_*anu 20

根据 Martin Fowler 的说法,有 3 种方法可以解决表继承问题:

  • 单表继承:一张表代表所有类型。未使用的属性为 NULL。
  • 具体表继承:每个具体类型一个表,每个表列对应类型的每个属性。表之间没有关系。
  • 类表继承:每种类型一个表,每个表只有新的、非继承的属性的属性。表是相关的,反映了实际的类型继承层次结构。

您可以从这些开始,以搜索每种方法的优缺点。其要点是所有方法都有主要缺点,没有任何方法具有压倒性优势。更好地称为对象关系阻抗失配,这个问题尚未找到解决方案。

就我个人而言,我发现糟糕的关系设计可能导致的问题类型比糟糕的类型设计导致的问题严重几个数量级。糟糕的数据库设计会导致查询缓慢、更新异常、数据大小爆炸、死锁和应用程序无响应,以及数十到数百 GB 的数据以错误的格式沉没。糟糕的类型设计导致难以维护和更新代码,而不是运行时。因此,在我的书中,正确的关系设计一遍又一遍地胜过任何 OO 类型的纯度。


Sol*_*zky 7

只要这个问题是我的类型/子类型设计模式(对于互斥子类)的实现是否正确?,这本身就是不知道如何将变量实体转换为关系表的延续,我会问:到底想优化什么?贮存?对象模型?查询复杂度?查询性能?优化一个方面与另一个方面需要权衡,因为您无法同时优化所有方面。

我完全同意Remus关于以下几点的观点

  • 每种方法都有利有弊(即始终存在的“视情况而定”因素),并且
  • 第一要务是数据模型的效率(低效的数据模型无法通过干净和/或高效的应用程序代码来纠正)

也就是说,您面临的选择是以下选项,按最小归一化到最大归一化的顺序排列:

  • 将属性E提升到基类型表
  • 将其保存在多个子类型表中
  • 完全正常化E了一个新的,中介子类表上相同的水平C,这AB将直接的子类(@ MDCCL的答案

让我们看看每个选项:

将属性移动E到基类型表

专业人士

  • 降低了需要E但不需要XY、 或 的查询的查询复杂性Z
  • 由于没有 JOIN ,对于需要E但不需要、 或(特别是聚合查询)的查询可能更有效。XYZ
  • 有可能在上创建索引(D, E)(如果是,则可能(D, E)在 EntityType <>上创建过滤索引C,如果允许这样的条件)

缺点

  • 不能标记ENOT NULL
  • 需要额外CHECK CONSTRAINT的基本类型表以确保E IS NULL当 EntityType = C(虽然这不是一个大问题)
  • 当 EntityType = 时,需要教育数据模型的用户为什么E必须NULL,甚至应该完全忽略C
  • E是固定长度类型时效率稍低,并且大部分行用于 EntityType C(即不使用E因此它是NULL),并且不使用SPARSE列上的选项或聚集索引上的数据压缩
  • 对于不需要的查询可能效率较低,E因为E基本类型表中的 会增加每行的大小,这反过来又会减少可以容纳在数据页上的行数。但这在很大程度上取决于 的确切数据类型E、FILLFACTOR、基类型表中有多少行等。

E在每个子类型表中保留属性

专业人士

  • 更清晰的数据模型(即不必担心教育他人为什么E不应该使用基类型表中的列,因为“它确实不存在”)
  • 可能更接近于对象模型
  • 可以将列标记为NOT NULL实体的必需属性
  • 不需要额外CHECK CONSTRAINT的基类型表来确保E IS NULL当 EntityType = C(虽然这不是一个巨大的收益)

缺点

  • 需要 JOIN 子类型 Table(s) 才能获得此属性
  • E由于 JOIN,需要时可能效率稍低,这取决于您拥有多少行A+B而不是C有多少行。
  • 对于仅处理实体AB(而不是 C)作为相同“类型”的操作,稍微困难/复杂一些。当然,你可以通过查看,做了这个抽象UNION ALL之间SELECT的连接表A,另一个SELECT用于连接表的B。这将降低 SELECT 查询的复杂性,但对INSERTUPDATE查询没有太大帮助。
  • 根据特定查询以及它们执行的频率,这可能是一种潜在的低效率,在这种情况下,建立索引(D, E)确实有助于一个或多个经常使用的查询,因为它们不能一起编制索引。

标准化E为基类和A&之间的中间表B

(请注意,我确实喜欢@MDCCL 的答案作为可行的替代方案,具体取决于具体情况。以下内容并不是对这种方法的严格批评,而是作为添加一些观点的一种手段——当然是我的——通过评估它与我已经提出的两个选项处于相同的上下文中。这将更容易澄清我所看到的完全规范化和当前部分规范化方法之间的相对差异。)

专业人士

  • 数据模型是完全规范化的(考虑到 RDBMS 的设计目的,这不会有任何本质上的错误)
  • 降低了需要Aand 的查询的查询复杂性B,但不是C(即不需要通过 加入的两个查询UNION ALL

缺点

  • 占用的空间稍多(Bar表重复了 ID,并且有一个新列,BarTypeCode)[可以忽略不计,但需要注意]
  • 查询复杂性略有增加,因为需要额外JOIN的东西才能访问AB
  • 增加了锁定的表面积,主要是INSERTDELETE可以通过将外键标记为隐式处理ON CASCADE DELETE)因为事务将在基类表上保持打开时间稍长(即Foo)[可以忽略不计,但需要注意]
  • 没有实际类型的直接知识 -AB- 在基类表中,Foo;它只知道Br可以是A或的类型B

    意思是,如果您需要对一般基本信息进行查询,但需要按实体类型分类或过滤掉一个或多个实体类型,则基类表没有足够的信息,在这种情况下,您需要LEFT JOINBar表。这也会降低索引FooTypeCode列的有效性。

  • A& Bvs交互没有一致的方法C

    意思是,如果每个实体都直接与基类表相关,以至于只有一个 JOIN 才能获得完整的实体,那么每个人都可以更快、更轻松地熟悉数据模型。将有一种通用的查询/存储过程方法,这使它们可以更快地开发并且不太可能出现错误。一致的方法还使将来添加新的子类型变得更快、更容易。

  • 可能不太适应随时间变化的业务规则:

    意思是,事情总是在变化,E如果它对所有子类型变得通用,那么向上移动到基类 Table是相当容易的。如果实体性质的变化使其成为值得的更改,那么将公共属性移出子类型也很容易。将一个子类型分解为两个子类型(只需创建另一个SubTypeID值)或将两个或多个子类型合并为一个是很容易的。相反,如果E后来成为所有子类型的共同属性怎么办?那么Bar表的中间层就没有意义了,增加的复杂性也不值得。当然,这种变化是否会在5年甚至10年内发生是不可能知道的,所以该Bar不一定,甚至不太可能是一个坏主意(这就是为什么我说“可能不太适应”)。这些只是需要考虑的要点;这在任何一个方向都是一场赌博。

  • 可能不适当的分组:

    意思是,仅仅因为E属性在实体类型之间共享AB并不意味着A并且B 应该组合在一起。仅仅因为事物“看起来”相同(即相同的属性)并不意味着它们相同。

概括

就像决定是否/何时进行非规范化一样,如何最好地处理这种特殊情况取决于考虑数据模型使用的以下方面,并确保收益大于成本:

  • 每个 EntityType 将有多少行(假设增长高于平均水平,至少 5 年后)
  • 这些表(基本类型和子类型)在 5 年内有多少 GB?
  • 什么特定的数据类型是属性 E
  • 它是只有一个属性还是有几个,甚至几个属性
  • 您需要哪些查询E以及它们执行的频率
  • 您需要哪些不需要的查询E以及它们执行的频率

我想我倾向于默认保留E在单独的子类型表中,因为它至少“更干净”。我会考虑转移E到基类型表 IF:大多数行不是针对 EntityType 的C并且行数至少为数百万;并且我经常执行需要E的查询和/或将从索引中受益的查询(D, E)执行非常频繁和/或需要足够的系统资源,这样索引会降低整体资源利用率,或至少防止超出可接受水平或持续时间足够长以导致过度阻塞和/或死锁增加的资源消耗激增。


更新

OP对这个答案的评论是:

我的雇主改变了业务逻辑,完全删除了 E!

这种变化特别重要,因为这正是我在上面的“E在基类和A&之间规范化到中间表B”部分(第 6 个要点)的“CONs”小节中预测的可能发生的情况。具体问题是在发生此类更改时重构数据模型的难易程度(而且它们总是如此)。有些人会争辩说任何数据模型都可以重构/更改,所以从理想开始。但是,虽然在技​​术层面上任何事情都可以重构,但实际情况是规模问题。

资源不是无限的,不仅仅是CPU/磁盘/RAM,还有开发资源:时间和金钱。企业不断为项目设定优先级,因为这些资源非常有限。而且很多时候(至少以我的经验),提高效率的项目(甚至是系统性能和更快的开发/更少的错误)优先于增加功能的项目。虽然这让我们技术人员感到沮丧,因为我们了解重构项目的长期好处是什么,但技术含量较低的业务人员更容易看到新功能和新功能之间的直接关系,这正是业务的本质。收入。这归结为:“我们稍后会回来解决这个问题”==“

考虑到这一点,如果数据的大小足够小,以至于可以非常查询地进行更改,和/或您有一个足够长的维护窗口,不仅可以进行更改,还可以在出现问题时回滚错误,然后标准化E为基类表和A&B子类表之间的中间表可以工作(尽管这仍然使您无法直接了解特定类型(AB) 在基类表中)。但是,如果这些表中有数亿行,并且引用这些表的代码数量惊人(在进行更改时必须测试代码),那么通常比理想主义更实用。这就是我多年来不得不处理的环境:基类表中有 9.87 亿行和 615 GB,分布在 18 个服务器上。如此多的代码命中这些表(基类和子类表)以致于有很多阻力 - 主要来自管理层,但有时来自团队的其他成员 - 由于开发量和需要分配的质量保证资源。

因此,再次重申,“最佳”方法只能根据具体情况确定:您需要了解您的系统(即有多少数据以及表和代码如何关联)、如何完成重构以及人员与您合作的(您的团队和可能的管理层——您能得到他们对这样一个项目的支持吗?)。我已经提到并计划了 1 到 2 年的一些更改,并且进行了多次冲刺/发布以实现其中的 85%。但是,如果您只有 < 100 万行并且没有很多代码与这些表相关联,那么您可能能够从更理想/“纯”的一面开始。

请记住,无论您选择哪种方式,请至少注意该模型在未来 2 年内的运作方式(如果可能)。注意什么有效,什么引起了痛苦,即使它在当时看起来是最好的主意(这意味着你也需要让自己接受失败——我们都这样做——这样你才能诚实地评估痛点)。并注意为什么某些决定有效或无效,以便您下次做出更有可能“更好”的决定:-)。


MDC*_*CCL 6

根据我对您的规范的解释,您想找到一种方法来实现两种不同(但相互关联)的超类型-子类型结构。

为了揭示一种实现上述任务的方法,我将在有问题的场景中添加两个称为和 的经典假设实体类型,我将在下面详细介绍。FooBar

商业规则

以下是一些有助于我创建逻辑模型的语句:

  • A Foo is either one Bar or one C
  • A Foo is categorized by one FooType
  • A Bar is either one A or one C
  • A Bar is classified by one BarType

逻辑模型

然后,生成的 IDEF1X [1]逻辑模型如图 1所示(您也可以从 Dropbox 以 PDF 格式下载):

图 1 - 假设的超类型-子类型关系数据模型

Foo 和 Bar 添加

我没有添加FooandBar使模型看起来更好,而是使其更具表现力。我认为它们很重要,原因如下:

  • 由于AB共享名为 的属性E,此功能表明它们是不同(但相关)类型的概念事件人物测量等的Bar子实体类型,我通过超实体类型表示,反过来,的子实体类型Foo,它将D属性保存在层次结构的顶部。

  • 由于C仅与正在讨论的其余实体类型共享一个属性,即,D这方面暗示它是另一种概念事件人物度量等的子实体类型,因此我凭借在Foo超级实体类型。

然而,这些只是假设,由于关系数据库旨在准确反映特定业务上下文的语义,因此您必须识别和分类特定领域中所有感兴趣事物,以便您可以准确地捕捉更多含义.

设计阶段的重要因素

意识到这样一个事实非常有用,抛开所有术语不谈,排他的超类型-子类型集群是一种普通关系。让我们用以下方式描述这种情况:

  • 每个独占超实体类型出现仅与一个子实体类型补充相关。

因此,在这些情况下存在一对一 (1:1) 的对应关系(或基数)。

正如您从之前的帖子中了解到的,鉴别器属性(列,实施时)在创建这种性质的关联时起着至关重要的作用,因为它指示与超类型连接的正确子类型实例。PRIMARY KEY 从 (i) 超类型到 (ii) 子类型的迁移也具有重要意义。

具体的 DDL 结构

然后我写了一个基于上述逻辑模型的 DDL 结构:

CREATE TABLE FooType -- Look-up table.
(
    FooTypeCode     CHAR(2)  NOT NULL,
    Description     CHAR(90) NOT NULL, 
    CreatedDateTime DATETIME NOT NULL,
    CONSTRAINT PK_FooType             PRIMARY KEY (FooTypeCode),
    CONSTRAINT AK_FooType_Description UNIQUE      (Description)
);

CREATE TABLE Foo -- Supertype
(
    FooId           INT      NOT NULL, -- This PK migrates (1) to ‘Bar’ as ‘BarId’, (2) to ‘A’ as ‘AId’, (3) to ‘B’ as ‘BId’, and (4) to ‘C’ as ‘CId’.
    FooTypeCode     CHAR(2)  NOT NULL, -- Discriminator column.
    D               INT      NOT NULL, -- Column that applies to ‘Bar’ (and therefore to ‘A’ and ‘B’) and ‘C’.
    CreatedDateTime DATETIME NOT NULL,
    CONSTRAINT PK_Foo                 PRIMARY KEY (FooId),
    CONSTRAINT FK_from_Foo_to_FooType FOREIGN KEY (FooTypeCode)
        REFERENCES FooType (FooTypeCode)
);

CREATE TABLE BarType -- Look-up table.
(
    BarTypeCode CHAR(1)  NOT NULL,  
    Description CHAR(90) NOT NULL,  
    CONSTRAINT PK_BarType             PRIMARY KEY (BarTypeCode),
    CONSTRAINT AK_BarType_Description UNIQUE      (Description)
);

CREATE TABLE Bar -- Subtype of ‘Foo’.
(
    BarId       INT     NOT NULL, -- PK and FK.
    BarTypeCode CHAR(1) NOT NULL, -- Discriminator column. 
    E           INT     NOT NULL, -- Column that applies to ‘A’ and ‘B’.
    CONSTRAINT PK_Bar             PRIMARY KEY (BarId),
    CONSTRAINT FK_from_Bar_to_Foo FOREIGN KEY (BarId)
        REFERENCES Foo (FooId),
    CONSTRAINT FK_from_Bar_to_BarType FOREIGN KEY (BarTypeCode)
        REFERENCES BarType (BarTypeCode)    
);

CREATE TABLE A -- Subtype of ‘Bar’.
(
    AId INT NOT NULL, -- PK and FK.
    X   INT NOT NULL, -- Particular column.  
    CONSTRAINT PK_A             PRIMARY KEY (AId),
    CONSTRAINT FK_from_A_to_Bar FOREIGN KEY (AId)
        REFERENCES Bar (BarId)  
);

CREATE TABLE B -- (1) Subtype of ‘Bar’ and (2) supertype of ‘A’ and ‘B’.
(
    BId INT NOT NULL, -- PK and FK.
    Y   INT NOT NULL, -- Particular column.  
    CONSTRAINT PK_B             PRIMARY KEY (BId),
    CONSTRAINT FK_from_B_to_Bar FOREIGN KEY (BId)
        REFERENCES Bar (BarId)  
);

CREATE TABLE C -- Subtype of ‘Foo’.
(
    CId INT NOT NULL, -- PK and FK.
    Z   INT NOT NULL, -- Particular column.  
    CONSTRAINT PK_C             PRIMARY KEY (CId),
    CONSTRAINT FK_from_C_to_Foo FOREIGN KEY (FooId)
        REFERENCES Foo (FooId)  
);
Run Code Online (Sandbox Code Playgroud)

使用这种结构,您可以避免在基表(或关系)中存储 NULL 标记,这会给您的数据库带来歧义。

完整性、一致性和其他注意事项

一旦你实现了你的数据库,你必须确保 (a) 每个独占超类型行总是由其对应的子类型对应物补充,反过来,保证 (b) 这样的子类型行与超类型鉴别器列中包含的值兼容. 因此,使用 ACIDTRANSACTIONS来确保在您的数据库中满足这些条件是非常方便的。

您不应该放弃数据库的逻辑健全性、自我表达性和准确性,这些方面无疑会使您的数据库更加可靠。

之前发布的两个答案已经包含了在设计、创建和管理数据库及其应用程序时当然值得考虑的相关要点。

通过 VIEW 定义检索数据

您可以设置一些视图来组合不同超类型-子类型组的列,这样您就可以检索手头的数据,而无需每次都编写必要的 JOIN 子句。通过这种方式,您可以轻松地从感兴趣的 VIEW(派生关系)中直接 SELECT 。

如您所见,“Ted”Codd 无疑是一位天才。他遗留下来的工具都相当坚固和优雅,当然也很好地融合在一起。

相关资源

如果您想分析一些涉及超类型-子类型关系的广泛数据库,您会发现@PerformanceDBA对以下堆栈溢出问题提出的非凡答案很有价值:


笔记

1. 对信息建模集成定义IDEF1X)是被确立为一个非常可取的数据建模技术标准通过标准的美国国家技术研究所(1993年12月NIST)。它完全基于(a) EF Codd 博士撰写的早期理论材料;关于(b) 数据实体关系视图,由PP Chen 博士开发;以及(c)逻辑数据库设计技术,由 Robert G. Brown 创建。值得注意的是,IDEF1X 是通过一阶逻辑形式化的。


归档时间:

查看次数:

3922 次

最近记录:

9 年,3 月 前