SQL Server 2016 - 时态表 - 如何识别用户

时间:2017-01-11 23:20:09

标签: sql-server sql-server-2016

是否可以获取有关修改历史表中数据的用户/连接的信息?我读到了可以使用时态表的审计场景,并且可以检测谁更改了数据。但是我该怎么做呢?

4 个答案:

答案 0 :(得分:9)

一个看似无懈可击的审核解决方案,它提供了进行每项更改的登录用户的名称(以及此页面上previous answer的重大改进):

SELECT 
  e.EmployeeID, e.FirstName, e.Score, 
  COALESCE (eh.LoggedInUser, o.CreatedBy, e.CreatedBy) AS CreatedOrModifiedBy, 
  e.ValidFromUTC, e.ValidToUTC

FROM dbo.Employees FOR SYSTEM_TIME ALL AS e
  LEFT JOIN dbo.EmployeeHistory AS eh    -- history table
    ON e.EmployeeID = eh.EmployeeID AND e.ValidFromUTC = eh.ValidToUTC
    AND e.ValidFromUTC <> eh.ValidFromUTC

OUTER APPLY
  (SELECT TOP 1 CreatedBy 
   FROM dbo.EmployeeHistory 
   WHERE EmployeeID = e.EmployeeID 
   ORDER BY ValidFromUTC ASC) AS o     -- oldest history record

--WHERE e.EmployeeID = 1
ORDER BY e.ValidFromUTC

Result set

  • 不使用触发器或用户定义的函数
  • 需要对表格进行细微更改
  • NB:请注意,SQL Server始终使用UTC而不是本地时间来处理时态表中的时间戳。
  • 编辑(2018/12/03)当同一记录同时发生多次更新时(例如在交易中),只返回最新的更改(见下文)


说明:

主表和历史表中添加了两个字段:

  • 要记录创建记录的用户的名称 - 正常的SQL默认值:
    CreatedBy NVARCHAR(128) NOT NULL DEFAULT (SUSER_SNAME())
  • 随时记录当前登录用户的名称。计算列:
    LoggedInUser AS (SUSER_SNAME())

当记录插入主表时,SQL Server不会在历史记录表中插入任何内容。但由于默认约束,字段CreatedBy记录了创建记录的人员。但是,如果/当记录更新时,SQL Server会将记录插入关联的历史记录表中。这里的关键思想是,进行更改的登录用户的名称将记录到历史记录表中,即主表中的字段LoggedInUser的内容(始终包含登录到连接的人员名称将保存到历史记录表中的字段LoggedInUser中。

这几乎是我们想要的,但并不完全 - 它背后的一个变化。例如。如果用户Dave插入了记录,但是用户Andrew进行了第一次更新,&#34; Andrew&#34;在历史记录表中记录为用户名,在Dave插入的记录的原始内容旁边。但是,所有信息都在那里 - 它只需要解开。加入系统为ROW START和ROW END生成的字段,我们得到了进行更改的用户(来自历史表中的上一个记录)。但是,历史记录表中没有记录最初插入的记录版本。在这种情况下,我们会检索CreatedBy字段。

这似乎提供了一个不漏水的审计解决方案。即使用户编辑了字段CreatedBy,编辑也会记录在历史记录表中。因此,我们从历史记录表中恢复CreatedBy的最旧值,而不是从主表中恢复当前值。

删除记录

上面的查询未显示谁从主表中删除了记录。这可以使用以下方法检索(可以简化?):

SELECT 
  d.EmployeeID, d.LoggedInUser AS DeletedBy, 
  d.CreatedBy, d.ValidFromUTC, d.ValidToUTC AS DeletedAtUTC
FROM
  (SELECT EmployeeID FROM dbo.EmployeeHistory GROUP BY EmployeeID) AS eh   -- list of IDs
OUTER APPLY
  (SELECT TOP 1 * FROM dbo.EmployeeHistory 
   WHERE EmployeeID = eh.EmployeeID 
   ORDER BY ValidToUTC DESC) AS d -- last history record, which may be for DELETE
LEFT JOIN
  dbo.Employees AS e
    ON eh.EmployeeID = e.EmployeeID
WHERE e.EmployeeID IS NULL          -- record is no longer in main table

示例表脚本

以上示例基于表脚本(历史表由SQL Server创建):

CREATE TABLE dbo.Employees(
  EmployeeID INT /*IDENTITY(1,1)*/ NOT NULL,
  FirstName NVARCHAR(40) NOT NULL,
  Score INTEGER NULL,
  LoggedInUser AS (SUSER_SNAME()),
  CreatedBy NVARCHAR(128) NOT NULL DEFAULT (SUSER_SNAME()),
  ValidFromUTC DATETIME2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL DEFAULT SYSUTCDATETIME(),
  ValidToUTC DATETIME2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL DEFAULT CAST('9999-12-31 23:59:59.9999999' AS DATETIME2),
  CONSTRAINT PK_Employees PRIMARY KEY CLUSTERED (EmployeeID ASC),
  PERIOD FOR SYSTEM_TIME (ValidFromUTC, ValidToUTC)
) 
WITH (SYSTEM_VERSIONING = ON ( HISTORY_TABLE = dbo.EmployeeHistory ))

编辑(2018/11/19)为system_time字段添加了默认约束,有些人认为这是最佳做法,如果您要将系统版本添加到现有的表格。

编辑(2018/12/03)根据@ JussiKosunen的评论更新(感谢Jussi!)。请注意,当多个更改具有相同的时间戳时,查询将仅返回当时的最后一次更改。以前,它为每个更改返回一行,但每个更改都包含最后一个值。查看一种方法,使其返回所有更改,即使它们具有相同的时间戳。 (请注意,这是一个真实世界的时间戳,而不是&#34; Microsoft timestamp&#34;这是deprecated以避免破坏物理世界。)

编辑(2019/03/22)修正了查询中显示已删除记录的错误,在某些情况下会返回错误的记录。

答案 1 :(得分:5)

编辑:请参阅本页其他地方的much better answer

我的解决方案不需要触发器。我在主表中有一个计算列,它始终包含登录用户,例如

CREATE TABLE dbo.Employees(
    EmployeeID INT NOT NULL,
    FirstName sysname NOT NULL,
    ValidFrom DATETIME2(7) GENERATED ALWAYS AS ROW START NOT NULL,
    ValidTo DATETIME2(7) GENERATED ALWAYS AS ROW END NOT NULL,
    LoggedInUser AS (SUSER_SNAME()),  --<<-- computed column

......等等。

字段LoggedInUser始终包含当前登录用户在每条记录中的名称,因此会在更改时保存到历史记录表中< / strong>任何记录。

当然,这在主表中并不是很有用,因为它没有显示谁为每条记录做了最后一次更改。 但是在历史表中,它会在更改时被冻结,这非常有用,(虽然它会在期末记录用户,而不是开头)。

请注意,作为计算列,LoggedInUser必须是可空的,因此历史表中的相应列也必须是。

主要(当前)表: Main table

历史记录表: History table

当然,在历史记录表中,它记录了谁将记录更改为该状态,而不是更改为该状态,即在有效期结束时登录的用户期。它也适用于删除,但SQL Server时态表系统不会在历史记录表中插入记录以进行插入。

任何关于如何改进这一点的想法都是受欢迎的,例如:如何记录在历史记录表中每个有效期的开始进行更改的人员。我有一个想法涉及主表中的另一个计算字段,它使用UDF来获取在历史表中进行最后一次更改的用户。

编辑:我从@Aaron Bertrand的优秀文章here中找到了许多灵感,它使用了一个触发器。

答案 2 :(得分:2)

在时态表的当前实现中,它仅记录基于时间的信息,而不记录进行更改的会话。并且不要读这个陈述,因为我有一些内部知识,知道将来这种情况可能会改变;我对此一无所知。如果您需要该信息,则需要将其记录在一行。这样做的一种经典方法是使用触发器来触发DML操作并代表用户维护该值。

答案 3 :(得分:0)

我正在考虑解决此问题的另一个选项是在基本时态表中使用LastModifiedBy字段,该字段在保存或更新行时填充。

这将显示谁修改了表格,从而创建了历史记录。正如Aaron上面提到的,你可以在一个触发器中完成它,但我的想法是在它进入插入/更新之前决定它并在记录更新时将值放在LastModifiedBy字段中。

每次修改记录时,这也会出现在历史记录表中。