【问题标题】:Design Pattern for Custom Fields in Relational Database关系数据库中自定义字段的设计模式
【发布时间】:2015-10-21 16:00:57
【问题描述】:

我分配了一项任务来创建(相对)简单的报告系统。在这些系统中,用户将看到报表的表格结果。一个表有一些字段,每个字段在每条记录中向用户提供部分信息。然而,我的问题是 developer 不会声明每个报告字段。它必须由系统的用户声明。所以我的报告表是动态的。

我在“Data Driven Custom View Engine in ASP.NET MVC”中看到了使用 Asp.net MVC 框架创建动态表单的示例,但我不知道这是否适合我的系统。

更新1:

目前我以以下实体关系图结束:

在上图中,我将报告的每条记录存储在Report 表中。我还将报告类型存储在ReportType 中。对于将在报告记录中使用的每个字段,我将使用ReportFieldValue。字段类型将存储在ReportField

所以如果我想先向我的数据库添加一条记录,我会在Report 表中添加一行。然后对于每个添加的记录字段,我将在ReportFieldValue 表中添加一行。

但是,您可能会注意到,在这些方法中,我必须将每个字段值存储在 char(255) 中。问题在于不应存储为字符串的字段类型(如 datetime)。这种类型的系统是否有任何设计模式或架构?

【问题讨论】:

  • 您使用的是什么 rdbms?是mysql、oracle还是sql server?
  • @ZoharPeled 我没有选择任何数据库,但您可以随意回答。

标签: mysql sql-server asp.net-mvc oracle database-design


【解决方案1】:

使用 MariaDB,它是 Dynamic Columns。实际上,这使您可以将所有杂项列放在一个列中,但仍然可以让您有效地访问它们。

我会在自己的列中保留一些常用字段。

More discussion of EAV 和建议(以及如何在没有动态列的情况下做到这一点)。

【讨论】:

  • 现在 MySQL 有了 JSON 列数据类型。
【解决方案2】:

您的设计是实体属性值 (EAV) 数据模型的变体,在数据库设计中通常被视为一种反模式。

也许对您来说更好的方法是创建一个包含 300 列(NUMBER_VALUE_1 到 NUMBER_VALUE_100、VARCHAR2_VALUE_1..100 和 DATE_VALUE_1..100)的报告值表。

然后,围绕跟踪哪些报告使用哪些列以及它们使用每列的用途来设计数据模型的其余部分。

这有两个好处:首先,您无需将日期和数字存储在字符串中(其好处已被指出),其次,您避免了与 EAV 模型相关的许多性能和数据完整性问题。

编辑——添加一些 EAV 模型的经验结果

使用 Oracle 11g2 数据库,我将 30,000 条记录从一个表移动到 EAV 数据模型中。然后我查询模型以取回这 30,000 条记录。

SELECT SUM (header_id * LENGTH (ordered_item) * (SYSDATE - schedule_ship_date))
FROM   (SELECT rf.report_type_id,
               rv.report_header_id,
               rv.report_record_id,
               MAX (DECODE (rf.report_field_name, 'HEADER_ID', rv.number_value, NULL)) header_id,
               MAX (DECODE (rf.report_field_name, 'LINE_ID', rv.number_value, NULL)) line_id,
               MAX (DECODE (rf.report_field_name, 'ORDERED_ITEM', rv.char_value, NULL)) ordered_item,
               MAX (DECODE (rf.report_field_name, 'SCHEDULE_SHIP_DATE', rv.date_value, NULL)) schedule_ship_date
        FROM   eav_report_record_values rv INNER JOIN eav_report_fields rf ON rf.report_field_id = rv.report_field_id
        WHERE  rv.report_header_id = 20 
        GROUP BY rf.report_type_id, rv.report_header_id, rv.report_record_id)

结果是:

1 row selected.

Elapsed: 00:00:22.62

Execution Plan
----------------------------------------------------------

----------------------------------------------------------------------------------------------------
| Id  | Operation                       | Name                        | Rows  | Bytes | Cost (%CPU)|
----------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                |                             |     1 |  2026 |    53  (67)|
|   1 |  SORT AGGREGATE                 |                             |     1 |  2026 |            |
|   2 |   VIEW                          |                             |   130K|   251M|    53  (67)|
|   3 |    HASH GROUP BY                |                             |   130K|   261M|    53  (67)|
|   4 |     NESTED LOOPS                |                             |       |       |            |
|   5 |      NESTED LOOPS               |                             |   130K|   261M|    36  (50)|
|   6 |       TABLE ACCESS FULL         | EAV_REPORT_FIELDS           |   350 | 15050 |    18   (0)|
|*  7 |       INDEX RANGE SCAN          | EAV_REPORT_RECORD_VALUES_N1 |   130K|       |     0   (0)|
|*  8 |      TABLE ACCESS BY INDEX ROWID| EAV_REPORT_RECORD_VALUES    |   372 |   749K|     0   (0)|
----------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   7 - access("RV"."REPORT_HEADER_ID"=20)
   8 - filter("RF"."REPORT_FIELD_ID"="RV"."REPORT_FIELD_ID")

Note
-----
   - 'PLAN_TABLE' is old version


Statistics
----------------------------------------------------------
          4  recursive calls
          0  db block gets
     275480  consistent gets
        465  physical reads
          0  redo size
        307  bytes sent via SQL*Net to client
        252  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
          1  rows processed

获得 30,000 行每行 4 列需要 22 秒。这方式太长了。从我们可以在 2 秒内查看的平板电脑上看,很容易。

【讨论】:

  • EAV模型在适当的场景下很有用,提问者的声音就是其中之一。创建 300 列听起来像是一场噩梦,尤其是当需要使用、链接或导入/导出数据到/从其他系统时!
  • 我不同意。如果一份报告真的需要全部 300 列,我会很感激将它们全部记录在一个记录中。我不能对所有 RDBMS 平台说话,但在 Oracle 上,单个记录的性能将比 EAV 模型好几个数量级。此外,根据您有多少时间,您可以使单记录方法非常易于使用(例如,将逻辑放入您的应用程序中,以便在元数据更改时使用元数据来编译特定于报告的数据库视图)跨度>
  • 我不知道哪个更糟糕,EAV 还是 300 个匿名列。
  • 两者都合作过,我绝对认为前者更糟。您可以努力减轻 300 列方法的困难(例如,基于元数据的动态维护视图层)。除非您有非常(不切实际?)简单的要求(例如,您的报告没有过滤条件和/或系统中没有很多报告数据),否则您几乎无法减轻 EAV 的缺点。跨度>
  • 如何索引这么多字段,由于索引很多,db 将比实际数据大 300 倍,并且您将存储数百万个空值。此外,您还必须构建动态 sql 才能在此类集合上执行搜索。我已经看到了这样的模型(Meridio DMS),并且可以通过标准化数据方法轻松超越它。 2 秒内 30k 条记录仍然很长。
【解决方案3】:

通过将VALUE 替换为NUMBER_VALUEDATE_VALUESTRING_VALUE,避免使用字符串类型的数据。这三种类型在大多数情况下都足够好。 如果需要,您可以稍后添加 XMLTYPE 和其他花哨的列。对于 Oracle,请使用 VARCHAR2 而不是 CHAR 以节省空间。

始终尝试将值存储为正确的类型。原生数据类型更快、更小、更易于使用且更安全。

Oracle 有一个通用数据类型系统(ANYTYPE、ANYDATA 和 ANYDATASET),但这些类型很难使用,在大多数情况下应该避免使用。

架构师通常认为对所有数据使用单个字段会使事情变得更容易。它使生成数据模型的漂亮图片变得更容易,但它使一切 否则更难。考虑以下问题:

  1. 在不知道数据类型的情况下,您无法对数据做任何有趣的事情。即使要显示数据,了解类型以证明文本的合理性也很有用。在所有的 99.9% 用例 3 列中的哪一列是相关的,对用户来说是显而易见的。
  2. 针对字符串类型的数据开发类型安全的查询是很痛苦的。例如,假设您要查找出生于这个千年的人的“出生日期”:

    select *
    from ReportFieldValue
    join ReportField
        on ReportFieldValue.ReportFieldid = ReportField.id
    where ReportField.name = 'Date of Birth'
        and to_date(value, 'YYYY-MM-DD') > date '2000-01-01'
    

    你能发现错误吗?即使您以正确的格式存储日期,上述查询也是危险的,而且很少有开发人员知道如何正确修复它。 Oracle 的优化使得强制执行特定的操作顺序变得困难。为了安全起见,您需要这样的查询:

    select *
    from
    (
        select ReportFieldValue.*, ReportField.*
            --ROWNUM ensures type safe by preventing view merging and predicate pushing.
            ,rownum
        from ReportFieldValue
        join ReportField
            on ReportFieldValue.ReportFieldid = ReportField.id
        where ReportField.name = 'Date of Birth'
    )
    where to_date(value, 'YYYY-MM-DD') > date '2000-01-01';
    

    您不想告诉每个开发人员都以这种方式编写查询。

【讨论】:

  • +1 表示“字符串类型”,以及为每种类型使用单独列的值。我已经进行了从前者到后者的转换,好处是巨大的。
  • @Jon Heller 请解释上述两个查询之间的区别。从逻辑上讲,它们在我看来都是一样的。
  • @Rahul 你是对的,它们看起来在逻辑上是等价的。不同之处在于ROWNUM 强制首先执行内联视图。这可确保在值过滤器之前应用名称过滤器,从而强制日期转换仅针对正确的类型进行。这令人困惑且难以重现,因为在某些情况下,Oracle 可能会在值过滤器之前自动应用名称过滤器,这就是为什么最好通过表更改来避免这些情况。
  • @JonHeller 明白了。非常感谢。 :)
【解决方案4】:

嗯,关于以正确的数据类型存储数据,您有一个很好的观点。
我同意这确实给用户定义的数据系统带来了问题。

解决此问题的一种方法是为每个数据类型组(整数、浮点、字符串、二进制和日期)添加表,而不是将值保留在 ReportFieldValue 表中。 但是,这会让您的生活更加艰难,因为您必须选择并连接多个表才能获得一个结果。

另一种方法是在ReportFieldValue 中添加一个数据类型列并创建一个用户定义的函数以将数据从字符串动态转换为适当的数据类型(使用数据类型列中的值),以便您可以将其用于排序、搜索等。

Sql 服务器还有一个名为 sql_variant 的数据类型,它应该支持多种类型,虽然我从未使用过它,但它的文档似乎很有希望。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2010-09-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-06-06
    相关资源
    最近更新 更多