【问题标题】:Designing a conditional database relationship in SQL Server在 SQL Server 中设计条件数据库关系
【发布时间】:2010-01-21 21:03:23
【问题描述】:

我有三种基本类型的实体:人员、企业和资产。每项资产可由一个且只有一个个人或企业拥有。每个人和企业可以拥有从 0 到多个资产。在 Microsoft SQL Server 中存储此类条件关系的最佳做法是什么?

我最初的计划是在 Assets 表中有两个可以为空的外键,一个用于 People,一个用于 Business。这些值之一将为空,而另一个将指向所有者。我看到这个设置的问题是它需要应用程序逻辑才能被解释和执行。这真的是最好的解决方案还是有其他选择?

【问题讨论】:

    标签: sql-server database database-design


    【解决方案1】:

    介绍超类型和子类型

    我建议您使用超类型和子类型。首先,创建PartyTypeParty 表:

    CREATE TABLE dbo.PartyType (
       PartyTypeID int NOT NULL identity(1,1) CONSTRAINT PK_PartyType PRIMARY KEY CLUSTERED
       Name varchar(32) CONSTRAINT UQ_PartyType_Name UNIQUE
    );
    
    INSERT dbo.PartyType VALUES ('Person'), ('Business');
    

    超类型

    CREATE TABLE dbo.Party (
       PartyID int identity(1,1) NOT NULL CONSTRAINT PK_Party PRIMARY KEY CLUSTERED,
       FullName varchar(64) NOT NULL,
       BeginDate smalldatetime, -- DOB for people or creation date for business
       PartyTypeID int NOT NULL
          CONSTRAINT FK_Party_PartyTypeID FOREIGN KEY REFERENCES dbo.PartyType (PartyTypeID)
    );
    

    子类型

    然后,如果有一个 Person 唯一的列,则创建一个 Person 表,其中仅包含这些列:

    CREATE TABLE dbo.Person (
       PersonPartyID int NOT NULL
          CONSTRAINT PK_Person PRIMARY KEY CLUSTERED
          CONSTRAINT FK_Person_PersonPartyID FOREIGN KEY REFERENCES dbo.Party (PartyID)
             ON DELETE CASCADE,
       -- add columns unique to people
    );
    

    如果存在企业独有的列,请创建一个仅包含这些列的 Business 表:

    CREATE TABLE dbo.Business (
       BusinessPartyID int NOT NULL
          CONSTRAINT PK_Business PRIMARY KEY CLUSTERED
          CONSTRAINT FK_Business_BusinessPartyID FOREIGN KEY REFERENCES dbo.Party (PartyID)
             ON DELETE CASCADE,
       -- add columns unique to businesses
    );
    

    使用和注意事项

    最后,您的Asset 表将如下所示:

    CREATE TABLE dbo.Asset (
       AssetID int NOT NULL identity(1,1) CONSTRAINT PK_Asset PRIMARY KEY CLUSTERED,
       PartyID int NOT NULL
          CONSTRAINT FK_Asset_PartyID FOREIGN KEY REFERENCES dbo.Party (PartyID),
       AssetTag varchar(64) CONSTRAINT UQ_Asset_AssetTag UNIQUE
    );
    

    超类型 Party 表与子类型表 Business 和 Person 共享的关系是“一对零或一”。现在,虽然子类型通常在另一个表中没有对应的行,但在这种设计中,有可能在两个表中都有一个 Party。但是,您实际上可能喜欢这样:有时一个人和一个企业几乎可以互换。如果没有用,虽然执行此操作的触发器很容易完成,但最好的解决方案可能是将 PartyTypeID 列添加到子类型表中,使其成为 PK 和 FK 的一部分,并在 @ 上放置一个 CHECK 约束987654337@.

    此模型的美妙之处在于,当您要创建一个对企业或个人有约束的列时,您可以对适当的表而不是聚会表进行约束。

    此外,如果需要,在约束上打开级联删除可能很有用,以及在子类型表上使用 INSTEAD OF DELETE 触发器来代替从超类型表中删除相应的 ID(这保证没有超类型行没有存在子类型行)。这些查询非常简单,可以在整个行存在或不存在的级别上工作,在我看来,这对任何需要检查列值一致性的设计来说都是一个巨大的改进。

    另外,请注意,在许多情况下,您认为应该放在其中一个子类型表中的列实际上可以组合到超类型表中,例如社会保险号。将其称为 TIN(纳税人识别号),它适用于企业和个人。

    ID 列命名

    是否将Person表中的列称为PartyIDPersonIDPersonPartyID的问题是您自己的喜好,但我认为最好将这些称为PersonPartyIDBusinessPartyID——容忍较长名称的成本,这避免了两种类型的混淆。例如,不熟悉数据库的人看到BusinessID 并且不知道这是PartyID,或者看到PartyID 并且不知道它被外键限制为仅限Business 表中的那些。

    如果您想为PartyBusiness 表创建视图,它们甚至可以是物化视图,因为它是一个简单的内部连接,您可以重命名PersonPartyIDPersonID 如果你真的这么想的话(虽然我不会)。如果它对您很有价值,您甚至可以在这些视图上创建INSTEAD OF INSERTINSTEAD OF UPDATE 触发器来为您处理对这两个表的插入,使视图在许多应用程序中看起来完全像它们自己的表。

    使您提议的设计按原样工作

    另外,我不想提它,但如果你想在你提议的设计中设置一个约束,强制只填写一列,下面是代码:

    ALTER TABLE dbo.Assets
    ADD CONSTRAINT CK_Asset_PersonOrBusiness CHECK (
       CASE WHEN PersonID IS NULL THEN 0 ELSE 1 END
       + CASE WHEN BusinessID IS NULL THEN 0 ELSE 1 END = 1
    );
    

    但是,我不推荐这种解决方案。

    最后的想法

    要添加的第三个自然子类型是组织,即人们和企业可以拥有成员资格。超类型和子类型还可以优雅地解决客户/员工、客户/供应商以及与您提出的问题类似的其他问题。

    注意不要将“Is-A”与“Acts-As-A”混淆。您可以通过查看您的订单表或查看订单计数来判断一方是客户,并且可能根本不需要客户表。也不要将身份与生命周期混淆:租赁的汽车最终可能会被出售,但这是生命周期的进展,应该使用列数据而不是表格存在来处理——汽车不是以 @987654355 开始的@ 然后变成ForSaleCar,它一直都是汽车。或者RentalItem,也许企业也会租用其他东西。你明白了。

    甚至可能不需要PartyType 表。派对类型可以通过相应子类型表中是否存在一行来确定。这也将避免PartyTypeID 与子类型表存在不匹配的潜在问题。一种可能的实现是保留PartyType 表,但从Party 表中删除PartyTypeID,然后在Party 表的视图中根据哪个子类型表具有相应的行返回正确的PartyTypeID。如果您选择允许双方同时成为两个子类型,这将不起作用。然后你会坚持使用子类型视图并知道BusinessIDPersonID 的相同值指的是同一方。

    进一步阅读此模式

    请参阅A Universal Person and Organization Data Model 以获得更完整的理论处理。

    我最近发现以下文章对于描述在数据库中建模继承的一些替代方法很有用。尽管特定于 Microsoft 的实体框架 ORM 工具,但您没有理由不能在任何数据库开发中自己实现这些:

    附:由于我有更多的经验,我不止一次地改变了我对子类型表中 ID 的列命名的看法。

    【讨论】:

    • 我需要更快地打字……当我开始回复时,只有 HLGEM 发布了。叹息。
    • 你输入了很多。出于对您对数据库架构设计的出色解释的尊重,我删除了我的帖子。
    • 你太体贴了!
    • @ErikE 我真的很高兴看到可以解决该要求,但是,我想知道:实现它真的是一个好主意,或者它会更好只创建 PersonAsset 和 BusinessAsset 表?
    • @oScarDiAnno 如果您没有Assets 表,您将如何对Asset 实施参照完整性?您将如何在系统的某些部分中使用资产,而不关心它是什么种类 资产?如果您没有Asset 表,您将如何添加新的资产类型而无需接触数据库中与资产一起使用的每一段代码
    【解决方案2】:

    您不需要应用程序逻辑来强制执行此操作。最简单的方法是使用 检查约束

    (PeopleID is null and BusinessID is not null) or (PeopleID is not null and BusinessID is null)
    

    【讨论】:

    • 我实际上比我更喜欢你的检查约束,因为只有两列。我的更适合扩展到任意数量的列——只需再添加一个子表达式。对于您的版本,添加另一列将需要更改每个子表达式。想象一下从 5 列变为 6 列...
    【解决方案3】:

    您可以拥有另一个实体,从中“扩展”个人和业务。我们在当前项目中将此实体称为 Party。个人和企业都有 FK to Party(is-a 关系)。并且 Asset 可能也有 FK to Party(属于关系)。

    话虽如此,如果将来一个 Asset 可以由多个实例共享,最好创建 m:n 关系,它提供了灵活性,但会使应用程序逻辑和查询更加复杂。

    【讨论】:

    • 也感谢您提出这个建议。这就是“做对了”。
    【解决方案4】:

    ErikE's answer 很好地解释了如何处理表中的超类型/子类型关系,这可能是我在你的情况下会去的,但是,它并没有真正解决你的问题' ve提出的也很有趣,即:

    1. 在 Microsoft SQL Server 中存储此类条件关系的最佳做法是什么?
    2. ...还有其他选择吗?

    对于那些我推荐 this blog entry on TechTarget 的人,它摘自 Eric Johnson 和 Joshua Jones 的“SQL Server 数据建模开发人员指南,涵盖 SQL Server 2005 和 2008”,其中涉及 3 个可能的选项。

    总结起来就是:

    1. 超类型表 - 几乎符合您的建议,有一个表,其中一些字段在填充其他字段时始终为空。当只有几个字段不共享时很好。因此,根据 Business 和 People 的不同程度,您可以将它们组合到一个表中,可能是 Owners,然后在 Asset 表中添加 OwnerID。
    2. 子类型表 - 基本上与超类型表相反,并且是您现在所拥有的。在这里,我们有很多独特的字段,可能有一两个相同的字段,所以我们只是让重复的字段出现在每个表中。您发现这并不适合您的情况。
    3. 超类型和子类型表 - 上述两种方法的组合,其中匹配字段放置在单个表中,而唯一的字段放置在单独的表中,并且匹配的 ID 用于将记录从一个表连接到其他。这与 ErikE 提出的解决方案相匹配,并且如前所述,也是我喜欢的解决方案。

    遗憾的是,它没有继续解释哪些是最佳做法(如果有的话),但它肯定是一本很好的读物,可以了解现有的选项。

    【讨论】:

    • 我刚才注意到了你的回答。我对#3 的描述有点困惑,你提到“另一个表用于将它们全部连接起来”。在我制定的方案中,没有“其他表”。有一个超类型表,然后是许多子类型表。我确实提供了一个PartyType 表,但是这个表并没有将它们全部连接起来,它只是为PartyTypeID 列提供了含义(实际上并不是必需的,例如,如果代码强制每个PartyTypeID 的含义在枚举中而不是使用表来记录详细信息)。 (续...)
    • 表格链接在一起的方式是特殊的一对零或一关系,其中PartyIDBusinessIDPersonId 可以互换——无需连接即可计算找出它们的含义,因为它们只是同一事物的不同名称(明确地说,BusinessID of 42 是 PartyID 42)。
    • 嗨 ErikE,你是对的,第三个表不是必需的,我的架构中有一些东西可以从 PartyID 创建项目的实例(因此 InstanceID 1、2 和 3 可能都映射到 PartyID 42)。我会澄清我的答案以消除这种混乱。
    • 我已经更新了我的答案,以更详细地介绍一些替代方案(链接),我认为您在此处列出了这些替代方案,但没有详细解释它们的含义和方式实施它们。
    • 干得好,我更喜欢给出解决方案的基本指导/指针的答案,帮助人们找到比我自己更权威的关于该主题的信息。我相信人们也会很高兴将它添加到您的答案中。
    【解决方案5】:

    您可以改为使用触发器强制执行逻辑。那么无论怎么修改记录,都只会填写其中一个字段。

    您也可以有一个 PeopleAsset 表和一个 BusinessAsset 表,但仍然会遇到强制它们中只有一个有记录的问题。

    【讨论】:

      【解决方案6】:

      资产将具有所有者的外键,您应该设置关联表来链接资产和企业。正如在其他 cmets 中所说,您可以使用触发器和/或约束来确保数据保持一致状态。 IE。删除企业时,请删除关联表中的行。

      【讨论】:

        【解决方案7】:

        Table People、Businesses 都可以使用 UUID 作为主键,并将两者联合到一个视图中以实现 sql 连接目的。

        因此,您可以简单地使用与人员和企业相关的资产中的一个外键列,因为 UUID 几乎是唯一的。你可以简单地查询如下:

        select * from Assets
        join view_People_Businesses as v on v.id = Assets.fk
        

        【讨论】:

          猜你喜欢
          • 2016-07-22
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2021-07-03
          • 1970-01-01
          • 2014-10-24
          相关资源
          最近更新 更多