使用单个列中多个表的ID

时间:2011-08-03 15:39:33

标签: database-design relational-database normalization

我的一位同事创建了一个类似于以下的模式。这是一个简化的模式,仅包含解决此问题所需的部分。

系统规则如下:

  1. 部门可以有0到多个部门。
  2. 分工必须只属于一个部门。
  3. 可以将文章分配给该部门或该部门的部门。
  4. 架构是:

    Department
    ---------- 
    DepartmentID (PK) int NOT NULL
    DepartmentName varchar(50) NOT NULL
    
    Division
    --------
    DivisionID (PK) int NOT NULL
    DepartmentID (FK) int NOT NULL
    DivisonName varchar(50) NOT NULL
    
    Article
    -------
    ArticleID (PK) int NOT NULL
    UniqueID int NOT NULL
    ArticleName varchar(50) NOT NULL
    

    他使用虚构规则(缺少更好的术语)定义了模式,所有DepartmentID都在1到100之间,所有DivisionID都在101到200之间。他说当查询Article表时,你将根据它所属的范围知道UniqueID是来自Department表还是Division表。

    我认为这是一个糟糕的设计,并提出了以下替代架构:

    Department
    ----------
    DepartmentID (PK) int NOT NULL
    ParentDepartmentID (FK) int NULL /* Self-referencing foreign key.  Divisions have parent departments. */
    DepartmentName varchar(50) NOT NULL
    
    Article
    -------
    ArticleID (PK) int NOT NULL
    DepartmentID (FK) int NOT NULL
    ArticleName varchar(50) NOT NULL
    

    我相信这是一个正确规范化的架构,可以正确地执行关系和数据完整性,同时遵守上述业务规则。

    我的具体问题是:

    我知道使用一列来包含来自两个域的值是糟糕的设计,我可以在Article表中论证外键的好处。但是,有人可以提供我可用于备份我的职位的特定数据库设计文章/论文的参考。如果我可以指出一些具体的东西,它将使它变得更容易。

5 个答案:

答案 0 :(得分:4)

您的同事实施了一项名为 Polymorphic Associations 的设计。也就是说,“外键”是指两个不同父表之一。大多数人添加另一列parent_type或类似的东西,以便您可以告诉给定行引用哪个父表。在你的同事的情况下,他反而细分了id的范围。这是一个脆弱的设计,因为您无法在数据库级别强制执行它。如果您要插入部门编号> 100,您无法知道您的文章是否适用于部门或部门。

虽然您已经开发了一个类似于Single Table Inheritance的设计,您可以在一个表中存储多个相关类型,因此确保主键保持唯一,并且文章可以引用任何任何实例相关类型。

这是另一种选择:

考虑面向对象的设计。如果您想允许两个不同的类有文章,您可以为这两个类创建一个公共超类或公共接口。您可以在SQL中执行相同的操作:

ArticleProducer
---------------
ProducerID (PK) int NOT NULL

Department
----------
DepartmentID (PK) int NOT NULL, (FK)->ArticleProducer
DepartmentName varchar(50) NOT NULL

Division
--------
DivisionID (PK) int NOT NULL, (FK)->ArticleProducer
DepartmentID (FK) int NOT NULL
DivisonName varchar(50) NOT NULL

Article
-------
ArticleID (PK) int NOT NULL, (FK)->ArticleProducer
UniqueID int NOT NULL
ArticleName varchar(50) NOT NULL

所以一篇文章必须由一个ArticleProducer制作。每个部门或部门都是ArticleProducer。

另见Why can you not have a foreign key in a polymorphic association?

有关多态关联的详情,请参阅我的演示文稿Practical Object-Oriented Models in SQL或我的书SQL Antipatterns: Avoiding the Pitfalls of Database Programming


来自Erwin Smout的评论:

你是对的,试图强制执行所有子类型表中的一行行有点棘手。遗憾的是,MySQL不支持任何存储引擎中的CHECK约束。您可以使用查找表实现类似的功能:

CREATE TABLE ArticleProducerTypes (ProducerType TINYINT UNSIGNED PRIMARY KEY);
INSERT INTO ArticleProducerTypes VALUES (1), (2);

CREATE TABLE ArticleProducer (
  ProducerID INT UNSIGNED NOT NULL PRIMARY KEY,
  ProducerType TINYINT UNSIGNED NOT NULL,
  UNIQUE KEY (ProducerID,ProducerType),
  FOREIGN KEY (ProducerType)
    REFERENCES ArticleProducerTypes(ProducerType)
) ENGINE=InnoDB;

CREATE TABLE DepartmentProducerType (ProducerType TINYINT UNSIGNED PRIMARY KEY);
INSERT INTO DepartmentProducerType VALUES (1);

CREATE TABLE Department (
  DepartmentID INT UNSIGNED NOT NULL PRIMARY KEY,
  DepartmentName VARCHAR(50) NOT NULL,
  ProducerType TINYINT UNSIGNED NOT NULL,
  FOREIGN KEY (DepartmentID, ProducerType) 
    REFERENCES ArticleProducer(ProducerID, ProducerType),
  FOREIGN KEY (ProducerType)
    REFERENCES DepartmentProducerType(ProducerType) -- restricted to '1'
) ENGINE=InnODB;

CREATE TABLE DivisionProducerType (ProducerType TINYINT UNSIGNED PRIMARY KEY);
INSERT INTO DivisionProducerType VALUES (2);

CREATE TABLE Division (
  DivisionID INT UNSIGNED NOT NULL PRIMARY KEY,
  ProducerType TINYINT UNSIGNED NOT NULL,
  DepartmentID INT UNSIGNED NOT NULL,
  FOREIGN KEY (DivisionID, ProducerType) 
    REFERENCES ArticleProducer(ProducerID, ProducerType),
  FOREIGN KEY (ProducerType)
    REFERENCES DivisionProducerType(ProducerType), -- restricted to '2'
  FOREIGN KEY (DepartmentID)
    REFERENCES Department(DepartmentID)  
) ENGINE=InnODB;

CREATE TABLE Article (
  ArticleID INT UNSIGNED NOT NULL PRIMARY KEY,
  ArticleName VARCHAR(50) NOT NULL,
  FOREIGN KEY (ArticleID)
    REFERENCES ArticleProducer(ProducerID)
);

现在,ArticleProducer中的每个给定行都可以被Department或Division引用,但不能同时引用它们。

如果我们想要添加一个新的生成器类型,我们在ArticleProducerTypes查找表中添加一行,并为新类型创建一对新表。例如:

INSERT INTO ArticleProducerTypes VALUES (3);

CREATE TABLE PartnerProducerType (ProducerType TINYINT UNSIGNED PRIMARY KEY);
INSERT INTO PartnerProducerType VALUES (3);

CREATE TABLE Partner (
  PartnerID INT UNSIGNED NOT NULL PRIMARY KEY,
  ProducerType TINYINT UNSIGNED NOT NULL,
  FOREIGN KEY (PartnerID, ProducerType) 
    REFERENCES ArticleProducer(ProducerID, ProducerType),
  FOREIGN KEY (ProducerType)
    REFERENCES PartnerProducerType(ProducerType) -- restricted to '3'
) ENGINE=InnODB;

但我们仍有可能 都不包含对ArticleProducer中给定行的引用;即,我们不能制定强制在其中一个从属表中创建行的约束。我没有解决方案。

答案 1 :(得分:2)

<强> 1NF

  

每个行 - 列交集都只包含一个值   适用的域名(没有别的)。

http://en.wikipedia.org/wiki/First_normal_form#1NF_tables_as_representations_of_relations

解决问题的最简单方法是为每个部门引入“默认”部门,这意味着“整个部门”。之后,只需将所有文章链接到各个部门。 也许这样的事情(DepartmentDivisionNo = 0意味着整个部门):

enter image description here

答案 2 :(得分:1)

我实际上喜欢Damir的回答 - 它重新思考了这个问题,并为这个新问题提供了正确的答案。但是,部门和部门之间存在差异 - 可能每个部门都可以访问属于其部门的文章。拥有属于默认或整个部门部门的文章意味着有两种不同类型的部门。从现在开始,您将进行诸如

之类的查询
select * from xxx x inner join division d where d.joinkey = x.joinkey and d.division != 0.

相反,我称我的解决方案是“不要吝啬关系”:

Department
---------- 
DepartmentID (PK) int NOT NULL
DepartmentName varchar(50) NOT NULL

Division
--------
DivisionID (PK) int NOT NULL
DepartmentID (FK) int NOT NULL
DivisonName varchar(50) NOT NULL

Article
-------
ArticleID (PK) int NOT NULL
ArticleName varchar(50) NOT NULL

ArticleBelongsToDepartment
--------------------------
ArticleID (PK) (FK) int NOT NULL
DepartmentID (FK) int NOT NULL

ArticleBelongsToDivision
--------------------------
ArticleID (PK) (FK) int NOT NULL
DivisionID (FK) int NOT NULL

现在,如何强制执行一些已经提出的约束?为了解决这个问题,您可以创建一个“文章箱”,其中每篇文章都必须属于一个箱子,而且部门和部门都有箱子。

但是这已经进入了杂草,你将无法解决所有案例 - 要么一篇文章依赖于一个部门,一个部门,或一个垃圾箱,或者它不是。部门依赖于垃圾箱或垃圾箱依赖于部门。其中一些问题最好通过事务和存储过程来解决,也许可以通过夜间完整性检查来解决。

答案 3 :(得分:0)

部门和部门应存储在同一个表中。像这样:

DepDiv
---------- 
ID (PK) int NOT NULL
Name varchar(50) NOT NULL
Type int -- ex.: 1 for department, 2 for division, etc., incase you need to differentiate later

它们是如此相似的元素 - 你应该对它们进行相同的处理。

之后,没有棘手的逻辑:id号码的范围是必要的。无论如何,这种方法太不可扩展了。

祝你好运。

答案 4 :(得分:0)

re:他使用虚构规则(缺少更好的术语)定义了模式,所有DepartmentID都在1到100之间,所有DivisionID都在101到200之间。 < / p>

如果那是他想要做的,他应该使用另一个领域,比如isDepartment yes / no。然后他会为ID,name和isDepartment的部门和部门设置一个表,ID字段将是Article表中的FK。

这将解决重叠的部门和部门ID,但不能解决部门和部门之间的1对多关系。要强制执行该关系,您需要两个表。

您还可以在与Article有FK关系的department和division表中引入AuthorID字段。这可能是一个自动生成的字段。这是一种规范分区表中复合键的方法。