7

SQL Server 数据库的现状

有一个包含以下列的表条目:

  • 条目 ID (int)
  • 条目名称 (nvarchar)
  • 入口大小 (int)
  • EntryDate(日期时间)

此外,应该可以为条目保存额外的元数据。这些元数据的名称和值应该可以自由选择,并且应该可以在不改变数据库表结构的情况下动态添加它们。每个元数据键可以是以下数据类型之一:

  • 文本
  • 数值
  • 约会时间
  • 布尔值(真/假)

因此,有一个表 DataKey 用以下列表示元数据名称和数据类型:

  • DataKeyID (int)
  • 数据键名 (nvarchar)
  • DataKeyType (smallint) 0:文本;1:数字;2:日期时间;3:位

根据元数据键的数据类型,可以在表中插入 Entry 和 DataKey 值的每个组合的 DataValue。对于每种数据类型,都有一个可为空的值列。此表具有以下列:

  • 数据值 ID (int)
  • EntryID (int) 外键
  • DataKeyID (int) 外键
  • TextValue (nvarchar) 可以为空
  • NumericValue (float) Nullable
  • DateValue(日期时间)可为空
  • 布尔值(位)可为空

数据库结构图:

在此处输入图像描述

目标

目标是检索满足 WHERE 子句中规范的条目列表。像下面的例子:

假设:

  • 元数据键 KeyName1 是文本
  • 元数据键 KeyName2 是 DateTime
  • 元数据键 KeyName3 是数字
  • 元数据键 KeyName4 是布尔值

询问:

... WHERE (KeyName1 = „Test12345“ AND KeyName2 BETWEEN ’01.09.2012 00:00:00’ AND
’01.04.2013 23:59:00’) OR (KeyName3 > 15.3 AND KeyName4 = True)

目标是以一种非常有效的方式进行这些查询,同时使用大量数据,例如

  • 条目数 > 2.000.000
  • 50 到 100 或可能 > 100 之间的数据键数
  • 每个条目至少指定值的子集,或者也可能是每个键的值 (2.000.000 * 100)

问题

第一个问题是在构建查询时出现的。通常查询需要具有可在 WHERE 子句中使用的列的集合。在这种情况下,查询中使用的列也是表 DataKey 中的条目,以便能够动态添加元数据而无需更改数据库表结构。在研究期间,已经找到了在运行时使用 PIVOT 表技术的解决方案。但事实证明,当数据库中有大量数据时,这种解决方案非常慢。

问题

  • 是否有更有效的方法或结构来为此目的保存数据?
  • 如何满足上面列出的要求,以及查询时的性能和时间消耗?

这是一个带有描述的数据库结构和一些示例数据的 sql fiddle:http ://www.sqlfiddle.com/#!3/d1912/3

4

7 回答 7

6

实体属性值设计(这就是您在此处所拥有的)中的基本缺陷之一是难以进行高效和高性能的查询。

存储数据更有效的结构是放弃 EAV 并使用规范化的关系形式。但这必然涉及在数据结构发生变化时更改数据库的结构(这应该是不言而喻的)。

您可以放弃您的 TextValue/NumericValue/DateValue/BoolValue 字段并将它们替换为单个sql_variant列,这将稍微降低您的查询复杂性,但基本问题仍然存在。

附带说明一下,如果您必须处理金钱,将所有数字存储为浮点数会导致问题。

于 2013-09-05T07:52:27.523 回答
1

我觉得没有资格评论什么是最好的或设计方法。事实上,我倾向于不回答。但是,我已经考虑过您的问题,并且您已经花时间清楚地描述了它,这就是我处理它的方式。

我会将每个元数据数据类型存储在自己的表中;所以

Table MetaData_Text:
    ID int identity
    EntryID int
    KeyName nvarchar(50)
    KeyValue nvarchar(max)

MetaData_DateTime、MetaData_Boolean 和 MetaData_Numeric 具有与此相同的结构,但在每种情况下具有相应不同的 KeyValue 列数据类型。

条目与这些表中的每一个之间的关系是 0-Many;虽然每个表中的每一行都属于一个条目。

要为条目添加新的元数据项,我只需使用一个存储过程,该过程采用 EntryID、键名和具有可能的元数据数据类型的可选参数:

 create procedure AddMetaData @entryid int, @keyname varchar(50), @textvalue varchar(max) = null, @datevalue datetime = null, @boolvalue bool = null, @numvalue float = null
 as ...

对于查询,我将定义一组函数来管理每种类型的 (a) 元数据数据类型和 (b) 需要对该数据类型执行的测试,例如:

 create function MetaData_HasDate_EQ(@entryid int, @keyname varchar(50), @val datetime)
 returns bool
 as begin
     declare @rv bool
     select @rv = case when exists(
       select 1 from MetaData_DateTime where EntryID = @entryid and KeyName = @keyname and KeyValue = @val) then 1 else 0 end;
     return @rv
 end

并将函数引用合并到所需的查询逻辑中

 SELECT ...
 FROM entry e ...
 WHERE (dbo.MetaData_HasText_EQ(e.EntryID, 'KeyName1', 'Test12345') <> 0
     AND dbo.MetaData_HasDate_Btwn(e.EntryID, 'KeyName2', '01.09.2012 00:00:00', '01.04.2013 23:59:00') <> 0)
   OR (dbo.MetaData_HasNum_GT(e.EntryID, 'KeyName3', 15.3) <> 0 
     AND dbo.MetaData_HasBool_EQ(e.EntryID, 'KeyName4', 1) <> 0)
于 2013-09-10T23:04:49.613 回答
1

我相信这种数据结构的性能问题可能需要重新设计结构。

但是,我认为这个相当简单的动态 sql 允许您根据需要进行查询,并且在我对 Entry 表中的 100,000 多行和 DataValue 表中的 500,000 行进行的快速测试中似乎运行得相当快。

-- !! CHANGE WHERE CONDITION AS APPROPRIATE
--declare @where nvarchar(max)='where Key0=0'
declare @where nvarchar(max)='where Key1<550'

declare @sql nvarchar(max)='select * from Entry e';

select @sql=@sql
    +' outer apply (select '+DataKeyName+'='
    +case DataKeyType when 0 then 'TextValue' when 1 then 'NumericValue' when 2 then 'DateValue' when 3 then 'BoolValue' end
    +' from DataValue v where v.EntryID=e.EntryID and v.DataKeyID='+cast(DataKeyID as varchar)
    +') '+DataKeyName+' '
from DataKey;

set @sql+=@where;

exec(@sql);
于 2013-09-12T13:48:11.733 回答
0

首先回答为什么人们使用 EAV 或 KVP,即使它在查询方面效率如此低下?博客和教科书有许多合理的理由。但在现实生活中,要避免与不合作的 DBA 打交道。

对于数据量较小的小型组织,拥有多用途数据库(OLTP + DW)是可以的,因为效率低下并不明显。当您的数据库变大时,是时候将您的在线数据复制到数据仓库中了。此外,如果数据用于分析,则应将其从您的关系数据仓库进一步复制到维度模型或平面和宽模型中以供使用。

这是我希望大型组织提供的数据模型:

  1. OLTP
  2. 关系数据仓库
  3. 报告的维度模型
  4. 用于分析的数据集市。

因此,要回答您的问题,您不应该查询您的 KVP 表并在其上创建视图并不能使其变得更好。它应该被展平(即枢轴)到物理表中。您拥有的是 1 和 2 的混合体。如果 #3 没有用户,则只需构建 #4。

于 2013-09-14T19:38:30.160 回答
0

您尚未指定有关表更新频率、添加新属性的频率等的任何背景信息...

查看您的输入,我认为您可以使用使标准化数据变平的快照。这并不理想,因为需要手动添加列,但它可以非常快。快照可以根据您的用户需求定期更新。

于 2013-09-11T22:36:28.300 回答
0

根据 Dan Belandi 的回答,我认为最简单的使用方法是使用存储过程/触发器来查看元数据表并相应地在数据表上构建视图。

代码将如下所示:

 -- drop old view
IF object_id('EntryView') IS NOT NULL DROP VIEW [EntryView]
GO
-- create view based on current meta-information in [DataKey]
DECLARE @crlf char(2)
DECLARE @sql nvarchar(max)

SELECT @crlf = char(13) + char(10)

SELECT @sql = 'CREATE VIEW [EntryView]' + @crlf
            + 'AS' + @crlf
            + 'SELECT *' + @crlf
            + '  FROM [Entry] e' 

SELECT @sql = @sql + @crlf      
            + ' OUTER APPLY (SELECT '+ QuoteName(DataKeyName) + ' = ' + QuoteName((CASE DataKeyType WHEN 0 THEN 'TextValue' 
                                                                                                    WHEN 1 THEN 'NumericValue' 
                                                                                                    WHEN 2 THEN 'DateValue' 
                                                                                                    WHEN 3 THEN 'BoolValue' 
                                                                                                           ELSE '<Unknown>' END)) + @crlf
            + '                FROM [DataValue] v WHERE v.[EntryID] = e.[EntryID] AND v.[DataKeyID] = ' + CAST(DataKeyID as varchar) + ') AS ' + QuoteName(DataKeyName)
 FROM DataKey

--PRINT @sql
EXEC (@sql)

-- 示例用法:

SELECT * 
  FROM EntryView 
 WHERE (Key1 = 0  AND Key2 BETWEEN '01.09.2012 00:00:00' AND '01.04.2013 23:59:00') 
    OR (Key3 > 'Test15.3' AND Key4 LIKE '%1%')
于 2013-09-14T20:41:16.040 回答
0

我会使用 4 个表,每个数据类型一个:

MDat1
DataValueID (int)
EntryID (int) Foreign-Key
DataKeyID (int) Foreign-Key
TextValue (nvarchar) Nullable
MDat2
DataValueID (int)
EntryID (int) Foreign-Key
DataKeyID (int) Foreign-Key
NumericValue (float) Nullable
MDat3
DataValueID (int)
EntryID (int) Foreign-Key
DataKeyID (int) Foreign-Key
DateValue (datetime) Nullable
MDat4
DataValueID (int)
EntryID (int) Foreign-Key
DataKeyID (int) Foreign-Key
BoolValue (bit) Nullable

如果我有可用的分区,我应该在所有 4 个表的 DataKeyID 上使用它。然后我应该使用 4 个视图:

SELECT ... FROM Entry JOIN MDat1 ON ... EnMDat1
SELECT ... FROM Entry JOIN MDat2 ON ... EnMDat2
SELECT ... FROM Entry JOIN MDat3 ON ... EnMDat3
SELECT ... FROM Entry JOIN MDat4 ON ... EnMDat4

所以这个例子:

WHERE (KeyName1 = „Test12345“ AND KeyName2 BETWEEN ’01.09.2012 00:00:00’ AND
’01.04.2013 23:59:00’) OR (KeyName3 > 15.3 AND KeyName4 = True)

就像:

...EnMDat1 JOIN EnMDat3 ON ... AND EnMDat1.TextValue ='Test12345' AND EnMDat3.DateValue BETWEEN ’01.09.2012 00:00:00’ AND
’01.04.2013 23:59:00’)
...
UNION ALL 
...
EnMDat2 JOIN EnMDat4 ON ... AND EnMDat2.NumericValue > 15.3 AND EnMDat4.BoolValue = True

这将比一个元数据表工作得更快。但是,如果您在 where 子句中有许多不同的场景,您将需要一个引擎来构建查询。您也可以省略视图,每次都从头开始编写语句。

于 2017-11-30T16:14:05.817 回答