10

只是想找出为以下场景设计表格的最佳方法:

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

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

CommentID
DocumentID
ProjectID
GroupID
ClientID
etc

其中只有一个 id 将有数据,其余的将为 NULL 或者我应该有一个单独的 CommentType 表并且我的评论表是这样的:

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

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

4

10 回答 10

7

选项 2 对于关系数据库来说不是一个好的解决方案。它被称为多态关联(如@Daniel Vassallo 所述),它打破了关系的基本定义。

例如,假设您在两个不同的行上有一个 1234 的 ResourceId。这些代表相同的资源吗?这取决于这两行的 CommentTypeId 是否相同。这违反了关系中类型的概念。有关详细信息,请参阅CJ Date 的SQL 和关系理论

另一个表明这是一个损坏的设计的线索是,您不能为 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
);

现在 Comments 引用 Commentable 资源,并强制执行引用完整性。给定的评论只能引用一种资源类型。不存在异常或资源 ID 冲突的可能性。

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

于 2010-06-16T17:05:20.067 回答
6

阅读数据库规范化。

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

您需要拆分所有表,以便其中保存的数据完全标准化,这将保证您进一步节省大量时间,并且养成习惯是更好的做法。

于 2010-06-16T16:24:57.353 回答
3

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

于 2010-06-16T16:24:35.330 回答
3

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

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

于 2010-06-16T16:25:59.880 回答
2

第一种方法不是很好,因为它非常非规范化。每次添加新实体类型时,都需要更新表。您最好将此作为文档的属性 - 即将评论内联存储在文档表中。

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

要规范化,您需要将注释表转换为每种资源类型的一个表。

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

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

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

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

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

如果有其他实体与任何类型的资源相关联(例如审计详细信息、访问权限等),那么使用资源映射表是可行的方法,因为它允许您添加规范化注释和任何其他与资源相关的实体。

于 2010-06-16T16:32:35.610 回答
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 表中。否则,您可以使用外键链接该表中的注释。

不过,在您的具体情况下,我不太喜欢这种方法,因为“有评论”不足以在我的脑海中将这些项目放在一起。

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

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。我喜欢这种方法,因为每个表都清楚它是什么。在通常比拥有“通用”表/解决方案更好的数据库中。

于 2010-06-16T17:56:06.360 回答
0

当铺应用:

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

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

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

transaction.trx_type char(1) {L=Loan, P=Purchase, I=Inventory, S=Sale}

设想:

客户最初典当商品,支付了几次利息,然后决定将商品出售给典当行,典当行然后将商品放入库存并最终将其出售给另一位客户。

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

transaction.main_amount DECIMAL(7,2)

在贷款交易中持有典当金额,在购买中持有购买价格,在库存和销售中持有销售价格。

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

于 2010-06-16T23:12:55.797 回答
0

在您提供的选项中,我会选择 2 号。

于 2010-06-16T16:24:28.870 回答
0

如果您携带有关所有评论的相同类型的数据,无论评论是什么,我都会投票反对创建多个评论表。也许评论只是“它的内容”和文本,但如果您现在没有其他数据,您很可能会:输入评论的日期、发表评论的人的用户 ID 等。使用多个表格,您必须为每个表重复所有这些列定义。

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

因此,虽然多指针字段很难看,但我倾向于认为这是正确的方法。我知道一些 db 人说一个表中不应该有一个空字段,你应该总是把它分成另一个表以防止这种情况发生,但我看不出遵循这条规则有什么真正的好处。

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

于 2010-06-16T16:54:17.883 回答
0

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

于 2010-06-16T16:34:34.897 回答