如何为用户定义的字段设计数据库?


145

我的要求是:

  • 需要能够动态添加任何数据类型的用户定义字段
  • 需要能够快速查询UDF
  • 需要能够基于数据类型对UDF进行计算
  • 需要能够根据数据类型对UDF进行排序

其他资讯:

  • 我主要是在寻找性能
  • 有几百万个主记录可以附加UDF数据
  • 当我上次检查时,当前数据库中有超过5000万个UDF记录
  • 在大多数情况下,UDF仅附加到几千个主记录中,而并非全部
  • UDF未加入或用作键。它们只是用于查询或报告的数据

选项:

  1. 创建一个包含StringValue1,StringValue2 ... IntValue1,IntValue2等的大表。

  2. 创建一个动态表,根据需要添加一个新列。我也不喜欢这个主意,因为除非您索引每个列,否则我认为性能会很慢。

  3. 创建一个包含UDFName,UDFDataType和Value的表。添加新的UDF时,生成一个View,该View仅提取该数据并将其解析为指定的任何类型。不符合解析条件的项目将返回NULL。

  4. 创建多个UDF表,每种数据类型一个。因此,我们会有用于UDFStrings,UDFDates等的表。可能会与#2相同,并在添加新字段时自动生成View。

  5. XML数据类型?我以前没有处理过这些,但是看到了它们的提及。不知道他们是否会给我我想要的结果,尤其是在性能方面。

  6. 还有吗


7
Martin Fowler建议使用2(用户可更新的模式)或5(索引的XML LOB):martinfowler.com/bliki/UserDefinedField.html
Neil McGuigan 2013年

另请参阅有关动态数据库架构的StackOverflow问题。
FloverOwe

Answers:


49

如果主要考虑性能,那么我将使用#6 ...每个UDF使用一个表(实际上,这是#2的变体)。该答案专门针对这种情况以及对数据分布和访问模式的描述而定制。

优点:

  1. 因为您指出某些UDF的值只占整个数据集的一小部分,所以单独的表可以为您带来最佳性能,因为该表的大小只足以支持UDF。有关指数也是如此。

  2. 通过限制聚合或其他转换必须处理的数据量,还可以提高速度。将数据拆分为多个表可以使您对UDF数据执行一些汇总和其他统计分析,然后通过外键将该结果与主表结合以获取未汇总的属性。

  3. 您可以使用表/列名来反映实际的数据。

  4. 您可以完全控制使用数据类型,检查约束,默认值等来定义数据域。不要低估由即时数据类型转换导致的性能下降。这样的约束还有助于RDBMS查询优化器制定更有效的计划。

  5. 如果您需要使用外键,则基于触发器的或应用程序级别的约束强制执行很少会内置内置的声明性引用完整性。

缺点:

  1. 这可能会创建很多表。强制执行模式分离和/或命名约定将缓解这种情况。

  2. 需要更多的应用程序代码来操作UDF定义和管理。我希望这仍然比原始选项1、3和4所需的代码少。

其他注意事项:

  1. 如果有任何有关数据性质的信息对将UDF进行分组有意义,则应鼓励这样做。这样,这些数据元素可以合并到一个表中。例如,假设您有用于颜色,尺寸和成本的UDF。数据中的趋势是该数据的大多数实例看起来像

     'red', 'large', 45.03 

    而不是

     NULL, 'medium', NULL

    在这种情况下,通过合并1个表中的3列不会造成明显的速度损失,因为很少有值将为NULL,并且避免创建2个以上的表,当需要访问所有3列时,联接数也减少了2 。

  2. 如果您从人口稠密且经常使用的UDF遇到性能障碍,则应考虑将其包括在主表中。

  3. 逻辑表设计可以带您到特定的位置,但是当记录数量真正增加时,您还应该开始查看您所选择的RDBMS提供的表分区选项。


1
清单!我和菲尔之间开玩笑,我希望这不违反规则。
GunnerL3510 2011年

谢谢,我想我会做一些变化。我们的大多数UDF数据都来自未映射的导入字段,这些字段仅需保留以作参考,因此我想将它们放在一个表中。其他UDF是根据需要定义的(我无法提前识别它们。通常在我们更改某些流程或决定追踪某些特殊内容几个月后会创建它们),它们通常在查询中使用。我想我将为这些值的每个逻辑单元制作一个单独的表。
雷切尔

我正在使用已过时/版本化UDF的表,我使用此方法stackoverflow.com/a/123481/328968来获取最新值。
彼得

22

我已经了很多关于这个问题的文章。最常见的解决方案是Entity-Attribute-Value反模式,与您在选项#3中描述的类似。 避免像瘟疫这样的设计

当我需要真正的动态自定义字段时,此解决方案使用的是将它们存储在XML块中,因此我可以随时添加新字段。但是要使其快速运行,还需要为需要搜索或排序的每个字段创建其他表(您不需要每个字段一个表,而是每个可搜索字段一个表)。有时将其称为倒排索引设计。

您可以在此处阅读有关此解决方案的2009年有趣的文章:http : //backchannel.org/blog/friendfeed-schemaless-mysql

或者,您可以使用面向文档的数据库,在该数据库中每个文档都应具有自定义字段。我会选择Solr


1
您能解释为什么我应该避免选择#3吗?我查看了您的一些示例,但它们确实与我要尝试的示例不同。我只是想要一个存储额外数据的地方,而不是一个存储所有属性的地方。
雷切尔

2
对于初学者,您将为谁设置属性NOT NULL?如何在不使所有属性变为唯一的情况下使属性变为唯一?它从那里继续。您最终将编写应用程序代码,以提供RDBMS已经为您提供的功能,甚至到必须编写某种映射类以简单地插入逻辑实体记录并将其取回的程度。
Bill Karwin 2011年

2
简短的答案是“不要混合数据和元数据”。为varchar列创建fieldnametablename将元数据标识符存储为数据字符串,这就是许多问题的开始。另请参见en.wikipedia.org/wiki/Inner-platform_effect
Bill Karwin 2011年

2
@Thomas:在倒排索引设计中,可以对数据类型和约束(例如UNIQUE和FOREIGN KEY)使用标准架构解决方案。当您使用EAV时,这些功能根本不起作用。我同意反向索引与EAV的共享具有非关系的特性,只是因为它支持每行不同的属性,但这是一个折衷点。
Bill Karwin 2011年

2
@thitami,多年来我了解到的是,任何解决方案都可能是适合您应用程序的解决方案。对于某些特定的应用程序,即使是EAV也可能是最糟糕的解决方案。如果不了解查询,就无法选择优化策略。每种优化都会以牺牲其他查询为代价来改善某些查询。
比尔·卡温

10

我很可能会创建一个具有以下结构的表:

  • varchar名称
  • varchar类型
  • 十进制NumberValue
  • varchar StringValue
  • 日期DateValue

当然,确切的类型取决于您的需求(当然也取决于您使用的dbms)。您还可以将NumberValue(十进制)字段用于int和booleans。您可能还需要其他类型。

您需要一些链接到拥有该值的主记录。为每个主表创建一个用户字段表并添加一个简单的外键可能是最简单,最快的。这样,您可以轻松快速地按用户字段过滤主记录。

您可能需要某种元数据信息。因此,您得到以下结果:

表UdfMetaData

  • 整数ID
  • varchar名称
  • varchar类型

表MasterUdfValues

  • int Master_FK
  • int MetaData_FK
  • 十进制NumberValue
  • varchar StringValue
  • 日期DateValue

无论您做什么,我都不会动态更改表结构。这是一场维护噩梦。我也不会使用XML结构,它们太慢了。


我喜欢您的策略,也许会选择它,但是在2017年,您会选择其他策略吗?像json
maztt

在我们的项目中,我们实现了自己的数据结构,该数据结构序列化为类似于json的内容。它具有typeave接口,可在不强制转换的情况下读取和写入数据,并且具有出色的编程语言集成。真的很棒 它具有与数据库中所有此类“文档”相同的问题。很难查询特定值,并且不能轻易引用“文档”之外的数据。根据使用情况,两者都不是问题。
Stefan Steinegger'3

除此之外,我在2011年提出的建议还是恕我直言。
Stefan Steinegger'3

10

这听起来像是一个问题,可以通过非关系解决方案(例如MongoDB或CouchDB)更好地解决。

它们都允许动态模式扩展,同时允许您保持所寻找的元组完整性。

我同意Bill Karwin的观点,EAV模型对您而言不是一种有效的方法。在关系系统中使用名称/值对本质上不是很坏,但是只有当名称/值对构成完整的信息元组时,它才能很好地工作。使用它迫使您在运行时动态重建表时,各种事情开始变得困难。查询成为枢纽维护中的一项练习,或者迫使您将元组重建推入对象层。

如果没有在对象层中嵌入模式规则,则无法确定空值或缺失值是有效条目还是缺少条目。

您将失去有效管理架构的能力。100个字符的varchar是否是“值”字段的正确类型?200个字符?应该是nvarchar吗?这可能是一个艰难的折衷,最终需要您对场景的动态性质进行人为限制。诸如“您只能具有x个用户定义的字段,并且每个字段只能是y个字符。

使用像MongoDB或CouchDB这样的面向文档的解决方案,您可以在一个元组中维护与用户关联的所有属性。由于联接不是问题,因此生活很幸福,因为尽管进行了大肆宣传,但两者都不适合联接。您的用户可以定义任意数量的属性(或者您将允许),这些属性的长度直到您达到约4MB时才易于管理。

如果您的数据需要ACID级别的完整性,则可以考虑拆分解决方案,其中高完整性数据位于关系数据库中,而动态数据位于非关系存储中。


6

即使您为用户添加自定义列提供了条件,对这些列的查询也不一定能很好地执行。查询设计有许多方面可以使它们表现良好,其中最重要的是首先应该确定应存储的内容。因此,从根本上来说,您是否要允许用户创建架构而无需考虑规范,并能够从该架构中快速获取信息?如果是这样,那么任何这样的解决方案都将很好地扩展,尤其是在您希望允许用户对数据进行数值分析的情况下。

选项1

IMO通过这种方法为您提供模式,而无需了解模式意味着什么,这是灾难的根源,也是报表设计者的噩梦。即,您必须具有元数据才能知道哪个列存储了哪些数据。如果该元数据弄乱了,则可能会破坏您的数据。另外,它可以很容易地将错误的数据放在错误的列中。(“什么?String1包含修道院的名称?我认为这是Chalie Sheen最喜欢的药物。”)

选项3,4,5

IMO的要求2、3和4消除了EAV的任何变化。如果您需要对此数据进行查询,排序或计算,那么EAV是克苏鲁的梦想,也是您的开发团队和DBA的噩梦。EAV会在性能方面造成瓶颈,并且不会为您提供快速获取所需信息所需的数据完整性。查询将很快转向交叉表高地结。

选项2,6

确实留下了一个选择:收集规范,然后构建模式。

如果客户希望在他们希望存储的数据上获得最佳性能,则他们需要与开发人员合作以了解他们的需求,以便尽可能高效地存储数据。仍然可以使用基于表的架构动态构建表单的代码将其存储在与其余表分开的表中。如果您有一个允许在列上使用扩展属性的数据库,则甚至可以使用这些属性来帮助表单构建器使用漂亮的标签,工具提示等,因此只需添加架构即可。无论哪种方式,要有效地构建和运行报告,都需要正确存储数据。如果所讨论的数据将包含许多空值,则某些数据库可以存储该类型的信息。例如,

如果这只是一袋数据,而无需进行任何分析,过滤或排序,那么我想说,EAV的一些变化可能会解决问题。但是,根据您的要求,即使将这些新列存储在单独的表中并根据这些表动态构建表单,最有效的解决方案还是获得正确的规范。

稀疏列


5
  1. 创建多个UDF表,每种数据类型一个。因此,我们会有用于UDFStrings,UDFDates等的表。可能会与#2相同,并在添加新字段时自动生成View。

根据我的研究,基于数据类型的多个表不会对性能有所帮助。特别是如果您有批量数据,例如具有50多个UDF的20K或25K记录。性能是最差的。

您应该使用具有多个列的单个表,例如:

varchar Name
varchar Type
decimal NumberValue
varchar StringValue
date DateValue

这应该是正确的,也应该是正确的。菲尔2011年以前的答案不再是一个好的建议今日2016
邑凯伦莱昂

我可以得到一个简单的示例来说明如何在sql中执行此过程吗?
Niroj

对不起,您的答复很晚,但是您希望数据库结构保持不变。我没有得到你@Niroj。能否请您详细解释一下。
阿米特承包商

4

这是一个有问题的情况,并且没有解决方案显示为“正确”。但是,就简单性和性能而言,选项1可能是最好的。

这也是某些商业企业应用程序中使用的解决方案。

编辑

现在提出的另一个选项是在数据库中使用json字段,但是在最初提出该问题时该选项不存在(或者至少还不成熟)。

许多关系数据库现在支持基于json的字段(可以包括子字段的动态列表)并允许对其进行查询

后退

MySQL的


1
我讨厌创建可能数百个未使用的列的想法。它与我所学到的有关SQL数据库设计的知识背道而驰。目前,我们有1300多个不同的用户定义值,尽管其中许多只是现有项目的重复名称,它们的名称有所不同。
雷切尔

一张桌子有1300种不同的UDF?每个用户都可以选择添加UDF,还是仅添加某种高级用户?
Ophir Yoktan,2011年

它是导入过程的一部分...它将所有未映射的数据添加到用户定义的字段中。由于没有人花时间将未映射的数据映射到现有的UDF字段,因此它只会创建新的字段,并且多年来增加了很多内容。
雷切尔

2

我曾经有过1、3和4的经验,他们最终要么一团糟,要么不清楚数据是什么,还是由于使用某种软分类将数据分解为动态记录类型而变得非常复杂。

我很想尝试XML,您应该能够对xml的内容强制执行模式以检查数据类型等,这将有助于保存UDF数据的不同集合。在较新版本的SQL Server中,您可以在XML字段上建立索引,这将有助于提高性能。(例如,请参见http://blogs.technet.com/b/josebda/archive/2009/03/23/sql-server-2008-xml-indexing.aspx


老实说,我根本没有研究过XML。这样做的主要缺点是我必须学习它的工作原理以及如何对其进行查询,而且我听说性能可能比其他选择还要差
Rachel

1
我会避免使用xml:它可以完成这项工作,并且过去我已经在xml中实现了类似的功能,但是随着数据结构的增长,性能变得很差,并且代码复杂度很高。
凯尔

2

如果您使用的是SQL Server,请不要忽略sqlvariant类型。这非常快,应该做好您的工作。其他数据库可能有类似的东西。

由于性能原因,XML数据类型不是很好。如果您在服务器上进行计算,那么您将不得不不断反序列化这些计算。

选项1听起来很糟糕,而且看起来很粗糙,但是从性能角度考虑可能是最好的选择。我之前创建过带有名为Field00-Field99的列的表,因为您无法超越性能。您可能还需要考虑INSERT性能,在这种情况下,这也是一个不错的选择。如果您希望它看起来整洁,可以随时在此表上创建视图!


谢谢,我将再看一下SQL变体。我最担心的是性能,我不确定性能如何处理,尤其是当我们谈论超过5000万行时
Rachel

刚刚发现sql_varients不能与LIKE子句一起使用...这对我来说是一个很大的缺点。当然,如果确实为每个UDF创建一个视图,则可以将其转换为基于SQL_VARIANT_PROPERTY(value,'BaseType')的数据类型...看起来似乎仍然对性能不利
Rachel

您可以使用LIKE,但必须先强制​​转换值。LIKE仅适用于varchars,因此您必须将sql_variant强制转换为varchar。只要您知道UDF是否为varchar(例如,因为类型存储在其他地方),您就可以将所有行过滤为varchar,然后强制转换并运行LIKE查询:例如。选择* FROM MyTable,其中variant_type ='v'Cast(variant_value as varchar(max))LIKE'Blah%'这样,您就不会将int等转换为会降低速度的字符串。
蒂姆·罗杰斯

我需要运行一些测试以查看其性能如何,尤其是数百万行的性能。知道任何有关使用sql_varients性能的在线文章吗?特别是具有强制转换和大量记录的情况?
雷切尔


1

过去,我没有使用任何这些选项(选项6?:))就已经非常成功地解决了这一问题。

我创建了一个模型供用户使用(存储为xml,并通过自定义建模工具公开),并从模型生成的表和视图中创建模型,以将基本表与用户定义的数据表连接在一起。因此,每种类型都有一个包含核心数据的基本表和一个具有用户定义字段的用户表。

以一个文档为例:典型的字段将是名称,类型,日期,作者等。这将出现在核心表中。然后,用户将使用自己的字段定义自己的特殊文档类型,例如contract_end_date,renewal_clause,等等等等。对于该用户定义的文档,将有一个核心文档表,即xcontract表,该表连接在一个公共主键上(因此,xcontracts主键在核心表的主键上也是外来的)。然后,我将生成一个视图来包装这两个表。查询时的性能很快。其他业务规则也可以嵌入到视图中。这对我来说真的很好。


1

我们的数据库为SaaS应用程序(帮助台软件)提供了支持,其中用户拥有超过7k的“自定义字段”。我们使用组合方法:

  1. (EntityID, FieldID, Value)搜索数据表
  2. entities表中的JSON字段,其中包含所有实体值,用于显示数据。(这样一来,您不需要一百万个JOIN即可获得这些值)。

您可以进一步拆分#1,使其具有“每个数据类型的表”,如该答案所示,这样您甚至可以索引您的UDF。

PS捍卫每个人不断抨击“实体-属性-价值”方法的几句话。几十年来,我们一直使用#1而不使用#2,并且效果很好。有时,这是业务决策。您是否有时间重写您的应用程序并重新设计数据库,或者您可以在云服务器上投入几美元,这些天这些天真的很便宜吗?顺便说一句,当我们使用#1方法时,我们的数据库拥有数以百万计的实体,可供数十万用户访问,而16GB的双核db服务器运行得很好


嗨@Alex,我遇到了类似的问题。如果我理解以及你有:1)custom_fields表中存储的值,如1 => last_concert_year2 => band,3 => music然后custom_fields_values使用值001,1,1976年002,1,1977年003,如图2所示,表Iron Maiden003,第3 ,Metal 希望示例对您有意义,并且对不起格式!
thitami

@thitami不完全是。按照您的示例:我有一个bands表,该表先行1,'Iron Maiden'然后custom_fields1,'concert_year' | 2,'music'然后custom_fields_values1,1,'1977'|1,2,'metal'
Alex

0

在评论中,我看到您说UDF字段将转储用户未正确映射的导入数据。

也许另一个选择是跟踪每个用户制作的UDF的数量,并说他们可以使用6个(或其他随机限制)自定义字段顶部,从而迫使他们重用字段。

当您遇到这样的数据库结构问题时,通常最好回到应用程序的基本设计(在您的情况下为导入系统),并对它施加更多限制。

现在,我要做的是选项4(EDIT),并添加了指向用户的链接:

general_data_table
id
...


udfs_linked_table
id
general_data_id
udf_id


udfs_table
id
name
type
owner_id --> Use this to filter for the current user and limit their UDFs
string_link_id --> link table for string fields
int_link_id
type_link_id

现在,请确保创建视图以优化性能并正确建立索引。这种标准化程度使数据库占用空间减小,但您的应用程序更加复杂。


0

我会推荐#4,因为Magento使用了这种类型的系统,这是一个高度认可的电子商务CMS平台。使用单个表使用fieldIdlabel列定义您的自定义字段。然后,为每种数据类型设置单独的表,并且在每个表中都有一个索引,该索引按fieldId和数据类型列进行索引。然后,在查询中使用类似以下内容的内容:

SELECT *
FROM FieldValues_Text
WHERE fieldId IN (
    SELECT fieldId FROM Fields WHERE userId=@userId
)
AND value LIKE '%' + @search + '%'

我认为这将确保用户定义类型的最佳性能。

根据我的经验,我曾在多个Magento网站上工作,这些网站每月为数百万用户提供服务,托管具有自定义产品属性的数千种产品,并且数据库可以轻松处理工作负载,甚至可以进行报告。

对于报告,您可以用于PIVOT将“ 字段”标签值转换为列名,然后将来自每个数据类型表的查询结果转换为这些数据透视表。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.