动态列与EAV在数据库表中的区别

11

我正在考虑一种方式,如果我有一个需要根据用户输入能够更改数据库模式的应用程序,该如何处理。

例如,如果我有一个“汽车”对象,其中包含汽车属性,如年份、型号、门数等,我该如何以这样的方式将其存储在数据库中,以便用户能够添加新属性?

我了解到EAV表似乎适合这种情况,但问题是当我尝试基于属性集过滤汽车列表时,查询会变得非常复杂。

我能否动态生成表格呢?我发现Sqlite支持ADD COLUMN,但当表格达到许多记录时,速度会变慢吗?而且貌似没有办法删除列。我必须创建一个不包含要删除的列的新表格,并从旧表格中复制数据。这对于大表格来说肯定很慢 :(


SQLite是严格要求吗?或者您会评估其他东西吗? - MatteoSp
1
这个问题没有一个确定的答案,只有很多“取决于情况”的回答。了解你的代码需要做什么,评估选项(到目前为止,下面有一些好的“这是你可以做的”答案),并预计比你想象中更大的编码痛苦。 - Philip Kelley
6个回答

13
I will assume that SQLite(或其他关系型数据库管理系统)是必需的。 EAVs 我曾经使用过EAV和通用数据模型,可以说这种数据模型非常混乱,长期使用很难处理。
假设您设计了一个包含三个表的数据模型:entitiesattributes和_entities_attributes_:
CREATE TABLE entities
(entity_id INTEGER PRIMARY KEY, name TEXT);

CREATE TABLE attributes 
(attribute_id INTEGER PRIMARY KEY, name TEXT, type TEXT);

CREATE TABLE entity_attributes 
(entity_id INTEGER, attribute_id INTEGER, value TEXT, 
PRIMARY KEY(entity_id, attribute_id));

在这个模型中,实体表将保存您的汽车,属性表将保存您可以关联到您的汽车的属性(品牌、型号、颜色等)及其类型(文本、数字、日期等),而 _entity_attributes_ 将保存给定实体的属性值(例如“红色”)。
请注意,使用此模型,您可以存储任意数量的实体,它们可以是汽车、房屋、计算机、狗或其他任何东西(好吧,也许您需要在实体上添加一个新字段,但对于示例来说已足够)。 INSERT非常简单。您只需要插入一个新对象、一堆属性和它们之间的关系。例如,要插入具有 3 个属性的新实体,您需要执行 7 次插入(一个用于实体、三个用于属性,以及三个用于关系)。
当您想执行 UPDATE 时,您需要知道要更新的实体是什么,并通过连接实体和其属性之间的关系来更新所需的属性。
当您想执行DELETE操作时,您还需要知道要删除的实体是什么,删除其属性,删除实体和其属性之间的关系,然后再删除实体。
但是当您想执行SELECT操作时,情况变得很棘手(您需要编写非常复杂的查询),而且性能会大大下降。
想象一下一个数据模型来存储汽车实体及其属性,就像您的示例中一样(假设我们要存储品牌和型号)。一个查询所有记录的SELECT将是:
SELECT brand, model FROM cars;

如果您像示例中一样设计通用数据模型,查询所有存储的汽车的SELECT将非常困难,并涉及三个表的连接。查询性能将非常差。
此外,请考虑属性的定义。所有属性都存储为TEXT,这可能会成为一个问题。如果有人将“red”存储为价格,会怎么样?
索引是另一件您无法从中受益的事情(或者至少不如所需),随着存储的数据增长,它们非常必要。
正如您所说,作为开发人员的主要关注点是查询非常难编写、难以测试和难以维护(客户要支付多少钱才能购买您拥有的所有红色、1980年、Pontiac Firebirds?),并且在数据量增加时性能非常差。
使用EAVs的唯一优点是可以使用相同的模型存储几乎所有内容,但就像拥有一个装满东西的盒子,您想找到一个具体的、小的物品一样。
Also, to use an argument from authority, I will say that Tom Kyte argues strongly against generic data models: http://tkyte.blogspot.com.es/2009/01/this-should-be-fun-to-watch.html https://asktom.oracle.com/pls/asktom/f?p=100:11:0::::P11_QUESTION_ID:10678084117056 Dynamic columns in database tables
On the other hand, as you mentioned, it's possible to generate tables dynamically by adding (and removing) columns when needed. For example, a basic car table can be created with known attributes and then columns can be added dynamically when needed (such as the number of exhausts).
The disadvantage is that existing tables will need to have columns added, and possibly new indexes built.

正如您所说,这个模型在使用SQLite时还有另一个问题,因为没有直接的方法来删除列,您需要按照http://www.sqlite.org/faq.html#q11中所述进行操作。

BEGIN TRANSACTION;
CREATE TEMPORARY TABLE t1_backup(a,b);
INSERT INTO t1_backup SELECT a,b FROM t1;
DROP TABLE t1;
CREATE TABLE t1(a,b);
INSERT INTO t1 SELECT a,b FROM t1_backup;
DROP TABLE t1_backup;
COMMIT;

无论如何,我认为你不需要删除列(或者至少这将是一个非常罕见的情况)。也许有人添加了“门数”作为一列,并存储了具有此属性的汽车。在删除列之前,您需要确保您的任何汽车都具有此属性,以防止数据丢失。但这当然取决于您的具体情况。
另一个缺点是,您需要为要存储的每个实体都有一个表(一个用于存储汽车,另一个用于存储房屋等等...)。
另一个选项(伪通用模型)
第三个选项可以是具有一些列来存储实体的id、名称和类型以及足够数量的通用列来存储实体属性的伪通用模型。
假设您创建了这样一张表:
CREATE TABLE entities
(entity_id INTEGER PRIMARY KEY,
 name TEXT,
 type TEXT,
 attribute1 TEXT,
 attribute1 TEXT,
 ...
 attributeN TEXT
 );

在这个表格中,您可以存储任何实体(如汽车、房屋、狗),因为您有一个类型字段,并且您可以为每个实体存储尽可能多的属性(在此例中为N)。如果您需要知道当类型为“红色”时属性37代表什么,您需要添加另一个表,将类型和属性与属性描述相关联。如果您发现其中一个实体需要更多属性,那么只需向实体表添加新列(attributeN+1,...)即可。在这种情况下,属性始终以TEXT形式存储(如EAVs),具有其缺点。但是,您可以使用索引,查询非常简单,该模型足够通用适用于您的情况,并且总的来说,我认为该模型的好处大于缺点。希望能对您有所帮助。

根据评论进行跟进:

使用伪泛型模型后,您的实体表将具有许多列。从文档(https://www.sqlite.org/limits.html)中可以看出,SQLITE_MAX_COLUMN的默认设置为2000。我曾经使用过具有100多个列的SQLite表格,并且性能非常好,因此40个列对于SQLite来说不应该是一个大问题。

正如您所说,大多数记录的大部分列都将为空,您需要为所有列建立索引以提高性能,但是您可以使用部分索引(https://www.sqlite.org/partialindex.html)。这样,即使有大量行,您的索引也会很小,并且每个索引的选择性都很高。

如果您只使用两个表来实现EAV,那么表之间的连接数量将少于我的示例,但查询仍然很难编写和维护,并且您需要执行多个(外部)连接以提取数据,这会降低性能,即使您拥有一个很好的索引,当您存储大量数据时也是如此。例如,想象一下您想要获取汽车的品牌、型号和颜色。您的SELECT语句将如下所示:
SELECT e.name, a1.value brand, a2.value model, a3.value color
FROM entities e
LEFT JOIN entity_attributes a1 ON (e.entity_id = a1.entity_id and a1.attribute_id = 'brand')
LEFT JOIN entity_attributes a2 ON (e.entity_id = a2.entity_id and a2.attribute_id = 'model')
LEFT JOIN entity_attributes a3 ON (e.entity_id = a3.entity_id and a3.attribute_id = 'color');

正如您所见,您需要为每个要查询(或过滤)的属性使用一个(左)外连接。对于伪通用模型,查询将如下所示:

SELECT name, attribute1 brand, attribute7 model, attribute35 color
FROM entities;

同时,考虑一下您的_entity_attributes_表的潜在大小。如果每个实体可能有40个属性,假设其中20个不能为空。如果您有10,000个实体,那么您的_entity_attributes_表将有200,000行,并且您将使用一个巨大的索引来查询它。使用伪通用模型,您将拥有10,000行和每列一个小索引。


感谢详细的解释!如果我选择伪泛型模型,那么数据库不会变得非常庞大吗?我的意思是,我将有大约40个列,其中许多对于大多数记录来说都是空的,并且所有这些列都需要建立索引,以便用户能够按属性过滤汽车。使用EAV模型,我只需要插入存在的属性即可。顺便说一下,我的EAV版本是一个单独的表,具有名称、值和指向汽车表的外键。 - Alex
@Alex 没关系!我已经编辑了我的答案,试图解决你的问题。 - antonio
那么,如果不超过100列,那么多列应该比EAV具有更好的性能?此外,可能会有几个表,每个表都有数十个列。即基本属性(颜色、品牌、重量等...),然后是针对不涵盖所有产品的属性的单独组 - 车辆属性(轮数,...),建筑物(房间数量,...)? - kravemir

5
这完全取决于您的应用程序需要如何推理数据。如果您需要运行需要在未知模式下进行复杂比较或连接的查询,SQL和关系模型很少是一个好的选择。例如,如果您的用户可以设置任意数据实体(例如您的示例中的“汽车”),并且希望查找发动机容量大于2000cc、至少有3个门、制造日期在2010年之后、当前所有者属于“小老太太”表的汽车,我不知道如何用SQL优雅地实现这一点。但是,您可以使用XML、XPath等实现类似的功能。如果您的应用程序具有已知属性的数据实体集,但用户可以扩展这些属性(这是像缺陷跟踪器这样的产品的常见要求),则“添加列”是一个好的解决方案。但是,您可能需要发明一种自定义查询语言来允许用户查询这些列。例如,Atlassian Jira的错误跟踪解决方案具有JQL,这是一种类似于SQL的语言,可用于查询错误。EAV非常适合存储和显示数据的任务。然而,在EAV架构中,即使是稍微复杂的查询也变得非常困难-想象一下如何执行上面编造的示例。

1
假设我有一个ORM,它可以相对容易地生成查询,因此,唯一的问题是性能。如果我的“非规范化”EAV表中有数百万行数据,那么这会成为多大的问题? - Tarlen
这取决于你的ORM的复杂程度(顺便说一下,我不知道有这样的东西,如果你有链接,我肯定会感兴趣)。这些查询的性能在很大程度上取决于这些查询如何有效地使用索引。这几乎肯定意味着“value”列的不同数据类型(字符串“10”与整数10不同),并且任何绕过索引的查询可能会非常慢(例如,“type like 'little_old_lady'”)。 - Neville Kuyt

3

对于您的使用情况,像MongoDB这样的文档导向数据库非常适合。


3
另一种选择是使用非规范化表来处理扩展属性。这是伪通用模型和数据库表中动态列的组合。不是在现有表中添加列,而是将列或列组添加到具有源表外键索引的新表中。当然,您需要一个良好的命名约定(例如`car`,`car_attributes_door`,`car_attributes_littleOldLadies`)。
您的选择问题变成应用左外连接以包括要包含的扩展属性。比非规范化慢,但不像实体属性值(EAV)那么慢。添加新的扩展属性成为添加新表的问题。比EAV难,但比修改表模式更容易/更快。删除属性成为删除整个表的问题。比修改表模式更容易/更快。这些新属性可以强类型化。与修改表架构一样好,比EAV或常规列更快。
我认为这种方法的最大优点是,通过单个`DROP TABLE`命令删除未使用的属性非常容易。您还可以选择稍后将经常使用的属性归一化为较大的组或主表,使用单个`ALTER TABLE`过程而不是每次添加列时都添加列的过程,这有助于缓解缓慢的`LEFT OUTER JOIN`查询。
最大的缺点是您会混淆您的表列表,尽管这通常并不是微不足道的问题。另外,我不确定左外连接性能是否比实体属性值(EAV)表连接更好。它绝对接近于EAV连接性能而不是规范化表连接性能。
如果您需要进行大量从强类型列中受益的值的比较/过滤,但您经常添加/删除这些列以使修改巨大的规范化表变得棘手,则这似乎是一个很好的折衷方案。

1

我建议使用EAV。

根据用户输入添加列听起来不太好,而且很快就会用尽容量。对非常扁平的表进行查询也可能会有问题。您想创建数百个索引吗?

与其将所有内容都写入一个表中,我会在主表中存储尽可能多的公共属性(价格、名称、颜色等),并将那些不太常见的属性存储在“额外”属性表中。稍加努力即可随时平衡它们。

EAV可以在小到中等大小的数据集上表现良好。既然您想使用SQLlite,我猜这不会是个问题。

您可能还希望避免过度规范化数据。由于我们当前拥有廉价的存储空间,您可以使用一个表来存储所有“额外”属性,而不是两个:

ent_id,ent_name,... ent_id,attr_name,attr_type,attr_value ...

反对EAV的人会说它在大型数据库上的性能很差。当然,它的性能肯定不如规范化结构,但您也不想在3TB的表上更改结构。


1

我有一个质量较低的答案,但可能是来自类似于以下HTML标记的标记:<tag width="10px" height="10px" ... />

以这种肮脏的方式,您将只有一个列作为varchar(max)用于所有属性,称之为Props列,并且您将像这样存储数据:

Props
------------------------------------------------------------
Model:Model of car1|Year:2010|# of doors:4
Model:Model of car2|NewProp1:NewValue1|NewProp2:NewValue2

通过使用一些函数,如concatCustom获取一个数组并返回一个字符串,以及unconcatCustom获取一个字符串并返回一个数组,所有工作都将转到业务层的编程代码。

为了更有效地处理特殊字符,如':''|',我建议使用'@:@''@|@'或者更稀有的分隔符。


以类似的方式,您可以使用“文本”或“二进制”字段,并将“XML”数据存储在列中。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接