【问题标题】:How to store historical records in a history table in SQL Server如何在 SQL Server 的历史表中存储历史记录
【发布时间】:2012-08-09 19:47:45
【问题描述】:

我有 2 张桌子,Table-ATable-A-History

  • Table-A 包含当前数据行。
  • Table-A-History 包含历史数据

我想在Table-ATable-A-History 中包含历史行中的最新数据行。

我可以想到两种方法来实现这一点:

  1. 每当有新数据行可用时,将当前行从Table-A 移动到Table-A-History 并使用最新数据更新Table-A 行(通过insert into selectselect into table

  2. 每当有新数据行可用时,更新Table-A 的行并将新行插入Table-A-History

在性能方面,方法 1 还是 2 更好?有没有更好的不同方法来实现这一点?

【问题讨论】:

标签: sql sql-server


【解决方案1】:

基本上,您希望跟踪/审核对表的更改,同时保持主表的大小。

有几种方法可以解决这个问题。下面讨论每种方式的优缺点。

1 - 使用触发器审核表。

如果您要审核表(插入、更新、删除),请查看我的如何防止不需要的事务 - 带有代码的 SQL 星期六幻灯片 - http://craftydba.com/?page_id=880。如果您选择,填充审计表的触发器可以保存来自多个表的信息,因为数据保存为 XML。因此,如有必要,您可以通过解析 XML 来取消删除操作。它会跟踪做出更改的人员和原因。

或者,您可以将审计表放在它自己的文件组中。

Description:
    Table Triggers For (Insert, Update, Delete)
    Active table has current records.
    Audit (history) table for non-active records.

Pros:
    Active table has smaller # of records.
    Index in active table is small.
    Change is quickly reported in audit table.
    Tells you what change was made (ins, del, upd)

Cons:
    Have to join two tables to do historical reporting.
    Does not track schema changes.

2 - 有效约会记录

如果您永远不会从审计表中清除数据,为什么不将该行标记为已删除,而是永久保留它?许多系统,如 people soft 使用有效约会来显示记录是否不再有效。在 BI 世界中,这称为类型 2 维表(缓慢变化的维度)。请参阅数据仓库研究所的文章。 http://www.bidw.org/datawarehousing/scd-type-2/ 每条记录都有开始和结束日期。

所有活动记录的结束日期为空。

Description:
    Table Triggers For (Insert, Update, Delete)
    Main table has both active and historical records.

Pros:
    Historical reporting is easy.
    Change is quickly shown in main table.

Cons:
    Main table has a large # of records.
    Index of main table is large.
    Both active & history records in same filegroup.
    Does not tell you what change was made (ins, del, upd)
    Does not track schema changes.

3 - 更改数据捕获(企业功能)。

Microsoft SQL Server 2008 引入了变更数据捕获功能。虽然这会在事后使用 LOG 读取器跟踪数据更改 (CDC), 它缺少诸如谁和什么做出了改变之类的东西。 MSDN 详细信息 - http://technet.microsoft.com/en-us/library/bb522489(v=sql.105).aspx

此解决方案取决于运行的 CDC 作业。 sql 代理的任何问题都会导致数据显示延迟。

查看变更数据捕获表。 http://technet.microsoft.com/en-us/library/bb500353(v=sql.105).aspx

Description:
    Enable change data capture

Pros:
    Do not need to add triggers or tables to capture data.
    Tells you what change was made (ins, del, upd) the _$operation field in 
    <user_defined_table_CT>
    Tracks schema changes.    

Cons:
    Only available in enterprise version.
    Since it reads the log after the fact, time delay in data showing up.
    The CDC tables do not track who or what made the change.
    Disabling CDC removes the tables (not nice)!
    Need to decode and use the _$update_mask to figure out what columns changed.

4 - 更改跟踪功能(所有版本)。

Microsoft SQL Server 2008 引入了更改跟踪功能。与 CDC 不同,它带有所有版本;但是,它带有一堆 TSQL 函数,您必须调用它们才能弄清楚发生了什么。

它旨在通过应用程序将一个数据源与 SQL 服务器同步。 TechNet 上有一个完整的同步框架工作。

http://msdn.microsoft.com/en-us/library/bb933874.aspx http://msdn.microsoft.com/en-us/library/bb933994.aspx http://technet.microsoft.com/en-us/library/bb934145(v=sql.105).aspx

与 CDC 不同,您可以指定数据库中的更改在被清除之前持续多长时间。此外,插入和删除不记录数据。更新仅记录更改了哪些字段。

由于您将 SQL Server 源同步到另一个目标,因此可以正常工作。 除非你写一个周期性的工作来找出变化,否则它不利于审计。

您仍然需要将这些信息存储在某个地方。

Description:
    Enable change tracking

Cons:
    Not a good auditing solution

前三个解决方案适用于您的审核。我喜欢第一个解决方案,因为我在我的环境中广泛使用它。

真诚的

约翰

演示文稿中的代码片段(汽车数据库)

-- 
-- 7 - Auditing data changes (table for DML trigger)
-- 


-- Delete existing table
IF OBJECT_ID('[AUDIT].[LOG_TABLE_CHANGES]') IS NOT NULL 
  DROP TABLE [AUDIT].[LOG_TABLE_CHANGES]
GO


-- Add the table
CREATE TABLE [AUDIT].[LOG_TABLE_CHANGES]
(
  [CHG_ID] [numeric](18, 0) IDENTITY(1,1) NOT NULL,
  [CHG_DATE] [datetime] NOT NULL,
  [CHG_TYPE] [varchar](20) NOT NULL,
  [CHG_BY] [nvarchar](256) NOT NULL,
  [APP_NAME] [nvarchar](128) NOT NULL,
  [HOST_NAME] [nvarchar](128) NOT NULL,
  [SCHEMA_NAME] [sysname] NOT NULL,
  [OBJECT_NAME] [sysname] NOT NULL,
  [XML_RECSET] [xml] NULL,
 CONSTRAINT [PK_LTC_CHG_ID] PRIMARY KEY CLUSTERED ([CHG_ID] ASC)
) ON [PRIMARY]
GO

-- Add defaults for key information
ALTER TABLE [AUDIT].[LOG_TABLE_CHANGES] ADD CONSTRAINT [DF_LTC_CHG_DATE] DEFAULT (getdate()) FOR [CHG_DATE];
ALTER TABLE [AUDIT].[LOG_TABLE_CHANGES] ADD CONSTRAINT [DF_LTC_CHG_TYPE] DEFAULT ('') FOR [CHG_TYPE];
ALTER TABLE [AUDIT].[LOG_TABLE_CHANGES] ADD CONSTRAINT [DF_LTC_CHG_BY] DEFAULT (coalesce(suser_sname(),'?')) FOR [CHG_BY];
ALTER TABLE [AUDIT].[LOG_TABLE_CHANGES] ADD CONSTRAINT [DF_LTC_APP_NAME] DEFAULT (coalesce(app_name(),'?')) FOR [APP_NAME];
ALTER TABLE [AUDIT].[LOG_TABLE_CHANGES] ADD CONSTRAINT [DF_LTC_HOST_NAME] DEFAULT (coalesce(host_name(),'?')) FOR [HOST_NAME];
GO



--
--  8 - Make DML trigger to capture changes
--


-- Delete existing trigger
IF OBJECT_ID('[ACTIVE].[TRG_FLUID_DATA]') IS NOT NULL 
  DROP TRIGGER [ACTIVE].[TRG_FLUID_DATA]
GO

-- Add trigger to log all changes
CREATE TRIGGER [ACTIVE].[TRG_FLUID_DATA] ON [ACTIVE].[CARS_BY_COUNTRY]
  FOR INSERT, UPDATE, DELETE AS
BEGIN

  -- Detect inserts
  IF EXISTS (select * from inserted) AND NOT EXISTS (select * from deleted)
  BEGIN
    INSERT [AUDIT].[LOG_TABLE_CHANGES] ([CHG_TYPE], [SCHEMA_NAME], [OBJECT_NAME], [XML_RECSET])
    SELECT 'INSERT', '[ACTIVE]', '[CARS_BY_COUNTRY]', (SELECT * FROM inserted as Record for xml auto, elements , root('RecordSet'), type)
    RETURN;
  END

  -- Detect deletes
  IF EXISTS (select * from deleted) AND NOT EXISTS (select * from inserted)
  BEGIN
    INSERT [AUDIT].[LOG_TABLE_CHANGES] ([CHG_TYPE], [SCHEMA_NAME], [OBJECT_NAME], [XML_RECSET])
    SELECT 'DELETE', '[ACTIVE]', '[CARS_BY_COUNTRY]', (SELECT * FROM deleted as Record for xml auto, elements , root('RecordSet'), type)
    RETURN;
  END

  -- Update inserts
  IF EXISTS (select * from inserted) AND EXISTS (select * from deleted)
  BEGIN
    INSERT [AUDIT].[LOG_TABLE_CHANGES] ([CHG_TYPE], [SCHEMA_NAME], [OBJECT_NAME], [XML_RECSET])
    SELECT 'UPDATE', '[ACTIVE]', '[CARS_BY_COUNTRY]', (SELECT * FROM deleted as Record for xml auto, elements , root('RecordSet'), type)
    RETURN;
  END

END;
GO



--
--  9 - Test DML trigger by updating, deleting and inserting data
--

-- Execute an update
UPDATE [ACTIVE].[CARS_BY_COUNTRY]
SET COUNTRY_NAME = 'Czech Republic'
WHERE COUNTRY_ID = 8
GO

-- Remove all data
DELETE FROM [ACTIVE].[CARS_BY_COUNTRY];
GO

-- Execute the load
EXECUTE [ACTIVE].[USP_LOAD_CARS_BY_COUNTRY];
GO 

-- Show the audit trail
SELECT * FROM [AUDIT].[LOG_TABLE_CHANGES]
GO

-- Disable the trigger
ALTER TABLE [ACTIVE].[CARS_BY_COUNTRY] DISABLE TRIGGER [TRG_FLUID_DATA];

** 审计表的外观 **

【讨论】:

  • 很好的阅读,感谢您的洞察力!我只是想验证我是否理解您的代码 sn-p。您只有 1 个“Log Table Changes”表,它将存储来自其他所有表的记录,而这些表中的实际记录存储在 XML 中?这样你就只有一个审计表了吗?
  • 最酷的部分是它是 2 你。假设您的公司纳税,您的数据保留期为 7 年。您可能希望在 chg_date 上使用多个审计表分区。请参阅我关于数据仓库技术的演示文稿。另一方面,如果您为企业销售冰淇淋,您的收据可能会保留 2 年。那么一张桌子就可以了。
  • 触发器的“缺点”不应该包括增加插入/更新/删除时间吗?我觉得这里正确决定的一部分是平衡写入速度与查询速度。
  • 在“记录的有效日期”下,con 是“主表的索引很大”。通过使用filtered index,可以为活动行管理索引的大小。 (这可能需要添加一列,例如IsActive,以在过滤谓词中使用。)历史查询可以使用覆盖所有行的索引。增加了索引存储,但可以保持查询性能。
【解决方案2】:

记录更改是我通常使用基表上的触发器来记录日志表中的更改。日志表有额外的列来记录数据库用户、操作和日期/时间。

create trigger Table-A_LogDelete on dbo.Table-A
  for delete
as
  declare @Now as DateTime = GetDate()
  set nocount on
  insert into Table-A-History
    select SUser_SName(), 'delete-deleted', @Now, *
      from deleted
go
exec sp_settriggerorder @triggername = 'Table-A_LogDelete', @order = 'last', @stmttype = 'delete'
go
create trigger Table-A_LogInsert on dbo.Table-A
  for insert
as
  declare @Now as DateTime = GetDate()
  set nocount on
  insert into Table-A-History
    select SUser_SName(), 'insert-inserted', @Now, *
      from inserted
go
exec sp_settriggerorder @triggername = 'Table-A_LogInsert', @order = 'last', @stmttype = 'insert'
go
create trigger Table-A_LogUpdate on dbo.Table-A
  for update
as
  declare @Now as DateTime = GetDate()
  set nocount on
  insert into Table-A-History
    select SUser_SName(), 'update-deleted', @Now, *
      from deleted
  insert into Table-A-History
    select SUser_SName(), 'update-inserted', @Now, *
      from inserted
go
exec sp_settriggerorder @triggername = 'Table-A_LogUpdate', @order = 'last', @stmttype = 'update'

日志记录触发器应始终设置为最后触发。否则,后续触发器可能会回滚原始事务,但日志表已经更新。这是一种令人困惑的状况。

【讨论】:

  • 是否可以这样做并记录来自其他地方的用户?假设某人通过了 Intranet 站点的身份验证?
  • @PatrickSchomburg 触发器只能对 SQL Server 看到的用户身份进行可靠访问。使用单个 SQL Server 帐户为所有 Web 用户访问数据库的网站存在问题。一种解决方法是使用Session_Context 存储网站用户的ID,然后在触发器中获取值进行记录。这要求每个人都遵循相同的规则:总是在做任何其他事情之前设置用户的 id,否则可能会发生坏事,例如记录了一个过时的 ID。
  • 效果很好。在多个用户共享一个登录名的系统中,记录一个用户总是有问题的。我主要在用户登录到 ActiveUsers 表并且工作正常的系统上工作。我所做的唯一更改是在 Update 触发器中记录 DELETE 操作是多余的,因为它已经从插入或先前的更改中记录下来,所以我删除了它。谢谢。
  • 有点晚了,但另一种选择是在基表中存储一个 LastModifiedByUserID 字段,然后让网站中的主要更新语句将用户 ID 存储到基表中。然后,在您的日志记录触发器中,您可以将 i.LastModifiedByUserID 添加到插入语句。
【解决方案3】:

最新版本的 SQL Server(2016+ 和 Azure)具有临时表,可提供所需的确切功能,作为一流的功能。 https://docs.microsoft.com/en-us/sql/relational-databases/tables/temporal-tables

微软的某个人可能读过这个页面。 :)

【讨论】:

  • 感谢您添加此内容。这个问题其实是很久以前的问题了。巧合的是,我正在开发一个具有类似要求的新项目,并且我实际上正在使用 Azure。我将研究临时表。干杯!
  • 新时态表的一大缺点,我不会切换到它们的原因是它们不包括触发表中更改的用户,有解决方法,但没有一个他们直截了当。
  • @bendataclear 我们通过在原始 table.FWIW 中添加 ChangeUser 列来解决这个问题。
  • @Billy 但是临时表包含已删除的行,因此这将显示以前的编辑用户和当前的编辑时间。
  • @bendataclear 确实如此,但主表中的当前记录将显示最后一个用户更改记录。为了确定做出更改的用户,您必须查看该系列中的下一条记录。
【解决方案4】:

方法 3 怎么样:使 Table-A 成为针对 Table-A-History 的视图。插入Table-A-History 并让适当的过滤逻辑生成Table-A。这样你只插入一个表。

【讨论】:

  • 我想我应该分开这些表,因为 Table-A 将保存大约 10K 经常使用的记录。历史表会变得很大并且使用更少。与表 A 相比,有 5-10% 的时间。性能方面,如果我将 Table-A 和 Table-A-History 结合起来,让数据库经常搜索 10K 记录而不是一个巨大的表不是更好吗
  • 可能,也可能不是,取决于插入与选择的比率。您还可以将 Table-A 设为索引视图 (msdn.microsoft.com/en-us/library/dd171921%28v=sql.100%29.aspx)。这可能会彻底解决您的搜索问题。
【解决方案5】:

尽管它会占用更多空间,但拥有包含最新记录的历史记录表也可以让您省去编写报告以及查看更改发生方式和时间的麻烦。我认为值得考虑的事情。

就性能而言,我希望它们是相同的。但是,您肯定不想从非历史表中删除记录(选项 1 的“移动”),因为您在两个表之间使用了参照完整性,对吧?

【讨论】:

  • 对,我会更新表 A 中的记录。Table-A-History 表使用代理键 + 链接到表 A 的外键
【解决方案6】:

我更喜欢方法1
另外,我还要维护历史表中的当前记录
这取决于需要。

【讨论】:

    【解决方案7】:

    选项 1 没问题。 但是你也有方法4:)

    1. 向表中插入新记录,

    2. 使用 mysql 调度程序将旧记录移至常规库中的存档表。您可以在负载最小的时候安排数据归档,例如在夜间。

    【讨论】:

    • 哎呀,对不起。但想法是一样的。如果您不想在白天表现不佳,那就在晚上做吧 ;-)
    • 我认为问题不仅与插入有关,还与更新和删除有关。如果插入的行在同一天更新或删除怎么办?在这种情况下,无法跟踪在同一天完成的更改。 (如果问题只涉及 INSERT,则不需要审计表,因为仅使用 INSERT 不会更改任何数据。)
    【解决方案8】:

    您可以像这样简单地创建程序或作业来解决此问题:

     create procedure [dbo].[sp_LoadNewData]
     AS
    INSERT INTO [dbo].[Table-A-History]
     (
     [1.Column Name], [2.Column Name], [3.Column Name], [4.Column Name]
     )    
     SELECT [1.Column Name], [2.Column Name], [3.Column Name], [4.Column Name]
     FROM dbo.[Table-A] S
    
     WHERE NOT EXISTS
     (
     SELECT  * FROM [dbo].[Table-A-History] D WHERE D.[1.Column Name] =S.[1.Column Name]
     )
    

    注意:[1.Column Name]是表格的通用列。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-06-05
      • 2015-11-27
      • 1970-01-01
      相关资源
      最近更新 更多