关系数据库中自定义字段的设计模式

Design Pattern for Custom Fields in Relational Database

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

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

更新1:

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

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

因此,如果我想先向我的数据库添加一条记录,我会向 Report Table 添加一行。然后对于每个添加的记录字段,我将添加一行到 ReportFieldValue table。

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

嗯,关于以正确的数据类型存储数据,你的观点非常好。
我同意这确实会给用户定义的数据系统带来问题。

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

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

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

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

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

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

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

  1. 如果不知道类型,就无法对数据做任何有趣的事情。即使要显示数据,了解证明文本的类型也很有用。在 99.9% 的所有 用例 3 列中的哪一列是相关的对用户来说是显而易见的。
  2. 针对字符串类型的数据开发类型安全的查询是很痛苦的。例如,假设您要为本千年出生的人查找 "Date of Birth":

    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';
    

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

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

也许更好的方法是创建一个报告值 table,其中包含 300 列(NUMBER_VALUE_1 到 NUMBER_VALUE_100,VARCHAR2_VALUE_1.. 100 和 DATE_VALUE_1..100).

然后,围绕跟踪哪些报告使用哪些列以及它们将每列用于什么目的来设计其余的数据模型。

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

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

使用 Oracle 11g2 数据库,我将 30,000 条记录从一个 table 移动到一个 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 秒。那是 way 太长了。从平坦的 table 我们会看到不到 2 秒,很容易。

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

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

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