【问题标题】:Metadata database design元数据数据库设计
【发布时间】:2014-03-20 22:35:04
【问题描述】:

我正在尝试将有关文档的元数据存储到 SQL Server 中。文档存储在文档存档中,并返回一个标识符,这样我就可以通过要求存档按标识符获取文档来取回该文档。

我们的用户希望能够根据不同的元数据搜索此文档。元数据可以是 1 个属性或 5 个,具体取决于文档类型,并且用户应该能够从管理站点创建新的文档类型。

我可以在这里看到两个解决方案。一种是每个文档类型都有自己的元数据表,其中所有元数据属性都是预定义的,如果应该添加一个,则需要创建一个新列。如果创建了新的文档类型,则需要创建新的元数据表。我们的 DBA 会被这样的解决方案吓坏了,而且我也看到了索引的问题。因为如果文档类型有 5 个不同的元数据属性,则需要在搜索中指定其中的 1 个或 4 个来进行搜索。然后我需要为可能的搜索的所有不同组合编写索引。

这是一个例子(虚构)

    |documentId | Name     | InsertDate | CustomerId | City   
    | 1         | John     | 2014-01-01 | 2          | London
    | 2         | John     | 2014-01-20 | 5          | New York
    | 3         | Able     | 2014-01-01 | 10         | Paris

我可以在这里说:

  • 给我所有 Name = 'John' 的文件
  • 给我所有 Name = 'John' And CustomerId = 5 的文档
  • 给我所有文档,其中 InserDate = '2014-01-01' 和 City = 'London'

这将是 3 个不同的索引,然后我没有涵盖所有可能的组合。这不切实际。

所以我正在研究邪恶的“EAV”(反)模式。

因此,我可以将元数据作为行,而不是将元数据作为列。

|documentId | MetaAttribute | MetaValue
| 1         | Name          | John
| 1         | InsertDate    | 2014-01-01
| 1         | CustomerId    | 2
| 1         | City          | London
| 2         | Name          | John
| 2         | InsertDate    | 2014-01-20
| 2         | CustomerId    | 5
| 2         | City          | New York
| 3         | Name          | Able
| 3         | InserDate     | 2014-01-01
| 3         | CustomerId    | 10
| 3         | City          | Paris 

在这里创建一个索引 om MetaAttribute och metaValue 很简单,并且已经涵盖了。如果创建了新文档类型,则可以使用该文档类型将新元数据创建到 MetaAttributeTable(包含不同文档类型的所有 MetaAttribute)中。因此,如果添加了新文档类型或将新属性添加到文档类型,则无需创建新表或库。相反,所有 MetaValues 大部分都是字符串 :( 并且查找文档 ID 的 SQL Query 有点复杂。

这是我想出来的。 (在本例中,MetaAttribute 是一个字符串,但它是 MetaAttribute 表的 ID)

SELECT * FROM [Document]
  WHERE ID IN (SELECT documentId FROM [MetaData]
                      WHERE  ((MetaAttribute = 'Name' AND MetaValue = 'John')
                         OR (MetaAttribute = 'CustomerId' and MetaValue = '5'))
                      GROUP BY [documentId]
                      HAVING Count(1) = 2)

在这里,我需要询问 Name = 'John' 和 CustomerId = 5。我通过查找 Name = 'John' 和 CustomerId = '5' 的所有记录并将其分组到 documentId 和计数上来做到这一点组中的项目。如果我得到 2,那么 Name = 'John' 和 CustomerId = '5' 对于这个搜索都是正确的。返回 documentId 并使用它来检索有关文档的信息,例如文档存档存储 ID。

应该有一个更好的 SQL 语句,不是吗?

所以我的问题是。有没有比这 2 种更好的方法。EAV 模式是否如此糟糕,以至于我应该坚持第一种方法并拥有一个吓坏了的 DBA 和“一千万个索引”

我们谈论的是一个每月将有大约 10 到 20 百万条新记录的系统,并且包含至少 3 年的数据......因此,这些表将非常大,并且良好的索引对于性能是必不可少的。

最好的问候 马格努斯

【问题讨论】:

  • 您有没有想过使用全文搜索而不是多个索引?

标签: sql-server database-design


【解决方案1】:

如果您有无限的属性,EAV 模型会很有吸引力——也就是说,任何人都可以将任何东西设置为属性。但是,从您的描述中听起来情况并非如此-可能的文档属性来自已知且相当有限的集合。如果是这种情况,常规标准化建议如下:

--  One per document
CREATE TABLE Document
 (
   DocumentId  --  primary key
  ,DocumentType
   ,<etc>
 )

--  One per "type" of document
CREATE TABLE DocumentType
 (
   DocumentTypeId  --  pirmary key
  ,Name
 )

--  One per possible document attribute.
--  Note that multiple document types can reference the same attribute
CREATE TABLE DocumentAttributes
 (
   AttributeId  --  primary key
  ,Name
 )

--  This lists which attributes are used by a given type
CREATE TABLE DocumentTypeAttributes
 (
   DocumentTypeId
  ,AttributeId
  --  compound primary key on both columns
  --  foeign keys on both columns
 )

--  This contains the final association of document and attributes
CREATE TABLE DocumentAttributeValues
 (
   DocumentId
  ,AttributeId
  ,Value
  --  compound primary key on DocumentId, AttributeId
  --  foeign keys on both columns ot their respective parent tables
 )

可以实现具有更健壮键的更紧凑的模型,以确保在数据库级别不能将属性分配给具有“不适当”类型的文档。

查询必须使用连接,但(大概)只有DocumentsDocumentAttributes 表会很大。 (AttributeId + Value) 上的索引有助于按属性类型进行查找,并且根据基数,(Value + AttributeId) 上的索引可以使搜索特定属性非常有效。


(编辑)

哦,聪明,我创建了两个同名的表。我已将最后一个重命名为 DocumentAttributeValues。 (免费建议显然物有所值!)

这显示了这些系统在 SQL 中的丑陋程度,因为您必须分别“查找”这两个属性。从好的方面来说,您不必担心“此类型是否与此文档一起使用”,因为在加载数据时已经(更好地)应用了这些规则。两个例子:

这个在连接中说明了所有内容,因此我认为它的性能可能比下一个更差:

--  Top-down
SELECT do.DocumentId
 from Documents do
  inner join DocumentAttributes da1
   on da.Name = 'Name'
  inner join DocumentAttributeValues dav1
   on dav1.AttributeId = da1.AttributeId
    and dav1.Value = 'John'
  inner join DocumentAttributes da2
   on da2.Name = 'CustomerId'
  inner join DocumentAttributeValues dav2
   on dav2.AttributeId = da2.AttributeId
    and dav2.Value = '5'

这个挑选出属性,然后找出哪些文档拥有所有这些属性。它可能会表现得更好,因为要处理的表少了一个:

--  Bottom-up
SELECT xx.DocumentId
 from (--  All documents with name "John"
       select dav.DocumentId
        from DocumentAttributes da
         inner join DocumentAttributeValues dav
          on dav.AttributeId = da.AttributeId
        where da.Name = 'Name'
         and dav.Value = 'John'
       --  This combines the two sets, with "all" keeping any duplicate entries
       union all
       --  All documents with CustomerId = "5"
       select dav.DocumentId
        from DocumentAttributes da
         inner join DocumentAttributeValues dav
          on dav.AttributeId = da.AttributeId
        where da.Name = 'CustomerId'
         and dav.Value = '5') xx  --  Have to give the subquery an alias
  group by xx.DocumentId
  having count(*) = 2

虽然可能会进一步优化,但您过滤的属性越多,查询就越难看。最多五个属性在 SQL 中可能工作正常,但如果您有大量属性,那么 NoSQL 解决方案可能就是您要寻找的。​​p>

(请注意,与我原来的帖子一样,我没有测试过这段代码,所以这里可能存在拼写错误或微妙的——或者不那么微妙的——错误。)

【讨论】:

  • 你能举一个 SQL 语句的例子来查找 Name = 'John' 和 CustomerId = '5' 的文档吗?
  • 感谢您的 SQL。我尝试了 INNER JOIN 场景,之后我的子查询与 group 和 have 更快,最多 4 个 INNER JOINS 更快,但不够快......
  • 这篇(冗长但有趣的)文章解释了每个人都应该了解的关系数据库中的名称-值对:simple-talk.com/content/article.aspx?article=292
【解决方案2】:

SQL Server 2008+ 提供了三个相关功能来处理此类情况:

  • Sparse Columns 允许您定义数百列,即使一次只使用一个子集
  • Column Sets 允许您对这些列进行分组并将它们视为一个组
  • Filtered indexes 只能索引其中实际包含值的行。

这些功能允许您使用或多或少的普通 SQL 语句来处理所有元数据列。

这些功能是专门为解决 EAV/元数据场景而添加的。

编辑

如果您有一组始终被填充的有限属性,则也不需要稀疏列或 EAV 反模式。

您可以像往常一样创建表并添加索引以优化您遇到的实际工作负载。某些类型的查询会比其他查询更频繁地发生,并且 SQL Server 的索引优化顾问可以根据使用 SQL Server 的 Profiler 捕获的跟踪来建议要使用的索引和统计信息。

很有可能只有一部分列会加速搜索,其余列可以作为include 列添加到索引中。

全文搜索

更强大的选项是使用 SQL Server 的Full Text Search。这将允许您使用任意属性执行查询。这是文档/内容管理系统、ERP 和 CRM 用来处理任意属性的另一种技术。

使用 FTS,您只需指定要包含在一个 FTS 索引中的列,而不必为每个属性创建单独的索引。

您可以像这样在 SELECT 查询中使用 FTS 谓词:

SELECT Name, ListPrice
FROM Production.Product
WHERE ListPrice = 80.99
   AND CONTAINS(Name, 'Mountain')

这可以导致更简单的查询(您只需编写修改后的选择)和管理(无需担心索引中的列顺序,只需管理一个 FTS 索引)

【讨论】:

  • 听起来很有趣,我会去看看。谢谢。
  • 我已经阅读了关于稀疏列的信息,看起来如果我在列中有很多 NULL 值。我不会有,所有的列都会有一个值。所以如果我有一个表:docTypeInvoice_MetaData。然后该表中的所有列都将具有值。我查看了过滤索引,但不知道它们应该如何帮助我。我不想过滤数据,我想创建一个性能良好的索引,无论我发送什么查询。如果我在 WHERE 语句中使用 1 或 4 列:)
  • 那么你也不需要 EAV。您不需要“1000 万个索引”或吓坏了的 DBA。只需根据您的实际工作量选择合适的索引即可。全文搜索可以涵盖即席查询。
  • 我要查找每个表中的特定列,然后监控哪些查询最频繁,并为这些查询创建索引,并为其他不常用的查询使用 FTS..
猜你喜欢
  • 2011-09-30
  • 2011-05-29
  • 2011-01-12
  • 2010-11-29
相关资源
最近更新 更多