数据库表中的动态列与EAV



如果我有一个应用程序需要能够根据用户输入更改数据库模式,我正试图决定该走哪条路。

例如,如果我有一个"car"对象,它包含汽车属性,如年份、型号、车门数量等,我如何将其存储在DB中,以便用户能够添加新属性?

我读过关于EAV表的文章,它们似乎适合这件事,但问题是,当我试图获得一组属性过滤的汽车列表时,查询会变得非常复杂。

我可以动态生成表吗?我看到Sqlite支持ADD COLUMN,但当表达到许多记录时,它的速度有多快?而且似乎没有办法删除列。我必须创建一个没有要删除的列的新表,并从旧表中复制数据。这在大桌子上肯定很慢:(

我假设SQLite(或另一个关系DBMS)是一个需求。

EAV

我曾经使用过EAV和通用数据模型,我可以说,从长远来看,数据模型非常混乱,很难使用。

假设您设计了一个具有三个表的数据模型:实体属性和_enties_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));

在该模型中,实体表将保存您的汽车,attributes表格将保存您可以与汽车关联的属性(品牌、型号、颜色…)及其类型(文本、编号、日期…),_entity_attributes_将保存给定实体的属性值(例如"红色")。

考虑到有了这个模型,你可以存储你想要的任意多个实体,它们可以是汽车、房子、电脑、狗或其他什么(好吧,也许你需要一个关于实体的新字段,但对于这个例子来说已经足够了)。

INSERT非常简单。您只需要插入一个新对象、一组属性及其关系。例如,要插入一个具有3个属性的新实体,您需要执行7个插入(一个用于实体,三个用于属性,三个关于关系

当您想要执行UPDATE时,您需要知道要更新的实体是什么,并更新所需的属性,以及实体与其属性之间的关系。

当您想要执行DELETE时,您还需要知道要删除的实体是什么,删除其属性,删除实体与其属性之间的关系,然后删除该实体。

但是,当您想要执行SELECT时,事情会变得很糟糕(您需要编写非常困难的查询),并且性能会严重下降。

想象一个数据模型来存储汽车实体及其属性,如您的示例所示(假设我们要存储品牌和模型)。查询所有记录的SELECT将是

SELECT brand, model FROM cars;

如果您像示例中那样设计一个通用数据模型,那么查询所有存储的汽车的SELECT将非常难以编写,并且将涉及一个3表联接。查询的性能将非常糟糕。

此外,考虑一下你的属性的定义。所有属性都存储为TEXT,这可能是个问题。如果有人犯了一个错误,并将"红色"作为价格存储,该怎么办?

索引是另一件你无法受益的事情(或者至少没有你想要的那么多),而且随着存储数据的增长,索引是非常必要的。

正如你所说,作为一名开发人员,主要担心的是查询真的很难写,很难测试,也很难维护(客户要花多少钱才能买到你拥有的1980年的庞蒂亚克火鸟?),当数据量增加时,查询的表现会很差。

使用EAV的唯一优点是,你可以用同一个模型存储几乎所有的东西,但这就像有一个装满东西的盒子,你想在里面找到一个具体的小东西。

此外,使用权威人士的论点,我要说Tom Kyte强烈反对通用数据模型:http://tkyte.blogspot.com.es/2009/01/this-should-be-fun-to-watch.htmlhttps://asktom.oracle.com/pls/asktom/f?p=100:11:0::::P11_QUESTION_ID:10678084117056

数据库表中的动态列

另一方面,正如您所说,您可以动态生成表,在需要时添加(和删除)列。在这种情况下,例如,您可以创建一个car表,其中包含您知道将要使用的基本属性,然后在需要时动态添加列(例如排气次数)。

缺点是需要向现有表中添加列,并(可能)构建新索引。

正如您所说,这个模型在使用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;

无论如何,我并不认为你真的需要删除列(或者至少这是一种非常罕见的情况)。也许有人将车门数量添加为一列,并使用此属性存储汽车。在删除列之前,您需要确保您的任何汽车都具有此属性,以防止数据丢失。但这当然取决于你的具体情况。

该解决方案的另一个缺点是,您需要为每个要存储的实体提供一个表(一个表用于存储汽车,另一个表存储房屋,等等…)

另一个选项(伪通用模型)

第三种选择可以是使用伪泛型模型,该模型包含一个表,该表具有存储实体的idnametype的列,以及给定(足够)数量的泛型列来存储实体的属性。

假设您创建一个这样的表:

CREATE TABLE entities
(entity_id INTEGER PRIMARY KEY,
name TEXT,
type TEXT,
attribute1 TEXT,
attribute1 TEXT,
...
attributeN TEXT
);

在该表中,您可以存储任何实体(汽车、房屋、狗),因为您有一个类型字段,并且您可以为每个图元存储任意多个attributes

如果您需要知道类型为"红色"时attribute37代表什么,则需要添加另一个将类型和属性与属性描述相关联的表。

如果你发现你的一个实体需要更多的属性,该怎么办?然后简单地将新列添加到实体表中(attributeN+1,…)

在这种情况下,属性总是存储为TEXT(如在EAV中),这有其缺点。

但是您可以使用索引,查询非常简单,该模型对于您的情况足够通用,总的来说,我认为该模型的好处大于缺点。

希望能有所帮助。


评论的后续行动:

使用伪泛型模型,实体表将有很多列。从文档中(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个非空属性。如果您有10000个实体,那么_entity_attributes_表将有200000行,并且您将使用一个巨大的索引对其进行查询。使用伪泛型模型,您将有10000行,每列有一个小索引。

这一切都取决于应用程序对数据进行推理的方式。

如果您需要运行查询,这些查询需要对事先不知道模式的数据进行复杂的比较或联接,那么SQL和关系模型很少适合。

例如,如果你的用户可以设置任意的数据实体(比如你的例子中的"汽车"),然后想找到发动机容量大于2000cc、至少有3个车门、在2010年之后制造的汽车,其当前车主是"小老太太"表中的一员,我不知道在SQL中有什么优雅的方法可以做到这一点。

但是,您可以使用XML、XPath等实现类似的功能。

如果您的应用程序有一组具有已知属性的数据实体,但用户可以扩展这些属性(bug跟踪器等产品的常见要求),那么"添加列"是一个很好的解决方案。但是,您可能需要发明一种自定义查询语言,以允许用户查询这些列。例如,AtlassianJira的错误跟踪解决方案具有JQL,这是一种用于查询错误的类似SQL的语言。

如果您的任务是存储然后显示数据,那么EAV非常好。然而,在EAV模式中,即使是中等复杂的查询也会变得非常困难——想象一下您将如何执行我上面虚构的示例。

对于您的用例,像MongoDB这样的面向文档的数据库会非常好。

我上面没有提到的另一个选项是为扩展属性使用非规范化表。这是伪泛型模型和数据库表中的动态列的组合。不将列添加到现有表中,而是将列或列组添加到具有源表FK索引的新表中。当然,您需要一个好的命名约定(carcar_attributes_doorcar_attributes_littleOldLadies)

  • 您的选择问题变成了应用LEFT OUTER JOIN来包含要包含的扩展属性。
    • 比标准化慢,但不如EAV慢
  • 添加新的扩展属性变成了添加新表的问题。
    • 比EAV更难,比修改表模式更容易/更快
  • 删除属性变成了删除整个表的问题。
    • 比修改表模式更容易/更快
  • 这些新属性可以是强类型的。
    • 与修改表模式一样好,比EAV或泛型列更快

我所看到的这种方法的最大优点是,与通过单个DROP TABLE命令删除任何其他属性相比,删除未使用的属性都非常容易。您还可以选择稍后使用单个ALTER TABLE进程将经常使用的属性规范化为更大的组或主表,而不是在添加时为添加的每个新列指定一个进程,这有助于解决缓慢的LEFT OUTER JOIN查询。

最大的缺点是你把你的表列表弄得一团糟,诚然,这通常不是一个微不足道的问题。我不确定LEFT OUTER JOIN的实际性能比EAV表联接好多少。与标准化表性能相比,它更接近EAV联接性能。

如果您对从强类型列中受益匪浅的值进行了大量比较/筛选,但添加/删除这些列的频率足够高,使得修改一个巨大的规范化表变得困难,那么这似乎是一个很好的折衷方案。

我会尝试EAV。

根据用户输入添加列对我来说不太好,而且很快就会耗尽容量。对非常平坦的表的查询也可能是一个问题。是否要创建数百个索引?

我不会把所有东西都写在一个表中,而是在主表中存储尽可能多的通用属性(价格、名称、颜色…),并在"额外"属性表中存储那些不太常见的属性。你总是可以稍后花点力气来平衡它们。

EAV可以在中小型数据集中表现良好。既然你想使用SQLlite,我想这不是一个问题。

您可能还希望避免对数据进行"过度"规范化。有了廉价的存储空间我们目前有,您可以使用一个表来存储所有"额外"属性,而不是两个:

ent_id,ent_name。。。ent_id、attr_name、attr_type、attr_value。。。

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

我有一个低质量的答案,但可能来自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获取字符串并返回数组。

为了提高':''|'等特殊字符的有效性,我建议使用'@:@''@|@'或更罕见的拆分器部分。


以类似的方式,您可以使用textbinary字段并在列中存储XML数据。

最新更新