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

Sey*_*avi 11 mysql sql-server oracle asp.net-mvc database-design

我已经分配了一个任务来创建(相对)简单的报告系统.在这些系统中,将向用户显示报告的表格结果.表有一些字段,每个字段为每个记录中的用户提供一些信息.但问题是开发人员不会声明每个报告字段.它必须由系统用户声明.所以我的报告表是动态的.

我在" ASP.NET MVC中的数据驱动自定义视图引擎"中看到了使用Asp.net MVC Framework创建动态表单的示例,但我不知道这对我的系统是否合适.

UPDATE1:

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

在此输入图像描述

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

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

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

Jon*_*ler 13

通过更换避免stringly类型的数据VALUENUMBER_VALUE,DATE_VALUE,STRING_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'
    
    Run Code Online (Sandbox Code Playgroud)

    你能发现这个bug吗?即使您以正确的格式存储日期,上述查询也很危险,很少有开发人员知道如何正确修复它.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';
    
    Run Code Online (Sandbox Code Playgroud)

    您不希望告诉每个开发人员以这种方式编写查询.


Mat*_*eak 8

您的设计是实体属性值(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)
Run Code Online (Sandbox Code Playgroud)

结果是:

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
Run Code Online (Sandbox Code Playgroud)

这是22秒,每个3列获得30,000行.也就是说方式太长.从一张平台上我们可以看到2秒以内,很容易.


Ric*_*mes 5

使用 MariaDB 及其动态列。实际上,这可以让您将所有杂项列放入一个列中,但仍然可以有效地访问它们。

我会将一些公共字段保留在自己的列中。

更多关于 EAV 的讨论和建议(以及如何在没有动态列的情况下做到这一点)。