通用数据库表设计

时间:2010-06-16 16:21:56

标签: sql sql-server database-design polymorphic-associations

尝试找出为以下场景设计表格的最佳方法:

我的系统中有几个区域(文档,项目,组和客户端),每个区域都可以记录针对它们的注释。

我的问题是我应该有这样一张桌子:

CommentID
DocumentID
ProjectID
GroupID
ClientID
etc

只有一个id会有数据,其余的都是NULL,或者我应该有一个单独的CommentType表,并且我的注释表是这样的:

CommentID
CommentTypeID
ResourceID (this being the id of the project/doc/client)
etc

我的想法是,从索引的角度来看,选项2会更有效。这是对的吗?

10 个答案:

答案 0 :(得分:5)

阅读数据库规范化。

您描述的方式中的空白将表明数据库设计不正确。

你需要拆分你所有的表格,使其中保存的数据完全正常化,这样可以节省大量的时间,保证这一点,并且养成习惯是更好的做法。 / p>

答案 1 :(得分:4)

选项2 是关系数据库的一个很好的解决方案。它被称为多态关联(正如@Daniel Vassallo所提到的)它打破了关系的基本定义。

例如,假设您在两个不同的行上具有1234的ResourceId。这些代表相同的资源吗?这取决于这两行的CommentTypeId是否相同。这违反了关系中 type 的概念。有关详细信息,请参阅C. J. Date的SQL and Relational Theory

另一个线索是,它是一个破碎的设计是你不能为ResourceId声明一个外键约束,因为它可以指向几个表中的任何一个。如果您尝试使用触发器或其他东西强制执行参照完整性,则每次添加新类型的可注释资源时都会发现自己会重写触发器。

我会用@mdma简要提及(但后来忽略)的解决方案来解决这个问题:

CREATE TABLE Commentable (
  ResourceId INT NOT NULL IDENTITY,
  ResourceType INT NOT NULL,
  PRIMARY KEY (ResourceId, ResourceType)
);

CREATE TABLE Documents (
  ResourceId INT NOT NULL,
  ResourceType INT NOT NULL CHECK (ResourceType = 1),
  FOREIGN KEY (ResourceId, ResourceType) REFERENCES Commentable
);

CREATE TABLE Projects (
  ResourceId INT NOT NULL,
  ResourceType INT NOT NULL CHECK (ResourceType = 2),
  FOREIGN KEY (ResourceId, ResourceType) REFERENCES Commentable
);

现在每种资源类型都有自己的表,但串行主键由Commentable唯一分配。给定的主键值只能由一种资源类型使用。

CREATE TABLE Comments (
  CommentId INT IDENTITY PRIMARY KEY,
  ResourceId INT NOT NULL,
  ResourceType INT NOT NULL,
  FOREIGN KEY (ResourceId, ResourceType) REFERENCES Commentable
);

现在注释引用可注释资源,强制执行参照完整性。给定的注释只能引用一种资源类型。不存在异常或资源ID冲突的可能性。

我在演示文稿Practical Object-Oriented Models in SQL和我的书SQL Antipatterns中详细介绍了多态关联。

答案 2 :(得分:3)

从外键的角度来看,第一个示例更好,因为您可以对列具有多个外键约束,但数据必须存在于所有这些引用中。如果业务规则发生变化,它也会更加灵活。

答案 3 :(得分:3)

要从@OMG Ponies' answer继续,您在第二个示例中描述的内容称为多态关联,其中外键ResourceID可以引用多个表中的行。但是在SQL数据库中,外键约束只能引用一个表。数据库无法根据CommentTypeID中的值强制执行外键。

您可能有兴趣查看以下Stack Overflow帖子,找到解决此问题的解决方案:

答案 4 :(得分:2)

第一种方法并不好,因为它非常规范化。每次添加新实体类型时,都需要更新表。你可能最好把它作为文件的属性 - 即。将注释内联存储在文档表中。

对于使用引用完整性的ResourceID方法,您需要在所有Document,Project等实体中都有Resource表和ResourceID外键。 (或使用映射表。)使“ResourceID”成为万事通,可以是documentID,projectID等。这不是一个好的解决方案,因为它不能用于合理的索引或外键约束。 / p>

要规范化,您需要将注释表放入每个资源类型的一个表中。

Comment
-------
CommentID
CommentText
...etc 

DocumentComment
---------------
DocumentID
CommentID

ProjectComment
--------------
ProjectID
CommentID

如果只允许一个注释,则在实体的外键上添加唯一约束(DocumentID,ProjectID等)。这样可以确保给定项只能有一行,因此只有一行注释。您还可以通过在CommentID上使用唯一约束来确保不共享注释。

编辑:有趣的是,这几乎与ResourceID的规范化实现并行 - 在表名中替换“Comment”,使用“Resource”并将“CommentID”更改为“ResourceID”,并且您具有关联ResourceID所需的结构与每个资源。然后,您可以使用单个表“ResourceComment”。

如果有其他实体与任何类型的资源相关联(例如审计细节,访问权限等),那么使用资源映射表是可行的方法,因为它将允许您添加标准化评论和任何其他资源相关实体。

答案 5 :(得分:1)

我不会选择其中任何一种解决方案。根据您的要求的某些细节,您可以使用超类型表:

CREATE TABLE Commentable_Items (
    commentable_item_id    INT    NOT NULL,
    CONSTRAINT PK_Commentable_Items PRIMARY KEY CLUSTERED (commentable_item_id))
GO
CREATE TABLE Projects (
    commentable_item_id    INT    NOT NULL,
    ... (other project columns)
    CONSTRAINT PK_Projects PRIMARY KEY CLUSTERED (commentable_item_id))
GO
CREATE TABLE Documents (
    commentable_item_id    INT    NOT NULL,
    ... (other document columns)
    CONSTRAINT PK_Documents PRIMARY KEY CLUSTERED (commentable_item_id))
GO

如果每个项目只能有一个注释而注释不共享(即注释只能属于一个实体),那么您可以将注释放在Commentable_Items表中。否则,您可以使用外键链接该表的注释。

在你的具体情况下,我不太喜欢这种方法,因为“有评论”并不足以将项目放在一起,就像在我脑海里一样。

我可能会使用单独的Comments表(假设每个项目可以有多个注释 - 否则只需将它们放在基表中)。如果可以在多个实体类型之间共享注释(即,文档和项目可以共享相同的注释),则具有中央注释表和多个实体 - 注释关系表:

CREATE TABLE Comments (
    comment_id    INT            NOT NULL,
    comment_text  NVARCHAR(MAX)  NOT NULL,
    CONSTRAINT PK_Comments PRIMARY KEY CLUSTERED (comment_id))
GO
CREATE TABLE Document_Comments (
    document_id    INT    NOT NULL,
    comment_id     INT    NOT NULL,
    CONSTRAINT PK_Document_Comments PRIMARY KEY CLUSTERED (document_id, comment_id))
GO
CREATE TABLE Project_Comments (
    project_id     INT    NOT NULL,
    comment_id     INT    NOT NULL,
    CONSTRAINT PK_Project_Comments PRIMARY KEY CLUSTERED (project_id, comment_id))
GO

如果要将注释约束到单个文档(例如),则可以在该链接表中的comment_id上添加唯一索引(或更改主键)。

所有这些“小”决定都将影响特定的PK和FK。我喜欢这种方法,因为每个表都清楚它是什么。在通常比具有“通用”表/解决方案更好的数据库中。

答案 6 :(得分:0)

在你给出的选项中,我会选择2号。

答案 7 :(得分:0)

选项2是一个很好的方法。我看到的问题是你将资源密钥放在该表上。可以复制来自不同资源的每个ID。当您将资源加入评论时,您很可能会提出不属于该特定资源的评论。这将被视为多对多的加入。我认为更好的选择是拥有资源表,注释表,然后是交叉引用资源类型和注释表的表。

答案 8 :(得分:0)

如果您携带有关所有评论的相同类型的数据而不管它们的评论是什么,我将投票反对创建多个评论表。也许评论只是“关于它的事情”和文本,但是如果你现在没有其他数据,你很可能会:输入评论的日期,制作评论的用户ID等等。有多个表,你必须为每个表重复所有这些列定义。

如上所述,使用单个引用字段意味着您无法在其上放置外键约束。这太糟糕了,但它不会破坏任何东西,它只是意味着你必须使用触发器或代码进行验证。更严重的是,加入变得困难。你可以说“使用(documentid)评论加入文件”。您需要基于类型字段值的复杂连接。

因此,虽然多指针字段很难看,但我倾向于认为这是正确的方法。我知道有些数据库人说在表中永远不应该有一个空字段,你应该总是把它分解成另一个表以防止这种情况发生,但是我没有看到遵循这个规则的任何真正优势。

就个人而言,我愿意听取有关利弊的进一步讨论。

答案 9 :(得分:0)

典当行申请:

我有单独的贷款,购买,库存和表格的表格。销售交易。 每个表行都通过以下方式连接到各自的客户行:

customer.pk [serial] = loan.fk [integer];
                     = purchase.fk [integer];
                     = inventory.fk [integer];
                     = sale.fk [integer]; 

我已将四个表合并到一个名为“transaction”的表中,其中一列:

transaction.trx_type char(1){L =贷款,P =购买,I =库存,S =销售}

情景:

客户最初瞄准商品,进行一些利息支付,然后决定将商品卖给典当行,典当商然后将商品放入库存并最终将其出售给另一个客户。

我设计了一个通用事务表,例如:

transaction.main_amount DECIMAL(7,2)

贷款交易中的

持有典当金额, 在购买中持有购买价格, 在库存和销售中保持销售价格。

这显然是一种非规范化设计,但使编程更容易,性能也有所改善。现在可以在一个屏幕内执行任何类型的事务,而无需更改为不同的表。