用于日志记录的标准化表结构

Dee*_*ons 3 normalization database-design sql-server log

如何规范化具有四列的表以记录单行的最新活动:

  1. 创建于

  2. 由...制作

  3. 修改时间

  4. 修改者

我们有几十个表,都有这 4 列。有没有办法或模式以有效和灵活的方式对此进行规范化?

Aar*_*and 6

我建议不要记录“最新活动”,而是保留完整的审计跟踪。为了最小化空间需求,您可能需要三个表:

CREATE TABLE dbo.Users
(
  UserID TINYINT IDENTITY(1,1) PRIMARY KEY, -- assuming <= 255 users
  Username NVARCHAR(128) NOT NULL UNIQUE,
  /* , other columns */
);

CREATE TABLE dbo.Tables
(
  TableID TINYINT IDENTITY(1,1) PRIMARY KEY, -- assuming <= 255 tables
  Name NVARCHAR(128) NOT NULL UNIQUE
  /* , other columns */
);
Run Code Online (Sandbox Code Playgroud)

该审计表假设所有的审计表中有一个INT主键(或将一个PK适合进入INT)。如果您有不同的数据类型或复合主键,这显然会更加复杂,在这种情况下,您可能只考虑不同的审计表 - 恕我直言,这比仅保留任何一行的最后修改更有价值。

CREATE TABLE dbo.AuditLog
(
  TableID       TINYINT NOT NULL 
                FOREIGN KEY REFERENCES dbo.Tables(TableID),
  ID            INT, -- loose reference to entity table's PK
  Action        CHAR(1) CHECK (Action IN ('I', 'U', 'D')),
  UserID        TINYINT NULL -- just in case
                FOREIGN KEY REFERENCES dbo.Users(UserID),
  EventDateTime DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
Run Code Online (Sandbox Code Playgroud)

(请注意,我故意没有规定聚集索引应该是什么,因为除了问题中提到的查询模式之外,我不知道您的所有查询模式。很可能您希望在 上进行聚类EventDateTime,特别是如果您将查询最近发生的事件,和/或您将定期清除数据,这绝不是一个坏主意。这至少将确保将新行添加到表的“末尾”而不是页面如果您选择诸如TableID作为前导列之类的内容,则会在整个地方发生分裂。)

现在您的触发器可以检查用户名(不确定您的身份验证方法,但这可能是通过SUSER_SNAME()),它知道它正在处理哪个表,它可以确定操作,因此它只需添加ID来自inserted/的(s) deleted。Hokey 示例,假设一个表命名dbo.foo为一个名为的主键FooID

INSERT dbo.tables(Name) SELECT N'dbo.foo';
GO

CREATE TRIGGER dbo.AuditFoo
ON dbo.foo
FOR INSERT, UPDATE, DELETE
AS
BEGIN
  SET NOCOUNT ON;

  DECLARE @UserID TINYINT, @TableID TINYINT, @now DATETIME;

  SELECT @UserID = UserID -- could be NULL
    FROM dbo.Users WHERE Username = SUSER_SNAME();

  SELECT @TableID = TableID 
    FROM dbo.Tables WHERE Name = N'dbo.foo';

  -- inserts
  INSERT dbo.AuditLog(TableID, ID, Action, UserID)
  SELECT @TableID, FooID, 'I', @UserID
    FROM inserted AS i WHERE NOT EXISTS
    (SELECT 1 FROM deleted AS d WHERE d.FooID = i.FooID);

  -- updates
  INSERT dbo.AuditLog(TableID, ID, Action, UserID)
  SELECT @TableID, FooID, 'U', @UserID
    FROM inserted AS i WHERE EXISTS
    (SELECT 1 FROM deleted AS d WHERE d.FooID = i.FooID);

  -- deletes
  INSERT dbo.AuditLog(TableID, ID, Action, UserID)
  SELECT @TableID, FooID, 'D', @UserID
    FROM deleted AS d WHERE NOT EXISTS
    (SELECT 1 FROM inserted AS i WHERE i.FooID = d.FooID);
END
GO
Run Code Online (Sandbox Code Playgroud)

每个语句/触发器调用中只会触发这些插入中的一个(好吧,为了学究,触发器本身将在 的情况下触发多次MERGE)。

如果您不想保留用户/表的列表,您可以随时动态填充它(尽管如果您没有严格控制列表,您可能会认真地重新考虑TINYINT上面的建议)。例如,要捕获您第一次看到且尚未手动盘点的用户:

  SELECT @UserID = UserID FROM dbo.Users WHERE Username = SUSER_SNAME();

  IF @UserID IS NULL
  BEGIN
    INSERT dbo.Users(Username) SELECT SUSER_SNAME();
    SELECT @UserID = SCOPE_IDENTITY();
  END
Run Code Online (Sandbox Code Playgroud)

但是对于桌子来说,这没有多大意义。只需在创建触发器时添加行,您甚至可以硬编码TableID而不是在运行时派生它。

在 中收集数据后dbo.AuditLog,您应该能够轻松获取有关任何特定类型的最后一个操作的信息,仅限于表、用户甚至单个实体。如果您在构建任何这些查询时需要帮助,请使用架构、一些示例行和所需/预期结果开始一个新问题。使用SQLFiddle会很有帮助。