我们处理来自客户的例行数据馈送,该客户只是将其数据库从一种看起来很熟悉的形式(每个实体一行,每个属性一列)重构为一个我不熟悉的形式(每个实体每个属性一行,):

之前:每个属性一列

ID   Ht_cm   wt_kg   Age_yr  ... 
1      190      82     43    ...
2      170      60     22    ...
3      205      90     51    ...


之后:所有属性一列

ID    Metric   Value
 1     Ht_cm     190
 1     Wt_kg     82
 1     Age_yr    43
 1      ...
 2     Ht_cm     170
 2     Wt_kg     60
 2     Age_yr    22
 2     ...
 3     Ht_cm     205
 3     Wt_kg     90
 3     Age_yr    51
 3     ...


此数据库结构有名称吗?相对优势是什么?用旧方法似乎更容易将有效性约束放在特定属性(非空,非负等)上,并且更容易计算平均值。但是我可以看到在不重构数据库的情况下添加新属性可能会更容易。这是结构化数据的标准/首选方法吗?

#1 楼

它被称为Entity-Attribute-Value(有时也称为“名称-值对”),当人们在关系数据库中使用EAV模式时,这是“方孔中的圆钉”的经典案例。

以下列出了不应该使用EAV的原因:


您不能使用数据类型。值是日期,数字还是金钱(十进制)都没有关系。总是要强制使用varchar。从轻微的性能问题到巨大的痛楚(可能必须在每月汇总报告中追逐1美分的费用吗?)之类的东西。
您不能(轻松地)实施约束。它需要大量的代码来执行“每个人的身高必须在0到3米之间”或“年龄必须不为null且> = 0”,而不是每个约束都需要1-2行在正确建模的系统中。
如上所述,您不能轻易保证获得每个客户所需的信息(一个客户可能缺少年龄,而另一个客户则可能缺少身高等)。 。您可以做到,但要比SELECT height, weight, age FROM Client where height is null or weight is null难得多。
再次相关,重复数据很难检测到(如果一个客户有两个年龄,会发生什么情况?对数据进行除EA ,如下所示,如果您将一个属性加倍,则将为您提供两行结果。如果一个客户端针对两个属性具有两个单独的条目,那么您将从以下查询中获得四行)确保属性名称一致。 “ Age_yr”可能会变成“ AGE_IN_YEARS”或“年龄”。 (当然,当您收到摘录时,与人们正在插入数据时相比,这没什么大问题,但是仍然如此。)
任何一种无关紧要的查询都是彻底的灾难。要将三属性的EAV系统关联起来,以便以合理的方式对其进行查询,需要对EAV表进行三个联接。

比较:

SELECT cID.ID AS [ID], cH.Value AS [Height], cW.Value AS [Weight], cA.Value AS [Age]
FROM (SELECT DISTINCT ID FROM Client) cID 
      LEFT OUTER JOIN 
    Client cW ON cID.ID = cW.ID AND cW.Metric = "Wt_kg" 
      LEFT OUTER JOIN 
    Client cH ON cID.ID = cH.ID AND cW.Metric = "Ht_cm" 
      LEFT OUTER JOIN 
    Client cA ON cID.ID = cA.ID AND cW.Metric = "Age_yr"


至:

SELECT c.ID, c.Ht_cm, c.Wt_kg, c.Age_yr
FROM Client c


以下是何时使用EAV的清单(非常简短):


如果绝对无法解决它,而您必须在数据库中支持无模式的数据。
当您只需要存储“东西”而不必期望这样做时它以更结构化的形式呈现。但是,请当心那个被称为“不断变化的需求”的怪兽。 。但是,在大多数情况下(包括上面的示例),麻烦将远远超过其价值。如果您需要广泛支持EAV型数据输入,则应考虑将其存储在键值系统中,例如Hadoop / HBase,CouchDB,MongoDB,Cassandra,BerkeleyDB。

评论


+1,但有一点注意:如果将不同类型的值放在不同的表中(可以,不是传统的EAV,而是某种改进),则可以使用数据类型。 (但是,还有一个问题:您如何知道新属性的类型?)

– dezso
2012年7月12日在6:37

同意,但是我要补充一点,当您保留与系统在语义上不相关的事物列表(不仅仅是无模式的事物)时,EAV也是一种很好的使用方法。例如,需要存储和列出产品功能的在线产品目录。您有要重新设定键/值对的列表,但是系统实际上并不知道或不在乎这些键或值的含义。在那种情况下,EAV的危险是无关紧要的。

–乔尔·布朗(Joel Brown)
2012年7月12日在12:03

@JoelBrown您现在不在乎,但是如果在路上一位副总裁要求知道目录中有多少件衬衫既有棕色纽扣又有纽扣领子,那将是写查询的麻烦。 EAV本身通常表示缺乏计划或远见。

– JNK
2012年7月12日12:36



@JoelBrown我不同意它的用法(非常小,非常狭窄)。但是,如果可能以任何结构化方式查询信息,则可能不应该使用EAV

– JNK
2012年7月12日在16:58

@JoelBrown如果您的业务需求或要存储的数据发生更改,则数据模型也应更改。您的数据模型不应一成不变。此外,对于关系数据库,人们有99%的时间使用EAV时将其推理归结为“我不想花时间思考如何存储我的数据”,而不是“考虑所有我知道的数据库模式和模型, EAV最适合此数据集”。重复一遍-在某些情况下,EAV很有用(甚至可能是“正确的”答案),但它们之间相差无几。

–西蒙·里格斯(Simon Righarts)
2012年7月13日,0:12

#2 楼

实体属性值(EAV)

包括我在内的许多人都认为这是一种反模式。

以下是您的替代选择:


使用数据库表继承
使用XML数据和SQLXML函数
使用nosql数据库,例如HBase


评论


绝对是大多数用例的反模式。如果您的数据集很小并且性能无关紧要,那么它可能对您有用。

– JNK
2012年7月12日在2:58

#3 楼

在PostgreSQL中,处理EAV结构的一种非常好的方法是附加模块hstore,该模块可用于8.4版或更高版本。我引用了手册:


该模块实现了hstore数据类型,用于在单个PostgreSQL值中存储
键/值对的集合。
在各种情况下都很有用,例如具有很多属性的行
很少检查,或者是半结构化数据。键和值只是
文本字符串。


自Postgres 9.2起,还有json类型和很多功能(大多数功能是9.3添加的) 。

Postgres 9.4将(大大优越!)“二进制JSON”数据类型jsonb添加到选项列表中。具有高级索引选项。

#4 楼

很高兴看到EAV db模型是如何被批评甚至被某些人视为“反模式”的。

就我而言,主要的缺点是:



如果您已经进行了一个项目,则学习曲线会更陡峭
前一段时间开始使用EAV。的确,随着您大大增加了联接(和表)的数量,查询变得很困难,因此会
花更多的时间让您理解。只需看一下Magento项目,看看项目外部的开发人员如何度过艰难的时光
在DB上工作,而文档却可以很好地维护。适用于报告,如果您需要使姓名的名称以“ M”等开头的人...

但是,您绝对不应该放弃此解决方案,这就是原因:


Simon谈到了被称为“不断变化的要求”的怪物。我喜欢这个表情:)。恕我直言,这就是为什么EAV可能是一个不错的选择的原因,因为它非常适合“更改”,因为您可以轻松地添加任意数量的属性。当然,这取决于我们正在更改的要求。如果我们正在谈论一个全新的业务,那么您当然必须检查一下dataModel,但是EAV提供了很多灵活性。仅仅因为它要求更加严格,并不意味着它就没有那么有趣了。
也有人说“您不能使用数据类型”。 :这是错误的。您可能有几个值表,每个数据类型一个。然后,您必须在属性表中指定哪种dataType是您的属性。实际上,经典关系/ EAV与类关系的混合在数据库设计中提供了许多有趣的潜力。


评论


对于第一次遇到的EAV设计,学习曲线更陡峭。在那之后,一切看起来都一样。

–超立方体ᵀᴹ
13年1月10日在20:54

临时评论:我不明白为什么“不适合举报”这一说法。 EAV似乎非常适合报告。从eav.values中选择ObjectId,其中propertyId =名称和值,例如'm%'。对虚拟架构的更改(例如,添加属性)可以包含在任何动态报告界面(例如下拉菜单)中,而无需重新编译。

– crokusek
2014年5月21日6:25



#5 楼

如果您有使用EAV结构的数据库,则可以通过多种方式查询数据。

@Simon的答案已经说明了如何使用多个联接执行查询。

使用的示例数据:

CREATE TABLE yourtable ([ID] int, [Metric] varchar(6), [Value] int);

INSERT INTO yourtable ([ID], [Metric], [Value])
VALUES (1, 'Ht_cm', 190),
    (1, 'Wt_kg', 82),
    (1, 'Age_yr', 43),
    (2, 'Ht_cm', 170),
    (2, 'Wt_kg', 60),
    (2, 'Age_yr', 22),
    (3, 'Ht_cm', 205),
    (3, 'Wt_kg', 90),
    (3, 'Age_yr', 51);


如果使用的是具有PIVOT函数的RDBMS(SQL Server 2005 + / Oracle 11g +),则您可以通过以下方式查询数据:

select id, Ht_cm, Wt_kg, Age_yr
from
(
  select id, metric, value
  from yourtable
) src
pivot
(
  max(value)
  for metric in (Ht_cm, Wt_kg, Age_yr)
) piv;


请参阅带有演示的SQL Fiddle

如果您无法访问PIVOT函数,那么您可以将聚合函数与CASE语句一起使用以返回数据:

select id,
  max(case when metric ='Ht_cm' then value else null end) Ht_cm,
  max(case when metric ='Wt_kg' then value else null end) Wt_kg,
  max(case when metric ='Age_yr' then value else null end) Age_yr
from yourtable
group by id


请参阅带有演示的SQL Fiddle

这两个查询将返回结果中的数据:

| ID | HT_CM | WT_KG | AGE_YR |
-------------------------------
|  1 |   190 |    82 |     43 |
|  2 |   170 |    60 |     22 |
|  3 |   205 |    90 |     51 |