如何构建实体模型以存储具有不同数据类型的任意键/值数据?

时间:2010-06-07 08:59:21

标签: database-design

我不断遇到这样的情况,即使用每行键/值模型而不是刚性列/字段模型将一组任意数据存储在表中是有用的。问题是,我想用正确的数据类型存储值,而不是将所有内容转换为字符串。这意味着我必须选择具有多个可空列的单个表,每个数据类型一个,或一组值表,每个数据类型一个。我也不确定是否应该使用完整的第三范式并将密钥分隔成一个单独的表,通过值表中的外键引用它们,或者是否更好地保持简单和存储值表中的字符串键并接受字符串的重复。

旧/坏:

此解决方案使得在流动环境中添加其他值非常痛苦,因为需要定期修改表格。

MyTable
============================
ID    Key1    Key2    Key3
int   int     string  date
----------------------------
1     Value1  Value2  Value3
2     Value4  Value5  Value6

单表解决方案

此解决方案通过单个表格实现简单性。查询代码仍然需要检查空值以确定该字段存储的数据类型。可能还需要检查约束来确保只有一个值字段包含非空数据。

DataValues
=============================================================
ID    RecordID    Key    IntValue    StringValue    DateValue
int   int         string int         string         date
-------------------------------------------------------------
1     1           Key1   Value1      NULL           NULL
2     1           Key2   NULL        Value2         NULL
3     1           Key3   NULL        NULL           Value3
4     2           Key1   Value4      NULL           NULL
5     2           Key2   NULL        Value5         NULL
6     2           Key3   NULL        NULL           Value6

多表解决方案

此解决方案允许每个表更简洁的用途,尽管代码需要提前知道数据类型,因为它需要为每种数据类型查询不同的表。索引可能更简单,更有效,因为需要索引的列更少。

IntegerValues
===============================
ID    RecordID    Key    Value
int   int         string int
-------------------------------
1     1           Key1   Value1
2     2           Key1   Value4

StringValues
===============================
ID    RecordID    Key    Value
int   int         string string
-------------------------------
1     1           Key2   Value2
2     2           Key2   Value5

DateValues
===============================
ID    RecordID    Key    Value
int   int         string date
-------------------------------
1     1           Key3   Value3
2     2           Key3   Value6

你如何解决这个问题?哪种解决方案更好?

此外,是否应将键列分隔为单独的表并通过外键引用,或者是否应将其保存在值表中并在出于某种原因更改密钥名称时进行批量更新?

6 个答案:

答案 0 :(得分:9)

首先,关系数据库不是为存储任意数据而设计的。关系模型的基本原理围绕着获取将要存储的数据性质的规范。

其次,您所建议的是实体 - 属性 - 值(EAV)的变体。 EAV的问题在于数据完整性,报告,性能和维护。它们有它们的位置,但它们类似于药物:在有限的数量和狭窄的环境中使用它们可以是有益的;太多会杀了你。

针对EAV编写查询是一个熊。因此,如果您要使用EAV,我看到它们成功的唯一情况是限制它们的使用,以便不允许任何人编写过滤特定属性的查询。即,任何人都不允许编写类似于Where AttributeName = 'Foo'的查询。这意味着您永远不能过滤,排序,计算,也不能将特定属性放在报表的特定位置。 EAV数据只是一大堆分类数据,可以在报告中大量涌现,但就是这样。我甚至看到人们将EAV实现为Xml blob。

现在,如果您在这种情况下使用EAV,并且因为它只是一团数据,我会使用单表方法。单表方法的一个显着优点是您可以添加一个检查约束,以确保您在IntValue,StringValue或DateValue列中只有一个且只有一个值。空值不会花费太多,如果这只是大量的数据,它将不会对性能产生任何影响。此外,它将使您的查询更简单,因为您可以使用简单的case语句来返回String,Integer或DateValue。

我可以看到多表方法的许多问题,其中最重要的是没有什么可以防止同一属性具有多种类型的值(例如IntegerValues中的行和StringValues中的行)。此外,要获取数据,您将始终必须使用三个左连接,这将使您的查询更加麻烦。

EAV的成本是纪律和警惕。它要求您的开发团队遵守纪律,在任何情况下都不会针对特定属性编写报告或查询。开发人员将从管理层那里得到很大的压力,“只是这一次”写一些过滤特定属性的东西。一旦你沿着黑暗的道路前进,它就会主导你的开发和维护。 EAV必须保持一堆数据,仅此而已。如果你不能在开发团队中保持这种纪律,那么我就不会实现EAV。为了避免以后的维护噩梦,我需要为任何新专栏制定规范。一旦用户想要对报表上的特殊位置进行过滤,排序,计算或放置属性,该属性必须成为第一类列。如果您能够保持使用规则,EAV可以很好地让用户存储他们想要的任何信息,并在您需要获取数据元素的规范时推迟时间,直到用户想要以前面提到的方式使用该属性。

答案 1 :(得分:3)

我更喜欢将密钥和值保存在一个表中。我正在构建的数据库现在收集有关简单主题/谓词/对象短语中的中文字符的数据点; subject和谓词都是字符串,但是对象可以有任何类型。表中有一些额外的结构信息(例如谓词类型)但不多。

我的db结构的一个特殊功能是谓词实际上被分成几个部分键。为了了解它是如何有用的,让我们考虑一下人物角色的一些数据点:

人 / reading / chinese / rén
人 / reading / japanese / on / jin
人 / reading / japanese / on / nin
人 / reading / japanese / kun / hito
人 / used-in / taiwan
人 / reading / prc
人 / reading / japan
人 / meaning / chinese / english / man; person; human
人 / meaning / japanese / english / man; person; human; quantifier for people
人 / form / strokecount / 2
人 / form / strokeorder / 34

每行代表一个数据点。第一个元素是主题,最后一个是对象,其间是谓词部分。对于谓词部分,有一个固定数量的列(3到5最有可能足够 - 扁平比嵌套更好);未使用的部分接收NULL值。使用这种模式,很容易制定sql语句,它返回有关给定字符的所有事实,或者所有关于许多给定字符的日语读数(包括on和kun),或者至少包含13和最多的所有字符24招,依此类推:

subject  predicate1  predicate2  predicate3 ob_type  ob_int ob_text       ob_bool
人       reading     chinese                text            rén
人       reading     japanese    on         text            jin
人       reading     japanese    on         text            nin
人       reading     japanese    kun        text            hito
人       used-in     taiwan                 bool                          true
人       reading     prc                    bool                          true
人       reading     japan                  bool                          true
人       meaning     chinese     english    text            man; perso...
人       meaning     japanese    english    text            man; perso...
人       form        strokecount            int       2
人       form        strokeorder            int       34

这种方法的优点在于,如果没有太多的思考和前期规划,您很快就可以开始将数据输入表中。当新的事实出现时,它们大多数时间都适合这种非常一般的结构;当你发现一些谓词很尴尬时,收集违规记录并更新它们来携带你喜欢的新词语并不太难。不再有架构迁移。好极了!

更具体地说,回答你的问题,我已经考虑过是否将值放在一个单独的表中以及是否在另一个表中实际表示谓词。

这是完全可能的,但对于我的初始版本,我发现更重要的是保持简单;如果在某些时候,事实证明存储所有那些重复的字符串会伤害存储和性能(我的意思是我的数据库中有大约70000个字符的笔划,因此仅仅是( len( 'form' ) + len( 'strokecount' ) ) * 7e4 == 1e6个字节的顺序才能拼写出来谓词),我相信迁移到更复杂的方法会相对容易。唉,这也意味着你必须修改你的查询。

当我听到人们声称当然你绝对必须将那些重复的谓词和不同的价值类型保存在单独的表格中时,我只是礼貌地微笑。几十年来,数据库已经过优化,可以处理大量数据并有效地组织稀疏表。该死的。我的意思是这整个方法都违背了每个人告诉你如何做到这一点,所以为什么不要大胆。

最后,我相信有三个主要因素可以帮助您决定如何构建数据库:

1)你能提出合理的SQL陈述给你预期的答案吗? (如果不是这样,你仍然需要决定你是否遇到了SQL的一个固有限制,一个可能或可能不能用不同的数据库模式解决的限制)。

2)那些查询表现不错吗?我从经验中知道“把它放在另一个方面”(在几MB的sqlite数据库中)可以在性能上产生真正的巨大差异,所以甚至如果您选择了一个大表的方法并且得到了令人不满意的查询时序,那么可能是错误的架构,但是选择另一种查询相同数据的方式可能会给您10倍的速度增益。

3)可扩展性。可扩展性。可扩展性。这是一个很难的,但也许你肯定知道你要做的就是收集有关你个人大理石系列的数据。在这种情况下,很难做到这一点。如果您承诺在一秒钟内将您所在国家/地区发布的任何书籍的数据提供给世界上的每个桌面,那么很难做任何正确的事情。大多数真实世界的场景介于两者之间,因此可扩展性意味着要问:如果这个或那个工具应该成为性能瓶颈,我能够升级它,还是失败,迁移到另一个产品?我会说一个大表的方法是如此简单,替代方案应该是丰富的。

啊,也许你感兴趣:我正在调查redis,这是NoSQLish数据库中的一个。它看起来很有趣,易于使用。一个大表的方法应该与那些已经变得如此受欢迎的CouchDB / MongoDB / whathaveyou'面向文档,无架构'的数据库完全兼容。

答案 2 :(得分:1)

我肯定会使用多表方法,这将使您的查询更简单,并摆脱许多未使用的字段。如果您希望规范化您的数据库,您还应该将密钥分成另一个表,但这只是一个偏好的问题。

我能想到的唯一更好的方法是使用面向文档的数据库,如MongoDB

答案 3 :(得分:1)

将值存储在原始二进制文件

通过将数据转换为原始byte []格式,将值存储在varbinary字段中。

为什么将所有内容转换为字符串很糟糕:

将所有内容转换为字符串非常昂贵,因为它们涉及实际转换。

例如:

整体,双打,浮点数,布尔,日期等...都需要转换为ASCII字符,这在存储和处理方面都很昂贵(特别是因为它们需要被抛弃)。 / p>

相信我,我之前通过将所有内容转换为双倍(我只使用标量值)来完成此操作并且它有效,但它很糟糕。

如果将所有内容转换为bytes [],则没有“真正的”格式转换,因为您正在复制类型的原始版本。唯一的补充就是你必须存储这个类型,这样你就可以知道在将数据从表中拉回来时将数据转换成什么。

您有两种选择:

  • 添加类型为
  • 的其他列
  • 通过添加包含bitflags的字节来指示类型,将类型连接到varbinary的前面

例如:

0000 0001 - int
0000 0010 - float
0000 0011 - double
0000 0100 - bool
0000 0101 - string
0000 0111 - date
  • 选项1添加了另一列,您说您不想要。
  • 选项2增加了复杂性。如果您可以处理位和字节,那么它是最好/最有效的方法。您使用bitmath来设置/读取类型标志,并使用Array.Copy()和Convert.ToType()方法将它们恢复为正确的类型。

有时,知道如何使用二进制和byte [] s可能是一个巨大的优势。

<强>更新

我不清楚为什么这会被投票。它满足了包含简单的每行键值对关系的问题要求。考虑到不需要将不必要的列添加到表中,并且值存储在其“原生”格式的二进制表示中,它也快速有效。

它增加了一个额外的步骤来插入/提取表中的项目,但普通的字典/数组将在较低的级别上执行相同的操作。唯一的主要区别是,这种存储值的方法带有它的类型。

这种方法唯一真正的缺点是:

  • 无法以有效的方式搜索值(因为需要对其进行检查以进行检查。
  • 它增加了复杂性,因为需要将数据转换为二进制格式并转换为二进制格式,并且需要存储类型。 (与添加类型列并将所有内容都转换为字符串相比,这仍然是一个明显的改进。

答案 4 :(得分:0)

单表设计的另一种替代方法是存储DataType

,其中有一个内部枚举可区分数据类型 即:

1 - string 2 - int 3 - date 4 - etc

表格

============================
ID    Key    Value    DataType
int   string string   int
----------------------------
1     Key  Value       1

我建议像Pragmatic Programmer书中那样将纯文本存储在值中,纯文本将超过所有数据类型,并允许您对其进行任何处理。

正如托马斯所说,EAV总是需要权衡取舍。当您插入数据以确保验证数据类型并将其作为您期望的正确类型插入时,便会遵守纪律。

查询时,仅根据代码中的数据类型进行分析。

即:if response.dataType == enum.date { parseDate(response)} else if response.dataType == enum.int { parseInt(response)} // etc etc

生产中最糟糕的情况,如果它在生产中失败并插入不正确的数据类型,则可以只更改数据库中的数据类型,并且代码应相应地进行解析

我只想声明/重申应该在适度中使用EAV,并且在这条道路上存在一定的权衡取舍,我建议您在继续之前阅读它们。

答案 5 :(得分:0)

您可以将实际的主要实体值存储为JSON blob之类的内容,然后使用IndexFieldName1,IndexFieldValue1来支持过滤和排序,从而获得两全其美的效果