通过互斥子类在类型/子类型设计模式中实现子类型的子类型


20

介绍

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

我们的数据模型由3个实体,这应标明的ABC。为了使事情简单,它们的所有属性都是int类型。

实体A具有以下属性:DEX;

实体B具有以下属性:DEY;

实体C具有以下属性:DZ;

由于所有实体都具有相同的属性D,因此我决定采用类型/子类型设计。

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

问题:

实体AB具有另一个公共属性E,但是该属性在实体中不存在C

题:

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

老实说,我不知道如何执行此操作,也不知道从哪里开始尝试,因此不知道该帖子。

Answers:


6

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

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

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

就是说,您面临的选择是在以下之间进行选择,按照从最小化到最大化的顺序排列:

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

让我们看一下每个选项:

将属性移动E到基本类型的表

专业人士

  • 的查询的需要也减少了查询的复杂性E,但没有XYZ
  • 可能更有效的查询的需要E,但没有XYZ(尤其是聚合查询),由于没有JOIN。
  • 可能在其上创建索引(D, E)(如果允许,则可能(D, E)在EntityType <>的位置上创建过滤索引C,如果允许这种条件的话)

缺点

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

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

专业人士

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

缺点

  • 需要JOIN子类型化Table才能获得此属性
  • E由于JOIN的缘故,在需要时效率可能会略低,这取决于您拥有A+的多少行,B而不是+的多少行C
  • 稍有难度/复杂,与单纯的实体处理操作AB(和 C)为相同的“类型”。当然,您可以通过一个View 对它进行抽象,该View UNION ALL在的一个SELECTJOINed表A和另一个SELECT的JOINed表之间执行a B。这将降低SELECT查询的复杂性,但对INSERTUPDATE查询却没有太大帮助。
  • 根据特定的查询及其执行的频率,在无法建立索引的情况下,(D, E)如果确实可以帮助一个或多个频繁使用的查询,则可能会导致效率低下。

规范化E为基类和A&之间的中间表B

(请注意,根据情况,我确实喜欢@MDCCL的回答作为一种可行的选择。以下内容并不意味着对这种方法进行严格的批评,而是作为一种增加观点的手段-当然是我的-通过评估(与我已经提出的两个选项相同),这将使澄清我所认为的完全规范化与当前部分规范化方法之间的相对差异变得更加容易。

专业人士

  • 数据模型已完全规范化(考虑到RDBMS的设计目的,对此不会有任何固有的错误)
  • 减少了需要A和的查询的查询复杂度(B但不需要)C(即,不需要通过联接的两个查询UNION ALL

缺点

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

    这意味着,如果您需要查询常规基本信息,但需要按实体类型进行分类或筛选出一种或多种实体类型,则基类表没有足够的信息,在这种情况下,您需要LEFT JOINBar表。这也将降低索引FooTypeCode列的效率。

  • 没有一致的方法来与ABvs 进行交互C

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

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

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

  • 可能不适当的分组:

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

摘要

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

  • 每个EntityType您将拥有多少行(假设未来的增长速度高于平均水平,则至少要走5年)
  • 这些表(基本类型和子类型)在5年内将分别具有多少GB?
  • 什么特定的数据类型是属性 E
  • 它是仅一个属性还是几个或什至几个属性
  • 您将需要哪些查询E以及执行的频率
  • 您将不需要哪些查询E以及将执行多久一次

我认为我倾向于默认保留E单独的子类型表,因为它至少是“清洁器”。我会考虑移动E到基本类型表IF:大多数行都不是 EntityType的C行数是至少在百万; 我更经常高于并不是需要执行的查询E和/或会从索引中受益的查询(D, E)要么非常频繁执行和/或需要足够的系统资源,使得具有指数降低整体的资源利用,或至少防止资源消耗激增,超过可接受的水平或持续足够长的时间,从而导致过度阻塞和/或死锁增加。


更新

OP 对这个答案发表了评论

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

这项更改特别重要,因为这正是我所预测的,可能会在上面的“规范化E为基类和A&之间的中间表B”部分的“ CONs”小节中发生(第六个要点)。具体的问题是,当发生此类更改时(并且总是如此),重构数据模型有多么容易/困难。有人会争辩说任何数据模型都可以重构/更改,所以从理想开始。但是,尽管在技​​术层面上确实可以重构任何东西,但实际情况是规模问题。

资源不是无限的,不仅仅是CPU /磁盘/ RAM,还有开发资源:时间和金钱。由于这些资源非常有限,因此企业一直在确定项目的优先级。在很多情况下(至少以我的经验),提高效率的项目(甚至是系统性能以及更快的开发/更少的错误)都在增加功能的项目之下。虽然这使我们的技术人员感到沮丧,因为我们了解重构项目的长期利益是什么,但技术水平较低的业务人员更容易看到新功能与新功能之间的直接关系,这只是业务的本质收入。归结为:“我们稍后会再解决此问题” ==“

考虑到这一点,如果数据的大小足够小,使得可以非常查询更改,和/或您拥有一个足够长的维护窗口,不仅可以进行更改,而且还可以回滚(如果有的话)错误,然后将其归一化为E基类表和AB子类表之间的中间表可能会起作用(尽管仍然使您对特定类型没有直接的了解(AB)。但是,如果这些表中有数亿行,并且有大量的代码引用这些表(进行更改时必须测试的代码),那么通常比实用主义更为实用。这是我多年来必须处理的环境:基类表中的9.87亿行和615 GB,分布在18台服务器上。如此多的代码命中了这些表(基类表和子类表),因此存在很大的阻力(主要是来自管理层,但有时来自团队的其他成员),由于开发量和需要分配的质量检查资源。

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

请记住,无论选择哪种方式,至少要注意该模型在未来两年内的工作方式(如果可能)。即使是当时最好的主意,也要注意有效的方法和引起疼痛的原因(这意味着您还需要让自己接受搞砸的习惯-我们都愿意-这样才能诚实地评估痛点)。并注意某些决定为何起作用或不起作用,以便您下次可以做出 “好”的决定:-)。


17

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

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

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

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


@AlwaysLearningNewStuff我认为这个问题是对dba.stackexchange.com/questions/139092的跟进,对吗?在实现中,您确实具有表继承。
Remus Rusanu

是的,在问这个问题之前,我想确保我正确理解了如何首先实现类型/子类型设计。现在,当某些(但不是全部!)子类具有共享属性时,我会遇到上述问题。我想知道在那种情况下是否可以做一些优化数据模型的事情,而不是忽略这个细微差别……
AlwaysLearningNewStuff,2016年

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加法

我没加Foo,并Bar更好地使模型的样子,而是使之更具表现力。我认为由于以下原因,它们很重要:

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

  • 由于C仅与正在讨论的其余实体类型共享一个属性,即,D这一方面暗示它是另一种概念事件度量等的子实体类型,因此我通过以下方式描述了这种情况:在Foo超级实体类型。

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

设计阶段的重要因素

意识到以下事实是非常有用的:撇开所有术语,排他的超型-亚型群集是普通关系。让我们以以下方式描述情况:

  • 每个排他上位实体类型出现都只与一个子实体类型补语相关。

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

从前面的帖子中可以知道,在创建具有这种性质的关联时,discriminator属性(在实现时为列)起着至关重要的作用,因为它指示了超类型所连接的正确子类型实例。主键从(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)  
);

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

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

实现数据库后,必须确保(a)每个排他超类型行始终由其对应的子类型对应项补充,并相应地确保(b)此类子类型行与超类型区分符列中包含的值兼容。因此,使用ACID TRANSACTIONS来确保在数据库中满足这些条件是非常方便的。

您不应该放弃数据库的逻辑健全性,自我表达能力和准确性,而这些方面绝对可以使您的数据库更加牢固。

先前发布的两个答案已经包含了相关的观点,在设计,创建和管理数据库及其应用程序时肯定值得考虑。

通过VIEW定义检索数据

您可以设置一些视图,以合并不同超类型-子类型组的列,以便可以检索手头的数据,而无需每次例如编写必要的JOIN子句。这样,您可以轻松地直接从感兴趣的VIEW(派生关系)中进行选择。

如您所见,毫无疑问,“泰德·科德”是个天才。他遗赠的工具非常强大,优雅,而且彼此之间的集成度很高。

相关资源

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


注意

1. 信息建模集成定义IDEF1X)是一种高度推荐的数据建模技术,它是由美国国家标准技术研究院(NIST)于1993年12月建立为标准的。它牢固地基于(a) EF Codd博士撰写的早期理论材料;上(b)中实体关系数据,通过开发的视图PP五福 ; 以及(c) Robert G. Brown创建的逻辑数据库设计技术。值得注意的是,IDEF1X是通过一阶逻辑形式化的。


我的老板改变了商业逻辑,消除E干脆!之所以接受用户srutzky的答案,是因为它提供了许多优点,可以帮助我做出选择最有效路线的决定。如果不是那样,我会接受你的回答。我已对您的回答进行了投票。再次感谢!
AlwaysLearningNewStuff '16
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.