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

时间:2015-07-30 10:30:15

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

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

我在' Data Driven Custom View Engine in ASP.NET MVC'使用Asp.net MVC Framework创建动态表单,但我不知道这对我的系统是否合适。

UPDATE1:

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

enter image description here

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

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

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

4 个答案:

答案 0 :(得分:13)

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 :(得分: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)

结果是:

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秒,很容易。

答案 2 :(得分:3)

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

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

More discussion of EAV和建议(以及如何在没有动态列的情况下执行此操作)。

答案 3 :(得分:1)

嗯,您对将数据存储在正确的数据类型中有一个非常好的观点 我同意这确实对用户定义的数据系统造成了问题。

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

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

Sql server也有一个名为sql_variant的数据类型,它应该支持多种类型,虽然我从未使用它,但它的文档看起来很有希望。