为历史数据正确设计EAV数据库



简介

我一直在读关于EAV数据库的文章,大多数缺点似乎都是与非常非常糟糕的EAV设计或从数据生成报告的困难有关。

通常,当你看到人们抱怨EAV时,他们会使用不到三个表来尝试在RDBMS中复制单独的表+列的功能。有时,这意味着将从小数到字符串的所有内容存储在单个TEXT值列中。EAV还破坏了数据完整性的安全保护,若你们不小心,这可能会非常糟糕。

然而,EAV确实提供了一种跟踪历史数据的简单方法,并允许我们在SQL和键值存储系统之间来回移动系统的各个部分。

如果我们根据不同的实体属性的类型来区分它们呢。这将使我们仍然能够处理belongsTo、Has、HasMany和HasManyThrough关系,以及与特定属性和实体绑定的正确索引值。

考虑以下两个基础实体

products (price -> decimal, title -> string, desc -> text, etc...)
attributes
options
[...]
int
datetime
string
text
decimal
relation
[id,foreign_key]
users (gender -> options, age -> int, username -> string, etc...)
attributes
options
[...]
int
datetime
string
text
decimal
relation
[id,foreign_key]

RDBMS模式设计

众所周知,用户档案和产品是世界上最多样化的项目之一。每家公司对它们的处理方式不同,并根据它们的需求有不同的"列"或"属性"。

以下是如何处理多个(嵌套和/或关系)实体的视图。

其思想是,每个实体都有一个主属性表,然后指定如何查找和解释这些值。这使我们能够处理特殊情况,如其他实体的外键,以及"选项"或十进制数。

实体类型{id,类型,//例如"博客"、"用户"、"产品"等。。创建日期}

entity {
id,
entity_type_id, 
created_at
}
attr {
id,
entity_id,
type,
name,
created_at
}
option {
id,
attr_id,
entity_id,
multiple, // multiple values allowed?
name,
created_at
}
attr_option {
id
attr_id,
entity_id,
option_id
option,
created_at
}
attr_int {
attr_id,
entity_id,
int,
created_at
}
attr_relation {
attr_id,
entity_id,
entity_fk_id,
created_at
}
attr_datetime {
attr_id,
entity_id,
datetime,
created_at
}
attr_string {
attr_id,
entity_id,
var_char,
created_at
}
attr_text {
attr_id,
entity_id,
text,
created_at
}
attr_decimal {
attr_id,
entity_id,
decimal,
created_at
}

这样的表可以让我们永远不必使用UPDATE ...,因为我们可以为每个改变值的新属性只使用INSERT INTO ...,并添加created_at来知道最近的值是什么。这非常适合记录历史数据(当然也可以例外)。

示例查询

首先,它是什么"类型"的实体?(用户、帖子、评论等)

SELECT * FROM entity_type et LEFT JOIN entity e ON e.entity_type_id = et.id WHERE e.id = ?

接下来,这个实体的属性是什么?(表属性)

SELECT * FROM attr WHERE entity_id = ?

接下来,这个实体的属性中存在哪些值?(attr_###表)

SELECT * FROM attr_option, attr_int, attr_relation, attr_text, ... WHERE entity_id = ?
vs
SELECT * FROM attr_option WHERE entity_id = ? if( ! multiple) ORDER BY created_at DESC LIMIT 1
SELECT * FROM attr_int WHERE entity_id = ? ORDER BY created_at DESC LIMIT 1
SELECT * FROM attr_relation WHERE entity_id = ? ORDER BY created_at DESC LIMIT 1
SELECT * FROM attr_text WHERE entity_id = ? ORDER BY created_at DESC LIMIT 1
...

这个实体存在什么关系?

假设我们有一个ID为34的"post"实体,并且我们想要它的"注释"(entity_type=2),这可以允许我们获取产品实体的注释实体ID:

SELECT * FROM entity AS e
LEFT JOIN attr_relation AS ar ON ar.entity_id = e.id
WHERE ar.entity_id = 34 AND e.entity_type = 2;

除了多个查询(键值存储无论如何都需要这些查询),这种方法还会存在什么问题

EAV"数据库"[原文如此]是字面上的数学上的直接地数据库及其元数据的三元组中的未记录描述,没有建立关系表、查询关系、查询元数据、类型检查、维护完整性、优化、原子处理或控制并发的功能。

软件工程原理规定,良好的EAV数据库的使用完全包括定义适当的抽象(类型、运算符、过程、解释器、模块)来重建DBMS的功能。

从EAV三元组及其含义到(碎片化的)数据库描述的映射的机械性质使其易于显示。

套用Greenspun的话,任何足够复杂的EAV项目都包含一个临时的、非正式指定的、充满错误的、缓慢实现的DBMS的一半。

我重复一遍:EAV是数据库及其元数据的三元组中的一个未记录的描述,没有DBMS。仅对数据库中已经证明DDL解决方案不能满足性能要求并且EAV解决方案可以而且值得的部分使用EAV。

以下是此设计的一些问题。

  • 如何查询给定实体?

  • 您将如何对应该是NOT NULL的属性进行建模?也就是说,确保给定的属性对其实体是强制性的,并且如果没有该属性的值,则无法创建实体。

  • 如何为UNIQUE列建模?假设您可以更改属性的值,然后将其更改回原始值。

  • 如何支持引用实体的外键除了整数主键之外的其他东西?

  • 如何将给定的属性限制为查找表中的值集?

解决大多数问题的唯一方法是使用应用程序代码。这就是EAV的问题:您最终会重新发明许多我们认为SQL是理所当然的约束。这是内部平台效应反模式的一个例子:

内部平台效应是软件架构师倾向于创建一个可定制的系统,从而成为他们所使用的软件开发平台的副本,而且往往是一个糟糕的副本。

第六范式不是EAV在第六范式中,每个属性需要一个单独的表,而不是每个数据类型。使用具有适当名称和数据类型的常规列。将此属性存储在一个不同的表中,使您能够存储历史修订。

这意味着您仍然不能在6NF中对NOT NULL进行建模,但至少您可以以非常传统的方式对UNIQUEFOREIGN KEY进行建模。

我一直在读关于EAV数据库的文章,大多数缺点似乎都与糟糕的EAV设计或从数据生成报告的困难有关。

生成报告的困难本质上不可避免地源于EAV DB所代表的事实:;人物XYZ的属性"出生日期"的值为"quot;人员XYZ的属性DECEASEDATE的值为"等等。

这不是最终用户认为用于携带关于个人XYZ(或任何其他)的信息的数据结构的典型形式,因此,在最终用户和数据库之间的某个地方,需要进行额外的转换(非常类似于数据透视,尽管不是100%)。每一个额外的转换都是错误和性能损失的潜在来源。

通常,当你看到人们抱怨EAV时,他们使用不到三个表来尝试复制RDBMS中单独表+列的功能。有时,这意味着将从小数到字符串的所有内容存储在一个TEXT值列中。

这只是EAV的缺点之一。属性级别的类型约束变得更难定义或不可能定义。但除此之外,还有其他因素。

EAV还破坏了数据完整性的安全保护,如果您不小心,这可能会非常糟糕。

这一点与报告生成的难度完全相关,这与表达有意义的查询的难度完全相同,也与表达违反某些给定规则的场景的难度完全一样。

然而,EAV确实提供了一种跟踪历史数据的简单方法,并允许我们在SQL和键值存储系统之间来回移动系统的各个部分。

BS&胡扯。严格应用的EAV将使时间信息与它所应用的事物一样远离任何其他"事件";常规的";属性如果你不这样做,那么你就不再(严格地)应用EAV了。参见Bill Karwin的回答:EAV!=6NF!!!!!!!!!6NF仍然具有所有的";结构";任何其他";常规的";DB也有,EAV就是要有效地从DB中删除该结构(参见philipxy的回答和Bill的"内部平台"备注)。

最新更新