一个看似无懈可击的审计解决方案,它给出了进行每次更改的登录用户的名称(以及对我在此页面上的 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
- 不使用触发器或用户定义的函数
- 需要对表格进行少量更改
-
注意:请注意,对于时态表中的时间戳,SQL Server 始终使用UTC,而不是本地时间。
-
编辑: (2018/12/03) (感谢@JussiKosunen!)当同一记录同时发生多个更新时(例如在事务中),仅返回最新的更改(见下文)
说明:
两个字段被添加到主表和历史表中:
- 要记录创建记录的用户的名称 - 一个普通的 SQL 默认值:
CreatedBy NVARCHAR(128) NOT NULL DEFAULT (SUSER_SNAME())
- 随时记录当前登录用户的姓名。计算列:
LoggedInUser AS (SUSER_SNAME())
将记录插入主表时,SQL Server 不会向历史表中插入任何内容。但是由于默认约束,字段CreatedBy 记录了谁创建了记录。但是,如果/当记录更新时,SQL Server 会将记录插入关联的历史记录表中。这里的关键思想是将进行更改的登录用户的名称记录到历史表中,即主表中字段LoggedInUser的内容(始终包含名称)登录到连接的人)保存到历史记录表中的字段LoggedInUser。
这几乎是我们想要的,但不完全是 - 这是一个落后的变化。例如。如果用户 Dave 插入了记录,但用户 Andrew 进行了第一次更新,则“Andrew”被记录为历史表中的用户名,紧挨着 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!)。请注意,当多个更改具有相同的时间戳时,查询仅返回当时的最后一次更改。以前它为每次更改返回一行,但每个更改都包含最后一个值。寻找一种让它返回所有更改的方法,即使它们具有相同的时间戳。 (请注意,这是一个真实世界的时间戳,而不是 deprecated 的“Microsoft timestamp”,以避免破坏物理世界。)
编辑: (2019/03/22) 修复了查询中显示已删除记录的错误,在某些情况下它会返回错误的记录。