我假设 SQLite(或其他关系 DBMS)是必需的。
EAV
我使用过 EAV 和通用数据模型,我可以说数据模型非常混乱,从长远来看很难使用。
假设您设计了一个包含三个表的数据模型:entities、attributes 和 _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;
无论如何,我真的不认为您需要删除列(或者至少这是一种非常罕见的情况)。也许有人将门数添加为一列,并使用此属性存储汽车。您需要确保您的任何汽车都具有此属性,以防止在删除该列之前丢失数据。但这当然取决于您的具体情况。
此解决方案的另一个缺点是,您需要为每个要存储的实体创建一个表(一个用于存储汽车,另一个用于存储房屋,等等......)。
另一种选择(伪通用模型)
第三种选择可能是使用伪泛型模型,其中的表具有用于存储 id、name 和 type 的列实体的数量,以及给定(足够)数量的通用列来存储实体的属性。
假设您创建了一个这样的表:
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 行和每列的一个小索引。