我们要求项目中存储数据库中实体的所有修订(更改历史记录)。目前,我们有两个设计方案:
e.g。为“员工”实体
设计1:
-- Holds Employee Entity
"Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"
-- Holds the Employee Revisions in Xml. The RevisionXML will contain
-- all data of that particular EmployeeId
"EmployeeHistories (EmployeeId, DateModified, RevisionXML)"
设计2:
-- Holds Employee Entity
"Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"
-- In this approach we have basically duplicated all the fields on Employees
-- in the EmployeeHistories and storing the revision data.
"EmployeeHistories (EmployeeId, RevisionId, DateModified, FirstName,
LastName, DepartmentId, .., ..)"
还有其他方法可以做这件事吗?
“设计1”的问题在于,每当您需要访问数据时,我们都必须解析XML。这将减慢进程并添加一些限制,例如我们无法在修订数据字段中添加联接。
“设计2”的问题在于我们必须复制所有实体上的每个字段(我们有大约70-80个实体,我们希望维护这些实体)。
答案 0 :(得分:50)
我认为这里要问的关键问题是“谁/将会使用历史?”
如果它主要用于报告/人类可读历史,我们过去已实施此计划......
创建一个名为'AuditTrail'的表或具有以下字段的表...
[ID] [int] IDENTITY(1,1) NOT NULL,
[UserID] [int] NULL,
[EventDate] [datetime] NOT NULL,
[TableName] [varchar](50) NOT NULL,
[RecordID] [varchar](20) NOT NULL,
[FieldName] [varchar](50) NULL,
[OldValue] [varchar](5000) NULL,
[NewValue] [varchar](5000) NULL
然后,您可以向所有表添加“LastUpdatedByUserID”列,每次在表上执行更新/插入时都应该设置这些列。
然后,您可以为每个表添加一个触发器,以捕获所发生的任何插入/更新,并在此表中为每个已更改的字段创建一个条目。由于每个更新/插入的表都提供了“LastUpdateByUserID”,因此您可以在触发器中访问此值,并在添加到审计表时使用它。
我们使用RecordID字段来存储正在更新的表的键字段的值。如果它是一个组合键,我们只是在字段之间用'〜'进行字符串连接。
我确信这个系统可能有缺点 - 对于大量更新的数据库,性能可能会受到影响,但对于我的网络应用程序,我们获得了比写入更多的读取,并且它似乎表现得非常好。我们甚至编写了一个小的VB.NET实用程序来根据表定义自动编写触发器。
只是一个想法!
答案 1 :(得分:36)
CREATE VIEW EmployeeHistory
AS
, FirstName, , DepartmentId
SELECT EmployeeId, RevisionXML.value('(/employee/FirstName)[1]', 'varchar(50)') AS FirstName,
RevisionXML.value('(/employee/LastName)[1]', 'varchar(100)') AS LastName,
RevisionXML.value('(/employee/DepartmentId)[1]', 'integer') AS DepartmentId,
FROM EmployeeHistories
答案 2 :(得分:19)
History Tables博客中的Database Programmer文章可能很有用 - 涵盖了此处提出的一些要点,并讨论了增量的存储。
修改强>
在History Tables文章中,作者(Kenneth Downs)建议维护至少七列的历史表:
不应在历史记录表中跟踪永不更改或不需要其历史记录的列以避免膨胀。存储数值的增量可以使后续查询更容易,即使它可以从旧值和新值派生。
历史记录表必须是安全的,禁止非系统用户插入,更新或删除行。只应支持定期清除以减小总体大小(如果用例允许)。
答案 3 :(得分:13)
我们已经实施了一个非常类似于Chris Roberts建议的解决方案的解决方案,这对我们来说非常有效。
唯一的区别是我们只存储新值。旧的值毕竟存储在上一个历史记录行
中[ID] [int] IDENTITY(1,1) NOT NULL,
[UserID] [int] NULL,
[EventDate] [datetime] NOT NULL,
[TableName] [varchar](50) NOT NULL,
[RecordID] [varchar](20) NOT NULL,
[FieldName] [varchar](50) NULL,
[NewValue] [varchar](5000) NULL
假设您有一个包含20列的表格。这样,您只需存储已更改的确切列,而不必存储整行。
答案 4 :(得分:13)
避免设计1;一旦你需要回滚到旧版本的记录 - 使用管理员控制台自动或“手动”,它就不是很方便了。
我真的没有看到Design 2的缺点。我认为第二个,History表应该包含第一个记录表中的所有列。例如。在mysql中,您可以轻松地创建与另一个表(create table X like Y
)具有相同结构的表。而且,当您要更改实时数据库中记录表的结构时,无论如何都必须使用alter table
命令 - 并且您也无需为历史记录表运行这些命令。
注释
RevisionId
列; ModifiedBy
- 创建特定修订版的用户。您可能还希望有一个字段DeletedBy
来跟踪删除特定修订的人。DateModified
应该是什么意思 - 要么意味着创建了这个特定修订版,要么意味着这个特定修订版被另一个版本替换。前者要求字段位于记录表中,并且在第一眼看来似乎更直观;但是,对于删除的记录(删除此特定修订版的日期),第二种解决方案似乎更实用。如果您选择第一个解决方案,则可能需要第二个字段DateDeleted
(仅当您需要它时)。取决于您和您实际想要记录的内容。设计2中的操作非常简单:
的修改强>如果你选择设计2,那么所需的所有SQL命令都非常简单,维护也很简单!也许,如果你在记录表中使用辅助列(RevisionId
,DateModified
),那么会更加容易 - 将两个表保持在完全相同的结构(除了独特的钥匙)!这将允许简单的SQL命令,它可以容忍任何数据结构的变化:
insert into EmployeeHistory select * from Employe where ID = XX
不要忘记使用交易!
至于扩展,这个解决方案非常有效,因为你不会来回转换任何来自XML的数据,只是复制整个表行 - 非常简单的查询,使用索引 - 非常高效!
答案 5 :(得分:12)
如果您必须存储历史记录,请创建一个与您正在跟踪的表格具有相同架构的影子表格以及“修订日期”和“修订版类型”列(例如“删除”,“更新”)。写入(或生成 - 见下文)一组触发器来填充审计表。
制作一个能够读取表的系统数据字典的工具并生成一个创建影子表的脚本和一组用于填充它的触发器,这是相当简单的。
不要尝试使用XML,XML存储的效率远低于此类触发器使用的本机数据库表存储。
答案 6 :(得分:7)
Ramesh,我参与了基于第一种方法的系统开发 事实证明,将修订版存储为XML会导致数据库的巨大增长并显着减慢速度 我的方法是每个实体有一个表:
Employee (Id, Name, ... , IsActive)
其中 IsActive 是最新版本的标志
如果要将一些其他信息与修订相关联,可以创建单独的表 包含该信息并使用PK \ FK关系将其与实体表链接。
这样您就可以将所有版本的员工存储在一个表中。 这种方法的优点:
请注意,您应该允许主键不唯一。
答案 7 :(得分:6)
我过去看过这种方式的方式是
Employees (EmployeeId, DateModified, < Employee Fields > , boolean isCurrent );
你永远不会“更新”这个表(除了更改isCurrent的有效),只需插入新行。对于任何给定的EmployeeId,只有1行可以具有isCurrent == 1.
维护它的复杂性可以通过视图和“而不是”触发器隐藏(在oracle中,我假设其他RDBMS类似的东西),如果表太大而无法处理,你甚至可以转到物化视图通过索引)。
这种方法没问题,但最终可能会遇到一些复杂的问题。
就个人而言,我非常喜欢你的Design 2方式,这也是我过去的做法。它易于理解,易于实现且易于维护。
它还为数据库和应用程序创建了非常少的开销,尤其是在执行读取查询时,这很可能是99%的时间。
自动创建历史表和触发器以维护(假设它将通过触发器完成)也很容易。
答案 8 :(得分:4)
我将与您分享我的设计,它与您的两种设计不同,因为每个实体类型需要一个表。我发现描述任何数据库设计的最好方法是通过ERD,这是我的:
在此示例中,我们有一个名为 employee 的实体。 用户表保存用户的记录,实体和 entity_revision 是两个表,其中包含您将拥有的所有实体类型的修订历史记录系统。以下是这种设计的工作原理:
entity_id 和 revision_id
的两个字段系统中的每个实体都有自己唯一的实体ID。您的实体可能会进行修订,但其entity_id将保持不变。您需要将此实体ID保留在employee表中(作为外键)。您还应该将实体的类型存储在实体表中(例如“员工”)。现在,对于revision_id,正如其名称所示,它会跟踪您的实体修订。我找到的最好方法是使用 employee_id 作为revision_id。这意味着您将为不同类型的实体提供重复的修订ID,但这对我没有好处(我不确定您的情况)。要做的唯一重要的注意事项是entity_id和revision_id的组合应该是唯一的。
entity_revision 表中还有一个 state 字段,表示修订状态。它可以属于以下三种状态之一:latest
,obsolete
或deleted
(不依赖于修订日期可以帮助您提高查询速度)。
关于revision_id的最后一点说明,我没有创建将employee_id连接到revision_id的外键,因为我们不希望为将来可能添加的每个实体类型更改entity_revision表。
<强> INSERTION 强>
对于要插入数据库的每个员工,您还将向实体和 entity_revision 添加记录。最后两条记录将帮助您跟踪记录插入数据库的人和记录。
<强>更新强>
现有员工记录的每次更新都将实现为两个插入,一个在employee表中,另一个在entity_revision中。第二个将帮助您了解记录更新的对象和时间。
<强> DELETION 强>
要删除员工,会在entity_revision中插入一条记录,说明删除并完成。
正如您在本设计中所看到的,数据不会被更改或从数据库中删除,更重要的是,每个实体类型只需要一个表。我个人觉得这个设计非常灵活,易于使用。但我不确定你,因为你的需求可能会有所不同。
<强> [UPDATE] 强>
在新的MySQL版本中支持分区后,我相信我的设计也带来了最好的表现之一。可以使用entity
字段对type
字段进行分区,而使用entity_revision
字段对分区state
进行分区。这将提升SELECT
查询,同时保持设计简洁。
答案 9 :(得分:3)
如果您想要执行第一个,您可能也想为Employees表使用XML。大多数较新的数据库允许您查询XML字段,因此这并不总是一个问题。无论是最新版本还是早期版本,都可以通过一种方式访问员工数据。
我会尝试第二种方法。您可以通过只有一个带有DateModified字段的Employees表来简化此操作。 EmployeeId + DateModified将是主键,您只需添加一行即可存储新版本。这样,归档旧版本和从归档中恢复版本也更容易。
另一种方法可能是Dan Linstedt的datavault model。我为荷兰统计局做了一个使用这个模型的项目,它运作得很好。但我不认为它对日常数据库使用有直接用处。你可能会从阅读他的论文中得到一些想法。
答案 10 :(得分:3)
如果确实只需要审计跟踪,我就会倾向于审计表解决方案(完成其他表上重要列的非规范化副本,例如UserName
)。但请记住,这种痛苦的经历表明,单一的审计表将成为未来的巨大瓶颈;为所有审计表创建单独的审计表可能是值得的。
如果您需要跟踪实际的历史(和/或未来)版本,那么标准解决方案是使用开始,结束和持续时间值的某种组合来跟踪具有多行的同一实体。您可以使用视图来方便地访问当前值。如果这是您采用的方法,如果您的版本化数据引用了可变但未修改的数据,则可能会遇到问题。
答案 11 :(得分:3)
数据修订是时间数据库的“valid-time”概念的一个方面。很多研究已经进入这个阶段,并且出现了许多模式和指南。我写了一篇冗长的回复,对那些感兴趣的人提出了this个问题。
答案 12 :(得分:2)
怎么样:
您创建主键(EmployeeId,DateModified),并获取“当前”记录,您只需为每个employeeid选择MAX(DateModified)。存储IsCurrent是一个非常糟糕的主意,因为首先,它可以被计算,其次,数据很容易失去同步。
您还可以创建一个仅列出最新记录的视图,并且大多数在您的应用中工作时使用它。这种方法的好处在于您没有重复的数据,并且您不必从两个不同的位置收集数据(当前在Employees中,并在EmployeesHistory中存档)以获取所有历史记录或回滚等)
答案 13 :(得分:2)
如果您想依赖历史数据(出于报告原因),您应该使用以下结构:
// Holds Employee Entity
"Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"
// Holds the Employee revisions in rows.
"EmployeeHistories (HistoryId, EmployeeId, DateModified, OldValue, NewValue, FieldName)"
全球应用解决方案:
// Holds Employee Entity
"Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"
// Holds all entities revisions in rows.
"EntityChanges (EntityName, EntityId, DateModified, OldValue, NewValue, FieldName)"
您也可以用XML保存修订版,然后一个修订版只有一条记录。这将是:
// Holds Employee Entity
"Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"
// Holds all entities revisions in rows.
"EntityChanges (EntityName, EntityId, DateModified, XMLChanges)"
答案 14 :(得分:1)
我们有类似的要求,我们发现用户只是想看更改的内容,而不是回滚任何更改。
我不确定您的用例是什么,但我们所做的是创建和审计表,该表随着对业务实体的更改而自动更新,包括任何外键引用和枚举的友好名称。
每当用户保存他们的更改时,我们重新加载旧对象,运行比较,记录更改并保存实体(所有这些都在单个数据库事务中完成,以防出现任何问题)。
这对我们的用户来说似乎非常有效,并且让我们头疼的是拥有一个与我们的业务实体具有相同字段的完全独立的审计表。
答案 15 :(得分:0)
听起来您想跟踪特定实体随时间的变化,例如ID 3,“bob”,“123主要街道”,然后是另一个ID 3,“bob”“234 elm st”,依此类推,实质上是能够找出修订历史,显示每个地址“bob”已经在
执行此操作的最佳方法是在每条记录上都有一个“当前”字段,并且(可能)是时间戳或FK到日期/时间表。
插入必须设置“当前”并且还取消设置前一个“当前”记录中的“当前”。查询必须指定“当前”,除非您想要所有历史记录。
如果它是一个非常大的表,或者预期会有大量修订,那么还有进一步的调整,但这是一种相当标准的方法。