如果我的应用程序需要能够根据用户输入更改数据库架构,我正在尝试决定走哪条路。
例如,如果我有一个包含汽车属性的“汽车”对象,例如年份,型号,门等,那么如何以这种方式将其存储在数据库中,用户应该能够添加新房产?
我读过有关EAV表的内容,他们看起来似乎是对的,但问题是当我尝试获取一组属性过滤的汽车列表时,查询会变得相当复杂。
我可以动态生成表吗?我看到Sqlite支持ADD COLUMN
,但是当表达到很多记录时它的速度有多快?看起来似乎没有办法删除列。我必须创建一个没有要删除的列的新表,并从旧表中复制数据。在大桌子上这肯定很慢:(
答案 0 :(得分:11)
我将假设SQLite(或其他关系DBMS)是一项要求。
<强> EAVS 强>
我使用过EAV和通用数据模型,我可以说数据模型非常混乱,从长远来看很难处理。
让我们假设您设计了一个包含三个表的数据模型:实体,属性和_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_将保存给定实体的属性值(例如&#34; red&#34;)。
考虑到使用此模型,您可以存储任意数量的实体,它们可以是汽车,房屋,计算机,狗或其他任何东西(好吧,也许您需要在实体上使用新字段,但它可以足够的例子)。
INSERT
非常简单。您只需要插入一个新对象,一堆属性及其关系。例如,要插入具有3个属性的新实体,您需要执行7个插入(一个用于实体,三个用于属性,另外三个用于关系。
当您想要执行UPDATE
时,您需要知道要更新的实体是什么,并更新所需的属性以及实体及其属性之间的关系。
如果要执行DELETE
,还需要知道要删除的实体是什么,删除其属性,删除实体与其属性之间的关系,然后删除实体
但是当你想要执行一个SELECT
时,事情变得讨厌(你需要编写非常困难的查询)并且性能下降得非常糟糕。
想象一下,如您的示例所示,存储汽车实体及其属性的数据模型(假设我们要存储品牌和型号)。查询所有记录的SELECT
将是
SELECT brand, model FROM cars;
如果您按照示例设计通用数据模型,则SELECT
查询所有存储的汽车将非常难以编写,并且将涉及3个表连接。查询将表现得非常糟糕。
另外,请考虑属性的定义。您的所有属性都存储为TEXT
,这可能是个问题。如果有人犯了错误并且存储了红色&#34;作为价格?
索引是你无法受益的另一件事(或者至少没有它所希望的那么多),并且随着存储的数据的增长它们是非常必要的。
正如你所说,作为开发人员的主要担忧是查询真的难以编写,难以测试和难以维护(客户需要付多少钱来购买所有红色,1980年,Pontiac Firebirds,你有?),并且当数据量增加时表现很差。
使用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 表,其中包含您将使用的基本属性,然后在需要时动态添加列(例如排气次数)。< / p>
缺点是您需要将列添加到现有表中,并且(可能)构建新索引。
正如您所说,此模型在使用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
);
在此表中,您可以存储任何实体(汽车,房屋,狗),因为您有类型字段,并且您可以存储任意数量的属性对于每个实体,如你所愿(在这种情况下为N)。
如果您需要知道 type37 代表什么 type 是&#34; red&#34;,您需要添加另一个与这些类型相关的表和具有属性描述的属性。
如果您发现某个实体需要更多属性,该怎么办?然后只需将新列添加到实体表(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个非空。如果你有10,000个实体,你的_entity_attributes_表将有200,000行,你将使用一个巨大的索引来查询它。使用伪通用模型,每列将有10,000行和一个小索引。
答案 1 :(得分:5)
这完全取决于您的应用程序需要对数据进行推理的方式。
如果您需要运行需要进行复杂比较的查询或者事先不知道其架构的数据的连接,那么SQL和关系模型很少适合。
例如,如果您的用户可以设置任意数据实体(例如您的示例中的“汽车”),然后想要找到发动机容量大于2000cc的车辆,至少有3个车门,在2010年之后制造,当前所有者是“小老太太”表的一部分,我不知道在SQL中这样做的优雅方式。
但是,您可以使用XML,XPath等实现类似的功能。
如果您的应用程序在具有已知属性的数据实体上设置了一组,但用户可以扩展这些属性(对错误跟踪器等产品的常见要求),则“添加列”是一个很好的解决方案。但是,您可能需要发明一种自定义查询语言,以允许用户查询这些列。例如,Atlassian Jira的错误跟踪解决方案有JQL,一种用于查询错误的类似SQL的语言。
如果你的任务是存储然后显示数据,那么EAV就很棒。然而,即使是中等复杂的查询在EAV模式中变得非常困难 - 想象一下如何执行上面的组成示例。
答案 2 :(得分:3)
对于您的用例,像MongoDB这样的面向文档的数据库会很棒。
答案 3 :(得分:2)
我上面没有提到的另一个选择是对扩展属性使用非规范化表。这是伪通用模型和数据库表中动态列的组合。您可以将列或列组添加到具有源目录FK索引的新表中,而不是向现有表中添加列。当然,您需要一个良好的命名约定(car
,car_attributes_door
,car_attributes_littleOldLadies
)
LEFT OUTER JOIN
来包含要包含的扩展属性的问题。
这种方法的最大优点是,与通过单个DROP TABLE
命令使用的其他属性相比,删除未使用的属性非常容易。您还可以选择稍后使用单个ALTER TABLE
流程将常用的属性归一化为更大的组或进入主表,而不是在添加新列时将它们归一化,这有助于降低{ {1}}个查询。
最大的缺点是您的表列表混乱不堪,这通常并不是一件容易的事。这样,我不确定LEFT OUTER JOIN
的实际性能要比EAV表联接好多少。绝对比标准化表性能更接近EAV连接性能。
如果您要进行很多值的比较/过滤器,这些操作会从强类型化的列中受益匪浅,但是您经常添加/删除这些列以使修改巨大的标准化表变得很困难,这似乎是一个不错的折衷。 / p>
答案 4 :(得分: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
获取一个字符串并返回一个数组。
为了提高':'
和'|'
等特殊字符的有效性,我建议使用'@:@'
和'@|@'
或更为罕见的分割器部分。
以类似的方式,您可以使用text
或binary
字段,并在列中存储XML
个数据。
答案 5 :(得分:1)
我会尝试EAV。
根据用户输入添加列对我来说听起来不是很好,而且您可以快速耗尽容量。非常平坦的桌子上的查询也可能是个问题。你想创建数百个索引吗?
不是将每个东西都写到一个表中,而是在主表中存储尽可能多的公共属性(价格,名称,颜色......)以及&#34; extra&#34中那些不常见的属性;属性表。您可以稍后通过一点努力来平衡它们。
EAV可以很好地适用于中小型数据集。既然你想使用SQLlite,我想这不是问题。
您可能还想避免&#34; over&#34;规范化您的数据。随着廉价的存储 我们目前有,您可以使用一个表来存储所有&#34;额外&#34;属性,而不是两个:
ent_id,ent_name,... ent_id,attr_name,attr_type,attr_value ...
人们反对EAV会说它在大型数据库上表现不佳。它确定它不会影响性能以及标准化结构,但您也不想在3TB表上更改结构。