具有有效性间隔的可移植表设计(历史化、时态数据库)

MrS*_*rub 5 postgresql index oracle database-design sql-server

我正在为一个应用程序设计一个数据模型,它必须跟踪数据的变化。

第一步,我的应用程序必须支持PostgreSQL,但我想在第二步中添加对其他 RDBMS(尤其是 Oracle 和 MS SQL 服务器)的支持。因此,我想选择使用较少专有功能的便携式数据模型。(表的 DDL 可能因 RDBMS 供应商而异。但应用程序中的 SQL 查询/语句对于所有支持的供应商应尽可能相同。)

例如,假设有users一张users_versions桌子。users_versions在 上有一个外键users

表的示例可能如下所示:

users
----------------
id | username
---------------- 
 1 | johndoe
 2 | sally

users_versions --> references id of user (userid)
---------------------------------------------------------------------------
id | userid | name     | street      | place     | validfrom  | validuntil
---------------------------------------------------------------------------
 1 |      1 | John Doe | 2nd Fake St | Faketown  | 2018-01-04 | 2018-01-05
 2 |      1 | John Doe | Real St 23  | Faketown  | 2018-01-05 | null
 3 |      2 | Sally Wu | Main St 1   | Lake Fake | 2018-04-02 | 2018-04-20
 4 |      2 | Sally Wu | Other St 99 | Chicago   | 2018-04-20 | null
Run Code Online (Sandbox Code Playgroud)

大多数 SQL 查询将查询当前有效的条目。在上面的概念示例中,这个 woule 看起来像

SELECT *
  FROM users_versions uv 
  INNER JOIN users u ON u.id = uv.userid
  WHERE uv.userid = 123 AND uv.validuntil IS NULL;
Run Code Online (Sandbox Code Playgroud)

某些用例(报告等)还需要选择数据的历史版本(例如,哪些数据在2017-12-31?)。但是这些在我的应用程序中不会对性能至关重要。

在上面的示例中,我可能会创建一个过滤的唯一索引validuntil以确保一次只有 1 个具有无限有效性的条目:

CREATE UNIQUE INDEX foo
  ON users_versions ( userid ) 
  WHERE validuntil IS NULL;
Run Code Online (Sandbox Code Playgroud)

据我所知,过滤索引只能用于 PostgreSQL 和 MS SQL 中的查询优化,而不能用于 Oracle。此外,索引null也可能是一件棘手的事情(可能/仅在多列索引中/不可能)。

因此,一种不同的方法users_versions可能是上面的结构加上valid由应用程序管理的显式列。最近的条目将获得一个1,所有历史条目将获得一个0。然后我可以创建两个索引,一个用于查询优化,一个用于完整性强制(一次只有 1 个有效条目):

CREATE INDEX optimization
  ON users_versions ( userid, valid );
Run Code Online (Sandbox Code Playgroud)

对于如下查询:

SELECT *
  FROM users_versions uv 
  INNER JOIN users u ON u.id = uv.userid
  WHERE uv.userid = 123 AND uv.valid = 1;
Run Code Online (Sandbox Code Playgroud)

还有一个索引来强制当前版本的完整性(例如 ORACLE 版本):

-- ORACLE: Entry with null-only columns ignored in indexing:
CREATE UNIQUE INDEX only_one_valid_version_per_user
  ON users_versions ( 
    CASE WHEN valid = 1 THEN userid ELSE null END,
    CASE WHEN valid = 1 THEN valid  ELSE null END
  );
Run Code Online (Sandbox Code Playgroud)

可能这个索引不能用于查询优化,但它应该确保每个只能有 1 个有效条目,userid但对于同一个userid.

您对这种允许使用性能的历史表的便携式设计有何建议?

  • validfrom+ validuntil,在当前有效的条目中validuntil设置为 (nullable)null
  • validfrom+ validuntilvaliduntil(不可为空)设置为远期日期,就像2999-12-31在当前有效条目中一样
  • validfrom+ validuntil+valid标志,与valid标志由应用程序管理和查询用于当前有效入境
  • ……?

插入新版本时,我的应用程序将始终执行两个步骤:

  • 使当前版本无效(设置validuntil为当前日期(加上,可选地,将valid标志设置为0))
  • 插入新版本(validfrom current date,加上,可选,带valid标志1

我没有要求数据库强制执行历史条目的无重叠时间间隔。我只需要确保只有 1 个条目具有无限的有效性。

对于一些非常大的表,可能值得拆分为currenthistory表:一张表仅包含当前有效版本 ( users_versions_current),另一张表包含所有历史版本 ( users_versions_history)。每当插入新版本时,以前的版本都会使用validfrom/validuntil插入..._history表中。

我应该考虑哪些方面?您了解文献、最佳实践建议等吗?

MDC*_*CCL 4

我必须说我同意其他答案的精神,并且我认为您应该首先专注于构建一个具有特定数据库管理系统(DBMS)的最佳数据库;可移植性虽然很重要,但应该是次要的。

\n\n
\n\n

从你提问的内容来看,你对这个问题似乎很熟悉。无论如何,我在这篇文章另一篇文章(包含示例图、说明性 DDL 代码等)中分享了我对涉及时间功能的两个场景的看法,以防您想看一下并建立一些类比。

\n\n

概念检查

\n\n

从概念层面开始分析,所考虑的业务规则可以表述如下:

\n\n
    \n
  • 可以有一对多用户
  • \n
  • 用户仅持有一个CurrentVersion
  • \n
  • 用户持有零个多个过去版本
  • \n
\n\n

如所示,实体类型CurrentVersionPastVersion涉及一对零或多对(或零对多对一)关联。除了基数之外,还可以推断我们正在处理两种不同的实体类型,因为在这种情况下,CurrentVersion实例没有ValidUntil属性,而PastVersion的所有实例都必须具有该属性。

\n\n

逻辑层次安排

\n\n

因此,我建议(a)为 \xe2\x80\x9ccurrent version\xe2\x80\x9d 行提供一个基表,(b)为 \xe2\x80\x9cpast version\xe2\x80\x9d 行提供一个基表。通过这种方式,每个表中保留的断言(即行)表示明显不同的 \xe2\x80\x94,尽管相关联的 \xe2\x80\x94 类型的事实(根据关系模型理论)避免了临时性在单个表中引入歧义。

\n\n

考虑到user您提出的示例 \xe2\x80\x94 并与上面的概念定义一致\xe2\x80\x94,两个表的结构几乎相同,但是user_version(即 \xe2\x80 \x9cpast\xe2\x80\x9d 版本)包括一个附加valid_until列,它user_id必须组成所述表的复合主键。该user_version.user_id列必须被约束为 FOREIGN KEY 引用user.user_id

\n\n

操纵

\n\n

当最新的 \xe2\x80\x9cup-to-date\xe2\x80\x9d 版本必须是 \xe2\x80\x9csaved\xe2\x80\x9c 时,\xe2\x80\x9cprevious\xe2\ 的整行x80\x9d 版本对表进行 INSERT 操作user_version,附加相应的值来指示执行操作的valid_until确切时刻。依次,user(即 \xe2\x80\x9ccurrent\xe2\x80\x9c)表中 \xe2\x80\x9c 行的值替换为 \xe2\x80\x9c 最近的行\xe2\x80\x9c 的,通过更新。

\n\n

表中的每一行都user满足您必须确保的无限有效性的需求(没有列valid_until,这些值在更新之前仍然有效,而更新可能永远不会到达)。

\n\n

正直

\n\n

当然,必须注意关联值的顺序(例如,防止重叠、拒绝无效日期等),就像整体完整性一样。我将利用ACID 事务来保证相关操作被视为 DBMS 本身内的单个工作单元。具有适当权限的存储过程(或 Postgres 中的函数)也会非常有帮助。

\n\n

不需要可为 NULL 的列 \xe2\x80\x94 带有 NULL 标记的表不描绘数学关系,因此不能期望它的行为如此,它可以标准化,等等。\xe2\x80\x94,也不能由一个valid(或多个)应用程序\xe2\x80\x94管理的列,它会违反数据库自我保护的原则\xe2\x80\x94,从而危及数据质量。

\n\n

可推导性

\n\n

和中的值之间的周期代表某个 \xe2\x80\x9cpast\xe2\x80\x9d 行在 \xe2\x80\x9ccurrent\xe2\x80\x9d 或 \xe2\x80\x9c effective\ 期间的整个时间。xe2\x80\x9d(可以按分钟等计算,并且可以方便地合并到视图中或在应用程序[app]代码中计算)。这个和其他相关方面意味着通过数据操作操作(主要是 SELECT 和一些子查询)导出数据。user_version.valid_fromuser_version.valid_untilvalidity_interval

\n\n

通过一个或多个应用程序从外部访问数据库

\n\n

构建,让我们说,一个面向对象的编程\xe2\x80\x9c中间层\xe2\x80\x9d,依次由一个或多个应用程序的\xe2\x80\x9c更高层\xe2\x80\x9d消耗(或另一种软件组件)也有助于数据库的可移植性,允许代码重用以及与迁移到其他 DBMS 的相当大的隔离。这个关于 .NET (C#) 中的存储库模式的资源可以带来这方面的一些想法。

\n\n

便携性考虑

\n\n

区分基于 SQL DBMS 构建的数据库的两个不同抽象级别非常重要。表的 (1) 结构和 (2) 约束以及 (3) 在表上执行的数据操作操作 \xe2\x80\x94INSERT、SELECT、UPDATE、DELETE、其组合\xe2\x80\x94 是以下元素逻辑级别。支持表和/或约束的 (4) 底层索引是 (\xe2\x80\x9clower\xe2\x80\x9c) 物理级组件。

\n\n

通过这种方式,相同的逻辑设计原则适用于所有主要的 SQL 平台,但是,正如您在问题中提到的,各种 SQL DBMS 提供的用于创建逻辑元素的工具之间的差异主要是语法上的,因此可移植性将是受某些方言特定的 SQL(DDL 和 DML)功能(可能还受 DBMS 特定的数据类型特征和名称)的影响,因此方便的方法是编写符合 ISO/IEC/ANSI 标准语法的 SQL 代码只要可行。

\n\n

您将面临的其他问题是,根据使用的特定 DBMS,相同的(逻辑)查询将以不同的方式执行(在物理级别),因此响应时间会相差很大,因此,您将不得不进行一些重写以提高速度。

\n\n

关于物理级别的机制,是的,每个 SQL 平台都提供不同类型的索引,尽管您应该确保特定于平台的索引设置不会影响数据库的逻辑级别布局(i ) 在同一个 DBMS 上创建/更改索引时或 (ii) 将某个数据库移植到另一个 DBMS 时(这一点与物理数据独立性相关)。

\n\n

在这方面,拥有一个完全独立于数据定义语言(DDL)的强大的存储定义语言(SDL)将非常方便,这将有助于实现逻辑层和物理层之间的关注点的明确分离,但这是一个不同的故事,所以你应该尝试尽可能地将逻辑声明的代码与物理设置的代码分开,以帮助可移植性\xe2\x80\x94很难用当前的DDL混合特性来完成,我知道\xe2\x80 \x94。

\n\n

速度

\n\n

此外,在抽象的物理层面上,您应该优化数据库的性能(通过单列或多列索引、升级网络带宽、改进操作系统和/或 DBMS 和/或硬件配置等),不损害逻辑结构和约束的质量,因此,(1) 数据的完整性和 (2) 结果集的可靠性面临风险。逻辑连贯性是总体性能中最重要的因素,提供不连贯信息的软件很难被视为数据库,即使 \xe2\x80\x9cworks\xe2\x80\x9d 特别快也没关系。数据库的可靠性和速度无疑是齐头并进的。

\n\n

观察

\n\n

至于你的问题中提供的说明性数据,看起来该users_versions.id列过多,因为它似乎是一个额外的列,旨在保留系统控制的代理键(例如, SQL Server 表中具有IDENTITY属性的列),使该表在逻辑上比必要的更宽,这意味着物理级别(例如补充索引)的 \xe2\x80\x9cheavier\xe2\x80\x9d 结构(以字节为单位),从而减慢数据操作操作的执行。

\n\n

此外,由于代理键值没有意义,因此它的封闭列不太可能被指定为 SELECT 操作的 WHERE 子句中的条件(相反,大多数查询可能会包含users_versions.user_idand/orvalid_from和/or valid_until,两者都包含 \xe2 \x80\x9cnatural\xe2\x80\x9d PRIMARY KEY),因此users_versions.id实际上不会增加​​任何好处,它实际上会成为一种负担,需要不必要的管理。鉴于上述所有内容,我认为这是优化整体系统功能和管理时应考虑的另一个因素。

\n\n

我对系统控制代理键列的更详细看法包含在这个答案中,如果您感兴趣的话,也包含在这个答案中。

\n\n

专门为一列启用时间功能

\n\n

在某些情况下,您必须仅为一列启用时间功能,因此这篇文章这篇文章也可以作为参考。

\n


归档时间:

查看次数:

533 次

最近记录:

8 年 前