7

我有一堆产品,每个产品都有一堆不同的可能属性。例如,产品 A 有名称、尺寸、颜色、形状。产品 B 有名称、卡路里、糖分等。解决此问题的一种方法是:

1) 创建表

Products (id, name)
Attributes (id, name)
Product_Attributes (product_id, attribute_id, value as string)

这允许最大的灵活性,但我听说很多人反对这样做,尽管我不知道为什么。我的意思是,如果这些表被称为 Teams、Players、Team_Players,我们都会同意这是正确的关系设计。

每个向我解释为什么这很糟糕的人都是在完全灵活的关系设计的上下文中这样做的,在这种设计中,您永远不会通过基本的几个基本初始表(例如 object、attribute、object_attribute)创建真正的表——我认为我们所有人都同意是坏的。但这是一个更受限制和包含的版本(只有产品,而不是系统中的每个对象),所以我认为将这两种架构组合在一起是不公平的。

你遇到了什么问题(经验或理论)使这个设计如此糟糕?

2)解决这个问题的另一种方法是创建一个 Product 表,其中包含一堆列,如大小、颜色、形状、重量、糖等,然后在最后包含一些额外的列,以给我们一些灵活性。这将创建通常由 NULL 填充的稀疏行。人们倾向于喜欢这种方法,但我的问题是,在这种方法失去其性能优势之前,您可以拥有多少列?如果你有 200 列,我想这不再是明智之举,但是 100 列呢?50列?25列?

3) 我知道的最后一种方法是将所有属性作为 blob(也许是 JSON)存储在 Products 表的单个列中。我喜欢这种方法,但感觉不对。查询很难。而且,如果您希望以后能够轻松更改属性的名称,则必须单独解析每条记录,或者通过某个 id 将它们键入您的 blob。如果您使用 id 路径,那么您将需要另一个表 Attributes 并且事情开始看起来像上面的方法#1,除了您将无法将 attribute_id 与您的 blob 连接,所以我希望您不想查询任何内容按属性名称。

我喜欢这种方法的地方在于您可以查询一个产品,并且在您的代码中您可以轻松地访问它拥有的所有属性——快速。而且,如果您删除了一个产品,您就不必清理其他表——很容易保持一致。

4) 我已经阅读了一些关于能够在某些 RDBMS 中索引强类型 xml 格式的内容,但老实说,我对这种方法知之甚少。

我被困住了。我觉得方法 #1 是最好的选择,但我读到的所有内容都这么说很臭。思考这个问题的正确方法是什么,以便能够决定在给定情况下什么是最佳方法?显然欢迎比我列出的更多的想法!

4

4 回答 4

10

通过对“实体属性值反模式”进行 Google 搜索,您可能会找到很多关于此主题的信息。

这种方法的一个问题是您最终将元数据与实际数据混合在一起。您的“属性”现在必须告诉数据库“值”列中的确切内容。这使得在前端、报告软件等中处理这些数据变得非常困难。

其次,您将很难在数据库中实际执行任何数据完整性。当您的产品具有“重量”属性时,如何阻止某人将“22 英寸”放入价值中?或者完全是一个非数值。你可能会说,“好吧,我的应用程序会处理这个问题。” 然后,每次要添加新属性时都需要更改应用程序,因为应用程序需要知道如何处理它。如果您要完成所有这些工作,只需添加一个新列。

第三,你如何强制一个给定的产品具有它需要的所有属性?在一行中,您可以使列 NOT NULL,然后他们需要将该行输入数据库。您不能在 EAV 模型中强制执行此操作。

第四,这种模型通常会导致很多混乱。人们不确定支持哪些“属性”,或者他们复制了一个属性,或者他们在创建报表时忘记处理某个属性。例如,如果我有一个“体重(kg)”属性和另一个“体重(lbs)”属性,有人问我,“您的数据库中最重的产品是什么?” 我最好记住我需要检查这两个属性。

第五,这种模式通常也会导致懒惰。嘿,实际上没有理由对我们的系统可以处理的产品进行任何分析,因为无论发生什么,我们都会添加一些属性。以我的经验,公司最好进行创建良好数据库设计所需的分析,而不是依赖于这样的反模式。您将了解有关数据库、应用程序以及可能的业务的内容。

第六,可能需要很多连接才能获得给定产品的单行数据。您可以将属性作为单独的行返回,但现在您必须提供自定义列表框来列出这些产品等。类似地,针对此模型编写搜索查询可能非常困难,在这两种情况下您都可能有性能问题。

这些只是我多年来遇到的一些问题。我确定还有其他人。

对于您的系统而言,正确的解决方案在很大程度上取决于您的业务和应用程序的具体情况。如果您的产品属于共享共同属性的几个类别,您可以考虑使用子类型表,而不是稀疏行。

于 2011-06-24T20:22:51.387 回答
2

灵活的数据模型有很多问题,但第一个可能会咬你的问题是查询很快就会变得笨拙。例如,如果您想获取每个产品的 Size 属性,则查询相对容易。

SELECT p.name product_name, 
       pa.value product_size
  FROM product p    
         left outer join product_attribute pa on (p.product_id = pa.product_id)
         left outer join attribute a on (pa.attribute_id = a.attribute_id and 
                                         a.name          = 'size')

如果你想获得尺寸和其他一些属性,比如颜色,事情会变得更棘手

SELECT p.name product_name, 
       pa_size.value product_size
       pa_color.value product_color
  FROM product p    
         left outer join product_attribute pa_size on (p.product_id = pa_size.product_id)
         left outer join product_attribute pa_color on (p.product_id = pa_size.product_id)
         left outer join attribute a_size on (pa_size.attribute_id = a.attribute_id and 
                                              a_size.name          = 'size')
         left outer join attribute a_color on (pa_color.attribute_id = a.attribute_id and
                                              a_color.name         = 'color')

很快,当您开始想要获取 10 个属性或编写复杂的搜索(向我展示颜色为蓝色且大小为中等的产品)时,对于开发人员编写和维护以及数据库优化器而言,查询开始变得非常复杂生成查询计划。如果您将 30 个表连接在一起,优化器必须非常非常快速地修剪它认为的计划树,以便能够在合理的时间范围内生成查询计划。这往往会导致优化器过早地丢弃有希望的路径,并为您的许多查询生成不太理想的路径。

反过来,这意味着您很快就会遇到新开发的瓶颈,因为开发人员无法正确处理他们的查询,或者开发人员无法让他们的查询足够快地返回。无论您通过不收集需求来确定有效属性是什么而预先节省的时间都会很快用完第 47 次迭代“为什么我不能从这个腐烂的数据模型中获取我想要的数据?”

除了给开发人员带来的成本之外,您最终还会为整个组织创造大量成本。

  • 没有查询工具能够很好地处理这种数据模型。因此,目前所有可以启动他们最喜欢的查询工具并从您的数据库中运行一些报告的用户现在都在等待开发人员编写他们的报告并为他们提取数据。
  • 数据质量变得非常难以执行。检查涉及多个属性的条件变得非常困难(即,如果产品的尺寸为中等,那么重量必须在 1 到 10 磅之间,如果指定了产品的高度,那么还需要宽度)所以人们不会做那些检查。他们不会编写报告来确定哪些地方违反了这些规则。因此,数据最终成为下游流程决定无法使用的一小部分数据,因为它不够完整。
  • 当了解核心实体可能会带来更好的整体设计时,您将太多的初始需求讨论转移到未来。如果您不能就产品的第一个版本需要支持的一组属性达成一致,那么您就不会真正理解该版本应该做什么。即使你成功地编写了一个非常通用的应用程序,这意味着一旦你构建了它就需要大量时间来配置它(因为那时必须有人弄清楚它支持哪些属性)。然后你会发现在配置应用程序时你错过了大量的需求,这些需求只有在定义属性时才变得清晰——如果你不知道是否指定了高度,你就无法知道宽度是必需的。他们'
    在最坏的情况下,配置过程中对这个问题的反应是立即确定您需要提供一种灵活的方式来指定业务规则和指定工作流,以便配置应用程序的人员在添加新属性时可以快速编写他们的业务规则这样他们就可以通过将属性分组在一起或跳过某些页面来控制应用程序的流程(即,如果产品类型是汽车,则有一个需要品牌和型号的页面,如果现在则跳过该页面)。但为了做到这一点,您最终将构建一个完整的开发环境。您将把实际编码应用程序的工作推给配置产品的人员。除非你碰巧非常擅长构建开发环境,
于 2011-06-24T20:45:52.480 回答
2

这种方法如此糟糕的原因是您不知道您必须连接到表的多少次才能获取所有属性。再加上加入同一张表 20 次往往会创建一个巨大比例的性能块。我假设产品将成为您系统的核心,因此是性能的关键所在。

现在你说产品属性会大不相同。我不同意。您的大量产品都会有许多共同的属性,例如价格、单位、尺寸、颜色、尺寸、重量。这些应该在产品表中作为通用属性。这些也是用户在挑选产品时最有可能搜索的内容。

其他属性可用作描述,但对大多数其他属性都没有作用(不会搜索它们或将它们放入订单详细信息中)。将它们放在描述或注释字段中。

最后,您剩下的几个属性可能会有所不同。但是它们有什么不同呢?它们对于特定类型的产品是否通用(书籍具有这些属性,相机具有这些属性),那么该类型产品的相关表可能会很好用。

一旦你完成了你的工作并弄清楚了所有这些,然后如果你仍然需要一张 EAV 表,就可以增加它的灵活性。上述步骤应涵盖 98% 以上的实际需求。

(如果您不知道需要为订单记录的属性字段,那么设计订单详细信息表也有点困难 - 您不能依赖 products 表)

(哦,我也完全同意@Tom H 所说的话。)

于 2011-06-24T20:30:33.693 回答
2

我的意思是,如果这些表被称为 Teams、Players、Team_Players,我们都会同意这是正确的关系设计。

不,我们不会。这就是为什么。

你从这个开始。

Products (id, name)
Attributes (id, name)
Product_Attributes (product_id, attribute_id, value as string)

让我们删除 id 号码,这样我们就可以看到真正发生了什么。(为清楚起见,列名较长。)

Products (product_name)
Attributes (attribute_name)
Product_Attributes (product_name, attribute_name, value as string)

并将其转化为球队和球员。. .

Teams (team_name)
Players (player_name)
Team_Players (team_name, player_name, value as string)

所以对于样本数据,我们可能有

Team                   Player             Value
--
St. Louis Cardinals    Boggs, Mitchell    ?
St. Louis Cardinals    Carpenter, Chris   ?
St. Louis Cardinals    Franklin, Ryan     ?
St. Louis Cardinals    Garcia, Jaime      ?

问号的地方到底是什么?假设我们要记录玩过的游戏数。现在样本数据看起来像这样。

Team                   Player             Value
--
St. Louis Cardinals    Boggs, Mitchell    23
St. Louis Cardinals    Carpenter, Chris   15
St. Louis Cardinals    Franklin, Ryan     19
St. Louis Cardinals    Garcia, Jaime      14

也想存储击球率?你不能。你不仅不能将击球率与打过的比赛一起存储,你也不能通过查看数据库来判断米奇·博格斯是否打了 23 场比赛,有 23 次安打,23 分,有 23 次“击球次数”,有 23 次单打,或三振出局23次。

于 2011-06-24T20:31:53.177 回答