【问题标题】:Dynamic columns in database tables vs EAV数据库表中的动态列与 EAV
【发布时间】:2015-07-19 11:22:33
【问题描述】:

如果我有一个需要能够根据用户输入更改数据库架构的应用程序,我正在尝试决定采用哪种方式。

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

我阅读了有关 EAV 表的信息,它们似乎适合这个问题,但问题是当我尝试获取由一组属性过滤的汽车列表时,查询会变得非常复杂。

我可以改为动态生成表格吗?我看到Sqlite支持ADD COLUMN,但是当表达到很多记录时它有多快?看起来没有办法删除一列。我必须创建一个没有要删除的列的新表,然后从旧表中复制数据。这在大桌子上肯定很慢:(

【问题讨论】:

  • SQLite 要求严格吗?或者你甚至会评估其他东西?
  • 这个问题没有一个答案,只有一大堆“取决于”。了解您的代码需要做什么,评估选项(到目前为止,下面有一些很好的“这是您可以做的”答案),并预测比您想要的更大的编码痛苦。

标签: database sqlite database-design entity-attribute-value


【解决方案1】:

我假设 SQLite(或其他关系 DBMS)是必需的。

EAV

我使用过 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));

在此模型中,entities 表将保存您的汽车,attributes 表将保存您可以关联到汽车的属性(品牌、型号、颜色、 ...) 及其类型(文本、数字、日期...),_entity_attributes_ 将保存给定实体的属性值(例如“red”)。

考虑到使用此模型,您可以存储任意数量的实体,它们可以是汽车、房屋、计算机、狗或其他任何东西(好吧,也许您需要一个关于实体的新字段,但对于示例来说已经足够了)。

INSERTs 非常简单。您只需要插入一个新对象、一堆属性及其关系。例如,要插入具有 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.html https://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
 );

在此表中,您可以存储任何实体(汽车、房屋、狗),因为您有一个 type 字段并且可以存储尽可能多的属性 em> 根据需要为每个 entity (在本例中为 N)。

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

如果您发现您的实体之一需要更多属性怎么办?然后只需将新列添加到 entities 表(attributeN+1,...)。

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

但是你可以使用索引,查询真的很简单,模型对于你的情况来说足够通用,而且总的来说,我认为这种模型的好处大于缺点。

希望对你有帮助。


从 cmets 跟进:

使用伪通用模型,您的 entities 表将有很多列。从文档 (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 欢迎您!我已经编辑了我的答案以尝试解决您的问题
  • 那么,如果一个列不超过100列,那么many-columns应该比EAV有更好的性能?此外,可能只有几张桌子,每张桌子都有几十列。 IE。基本属性(颜色、品牌、重量等...),然后是不涵盖所有产品的属性的各个组 - 车辆属性(车轮数,...),建筑(房间数,...) ?
【解决方案2】:

这完全取决于您的应用程序需要对数据进行推理的方式。

如果您需要运行查询,需要对您事先不知道其架构的数据进行复杂的比较或连接,SQL 和关系模型很少适合。

例如,如果您的用户可以设置任意数据实体(如示例中的“汽车”),然后想要查找发动机容量大于 2000cc、至少有 3 扇门、2010 年之后制造的汽车,其当前所有者是“小老太太”表的一部分,我不知道在 SQL 中执行此操作的优雅方式。

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

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

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

【讨论】:

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

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

【讨论】:

    【解决方案4】:

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

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

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

    最大的缺点是您的餐桌列表很混乱,诚然,这通常不是一个小问题。那我不确定LEFT OUTER JOIN 的实际性能比 EAV 表连接好多少。它肯定比规范化表性能更接近 EAV 连接性能。

    如果您正在对从强类型列中受益匪浅的值进行大量比较/过滤,但您频繁地添加/删除这些列以致难以修改巨大的规范化表,这似乎是一个不错的折衷方案。

    【讨论】:

      【解决方案5】:

      我会尝试 EAV。

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

      我不会将所有内容都写入一个表,而是将尽可能多的通用属性(价格、名称、颜色……)存储在主表中,而将那些不太常见的属性存储在“额外”属性表中。您可以稍后通过一点努力来平衡它们。

      EAV 可以很好地处理中小型数据集。既然你想使用 SQLlite,我想这不是问题。

      您可能还希望避免“过度”规范化数据。用便宜的存储 我们目前有,您可以使用一张表来存储所有“额外”属性,而不是两张:

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

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

      【讨论】:

        【解决方案6】:

        我有一个低质量的答案,但可能来自 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 数据。

        【讨论】:

          猜你喜欢
          • 2011-11-11
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多