SQL Server 2016 - 临时表 - 如何识别用户
Posted
技术标签:
【中文标题】SQL Server 2016 - 临时表 - 如何识别用户【英文标题】:SQL Server 2016 - Temporal Table - how to identify the user 【发布时间】:2017-05-26 22:26:01 【问题描述】:是否可以获得有关修改历史表中数据的用户/连接的信息?我读到了可以使用时态表的审计场景,并且可以检测到谁更改了数据。但是我该怎么做呢?
【问题讨论】:
这并不是真正的审计。如果您考虑一下,当用户修改一行时,基表中的行就是用户更改的行。因此,您无法捕获导致创建历史记录行的人的用户名,他们可能正在更新我昨天更新的行,因此移动到历史记录表的行代表我昨天所做的更改。因此,您需要在基表中有一个使用触发器更新的列,但这将创建两个版本的历史记录行(一个使用以前的编辑器,一个使用新编辑器)。 不,你不能用 INSTEAD OF 触发器偷偷摸摸。我试了一下,有漏洞,但是I reported it, and they fixed it. 感谢提示。我阅读了这篇德语/英语 msdn 文章 msdn.microsoft.com/en-us/library/mt631669.aspx Data Audit “在存储关键信息的表上使用临时系统版本控制,您需要跟踪哪些更改、何时更改以及由谁更改。由谁,并在任何时间点执行数据取证。”我目前使用带有用户列和 cdc 的解决方案。我认为我可以用临时表替换这个解决方案。不过看来我得等下一次更新了。 是的,那篇文章确实提到了审计数据,但这不是我所说的审计的意思。除了一个挥手的“由谁”之外,它根本没有提到任何关于审核进行更改的用户的明确内容。 并且文档现在已经更新 - 请注意no longer makes any kind of statement about "by whom." 【参考方案1】:一个看似无懈可击的审计解决方案,它给出了进行每次更改的登录用户的名称(以及对我在此页面上的 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) 修复了查询中显示已删除记录的错误,在某些情况下它会返回错误的记录。
【讨论】:
小问题:查询在事务中多次更新同一行后显示重复项,因为它们在历史记录表中的时间相同。这可以通过执行 top 1 apply 而不是 left join 来解决(虽然更昂贵)。 我之前评论的补充,或者将AND eh.ValidFromUTC != eh.ValidFromUTC
添加到左连接以确保只返回一个值。
感谢@JussiKosunen 接受这个建议!我想你的意思是AND e.ValidFromUTC != eh.ValidFromUTC
(在一种情况下e
不是eh
)。我已经更新了答案。
这似乎也有效。我的确实有一个错字,但是我的意思是AND eh.ValidFromUTC != eh.ValidToUTC
(都来自eh
,但是From
不是To
),因为这似乎是SQL Server在您使用FOR SYSTEM_TIME ALL
防止重复时所做的事情
很好,正是我会尝试的(我的意思是,如果我们走了这么远,必须是一种没有触发器的方法)。谢谢你,我什至没有机会考虑如何解决这个问题,你从中获得了乐趣:p。【参考方案2】:
编辑:在此页面的其他地方查看我的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
字段始终包含当前登录用户的名称在每条记录中,因此在进行任何更改时保存到历史记录表中 任何记录。
当然,这在主表中不是很有用,因为它不会显示谁对每条记录进行了最后一次更改。 但在历史记录表中,它会在更改发生时被冻结,这非常有用,(尽管它记录的是在周期结束时的用户,而不是开始时)。
注意,LoggedInUser
作为计算列必须可以为空,因此历史表中的对应列也必须为空。
主(当前)表:
历史表:
当然在历史表中,它记录的记录是谁将记录从那个状态改变,而不是到那个状态,即有效期结束时的登录用户时期。它也适用于删除,但 SQL Server 时态表系统不会在历史表中插入记录以进行插入。
欢迎任何关于如何改进这一点的想法,例如如何在历史表中记录每个有效期的开始是谁进行了更改。我有一个想法,涉及主表中的另一个计算字段,它使用 UDF 来获取在历史表中进行最后一次更改的用户。
编辑:我从@Aaron Bertrand 的优秀文章here 中找到了很多灵感,这篇文章使用了触发器。
【讨论】:
感谢分享您的解决方案,这对我非常有用。【参考方案3】:在时态表的当前实现中,它仅记录基于时间的信息,而不记录有关进行更改的会话的其他任何信息。不要因为我知道这种情况在未来可能会改变的某种内幕消息而阅读该声明;我对此一无所知。如果您需要该信息,则需要将其记录在行中。执行此操作的经典方法是使用触发 DML 操作并代表用户维护该值的触发器。
【讨论】:
【参考方案4】:我正在考虑解决此问题的另一个选项是在保存或更新行时填充基本时态表中的 LastModifiedBy 字段。
这将显示谁修改了表并因此创建了历史记录。正如 Aaron 上面提到的,您可以在触发器中执行此操作,但我的想法是在插入/更新之前决定它,并在更新记录时将值放入 LastModifiedBy 字段。
这也将在每次修改记录时出现在历史记录表中。
【讨论】:
如果他们可以通过存储过程控制所有数据访问和/或可以访问发布更新的应用程序代码,这将起作用。许多人做不到。 我使用 LastModifiedBy 字段并利用新的 SESSION_CONTEXT 功能制作了一个(尚未准备好生产)解决方案。我在字段上创建了一个默认约束,其值为SESSION_CONTEXT('UserID')
,我在打开连接时设置了该约束。它可以正常工作,但如果有人明确说明该值,则可以被覆盖。以上是关于SQL Server 2016 - 临时表 - 如何识别用户的主要内容,如果未能解决你的问题,请参考以下文章