使用多个用逗号分隔的外键是否错误?如果是,为什么?


31

有两个表:DealDealCategories。一笔交易可以有很多交易类别。

因此,正确的方法应该是制作一个DealCategories具有以下结构的表:

DealCategoryId (PK)
DealId (FK)
DealCategoryId (FK)

但是,我们的外包团队通过Deal以下方式将多个类别存储在表中:

DealId (PK)
DealCategory -- In here they store multiple deal ids separated by commas like this: 18,25,32.

我觉得他们做错了,但是我不知道如何清楚地解释为什么这是不对的。

我应该如何向他们解释这是错误的?或者也许我是错的人,这是可以接受的?


20
你是对的。将逗号分隔的列表存储在数据库列中真的不好吗?。简短的回答:是的,这很糟糕。
ypercubeᵀᴹ

7
立即射击外包团队,然后再造成伤害...(-_-)
拉法2012年

Answers:


49

是的,这是一个糟糕的主意。

而不是去:

SELECT Deal.Name, DealCategory.Name
FROM Deal
  INNER JOIN
     DealCategories ON Deal.DealID = DealCategories.DealID
  INNER JOIN
     DealCategory ON DealCategories.DealCategoryID = DealCategory.DealCategoryID
WHERE Deal.DealID = 1234

您现在必须去:

SELECT Deal.ID, Deal.Name, DealCategories
FROM Deal
WHERE Deal.DealID = 1234

然后,您需要在应用程序代码中进行一些操作,以将该逗号列表拆分为多个数字,然后分别查询数据库:

SELECT DealCategory.Name
FROM DealCategory
WHERE DealCategory.DealCategoryID IN (<<that list from before>>)

这种设计反模式源于对关系建模的完全误解(您不必害怕表。表是您的朋友。使用它们),或者是一种被误导的信念,即用逗号分隔的列表进行拆分会更快在应用程序代码中,它比添加链接表要更是如此(从不)。第三种选择是,他们对SQL没有足够的信心/能力来设置外键,但是如果是这样,他们就应该与关系模型的设计无关。

SQL Antipatterns(Karwin,2010年)在此反模式(他称为“ Jaywalking”)上专门撰写了整整一章,第15-23页。同样,作者在SO上发表了类似问题。他指出的关键点(应用于此示例)是:

  • 查询特定类别中的所有交易相当复杂(解决该问题的最简单方法是正则表达式,但正则表达式本身就是一个问题)。
  • 没有外键关系,您将无法强制执行参照完整性。如果删除DealCategory nr。#26然后,您必须在应用程序代码中进行每笔交易,以查找对类别#26的引用并将其删除。这是应该在数据层处理的事情,必须在您的应用程序中处理它是很不好的事情
  • 同样,汇总查询(COUNTSUM等)从“复杂”到“几乎不可能”。询问您的开发人员,他们将如何获得所有类别的列表以及该类别中交易数量的清单。经过适当的设计,这就是SQL的四行。
  • 更新变得更加困难(例如,您的交易分为五个类别,但是您想要删除两个类别并添加其他三个类别)。这是经过适当设计的三行SQL。
  • 最终,您会遇到VARCHAR列表长度限制。尽管如果您有一个超过4000个字符的逗号分隔列表,那么无论如何,解析该怪物的速度都会变得很慢。
  • 从数据库中拉出一个列表,将其拆分,然后再返回数据库进行另一个查询,从本质上讲比一个查询要慢。

TLDR:这是一个从根本上存在缺陷的设计,无法很好地扩展,它甚至给最简单的查询都带来了额外的复杂性,而且开箱即用会降低应用程序的速度。


1
西蒙,有人问了同样的问题(dba.stackexchange.com/questions/17824/…),但我不清楚为什么同一张桌子上的FK和PK会破坏 3FN。
jcho360

2
我不确定他们是否想在“交易”和“类别”之间建立多对多关系,或者是某种类别的层次结构。无论哪种方式,这都是主要的副业,用逗号分隔的字段而不是链接表是一个坏主意。
西蒙·里加特斯

4

但是,我们的外包团队通过以下方式将多个类别存储在Deal表中:

DealId(PK)DealCategory-在这里,它们存储多个交易ID,并用逗号分隔,例如:18、25、32。

如果您需要查询给定交易的类别,那实际上是一个好的设计。

但是,如果您想知道给定类别中的所有交易,那就太糟糕了。

而且,这样做还很困难且容易出错,例如更新,计数,联接等。

非规范化有它的位置,但是您必须记住,它针对一种类型的查询进行了优化,但会牺牲您可能针对相同数据进行的其他所有查询。如果您知道您将始终以一种模式进行查询,那么使用非规范化设计可能会给您带来优势。但是,如果有可能您可能需要更大的查询类型灵活性,请坚持使用标准化设计。

像其他任何形式的优化一样,您需要先确定要运行哪些查询,然后才能确定非规范化是否合理。


1
您是否真的认为用逗号分隔子ID的字符串有帮助?我的意思是,应用程序必须先读取,然后解析ID并查询所有子项,例如select * from DealCategories where DealId in (1,2,3,4,...)。您在数据库设计方面比我拥有更多的经验,因此也许在某些情况下您有充分的理由在非常特定的情况下进行这种“极端调整” 。我唯一要证明这一点的想法是selectDeal / DealCategory上的负担非常大。在我看来,这就像一个没有任何数据库设计知识的外包团队,除了创建表之外,还创建了它。
埃里克·哈特

1
@ErikHart,这是非规范化,它可能会有所帮助,但我的观点是,它完全取决于您需要运行的查询。没错,非规范化会使所有查询的性能变差,除非它针对一个查询进行了优化。如果您只需要运行一个查询,而不关心其他查询,那将是双赢。但是这些情况很少见,因为通常我们希望以多种方式灵活地查询数据。
Bill Karwin 2014年

1
@ErikHart,如果为该外包团队提供了仅包含针对此数据的一个查询的项目规范,则他们可以仅针对该特定查询设计一种优化。换句话说,“您要了,就得到了。” 但是,外包提供商没有理由计划数据的未来使用-他们按照规范中所写的内容来实现应用程序。
Bill Karwin 2014年

1

列中的多个值与第一范式相反。

由于将表链接到数据库中,因此绝对也不会提高速度。您必须先阅读和解析一个字符串,然后为“交易”选择所有类别。

正确的实现将是带有DealId和DealCategoryId的联结表,例如“ DealDealCategories”。

层次结构实施不正确?

同样,将DealCategories中的FK转换为另一个DealCategory似乎对DealCategories的层次结构/树的实现不好。通过父ID(所谓的邻接表)关系处理树是一件痛苦的事!

在实现层次结构时,请检查嵌套集(易于阅读,但难于修改)和闭包表(最佳总体性能,但可能会占用大量内存-对于您的DealCategories来说可能不是太多)!

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.