web-dev-qa-db-ja.com

ロギング用の正規化されたテーブル構造

1つの行の最新のアクティビティをログに記録するために4つの列を持つテーブルを正規化するにはどうすればよいですか。

  1. 作成日

  2. によって作成された

  3. 変更日

  4. 変更された

数十のテーブルがあり、すべてに4つの列があります。これを効率的かつ柔軟に正規化する方法またはパターンはありますか?

3
Deeptechtons

「最新のアクティビティ」をログに記録するのではなく、完全な監査証跡を保持することをお勧めします。スペース要件を最小限に抑えるために、3つのテーブルが必要になる場合があります。

_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 */
);
_

この監査テーブルでは、監査対象のすべてのテーブルにINT主キー(またはINT)。異なるデータ型や複合主キーがある場合、これは明らかに複雑になります。その場合、異なる監査テーブルを検討するだけでかまいません。1つの行の最後の変更のみを保持するよりもさらに重要です。

_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
);
_

(質問で言及されているものを除いて、すべてのクエリパターンがわからないため、クラスター化インデックスを意図的に指定しなかったことに注意してください。EventDateTime、特に最近発生したイベントを照会する場合、および/または定期的にデータをパージする場合、これは決して悪い考えではありません。これにより、少なくとも新しい行が「末尾」に追加されることが保証されます。先頭列としてTableIDのようなものを選択した場合、ページ分割の代わりにテーブル全体で発生します。)

これで、トリガーはユーザー名を確認できます(認証方法は不明ですが、これはSUSER_SNAME()を使用する場合があります)。処理するテーブルがわかっているため、アクションを決定できるため、追加する必要がありますID/inserteddeleted(s)。 FooIDという主キーを持つ_dbo.foo_という名前のテーブルを想定するHokeyの例:

_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
_

ステートメント/トリガーの呼び出しごとに、これらの挿入の1つだけが起動します(まあ、まとまって言えば、トリガー自体は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
_

しかし、テーブルの場合、これはあまり意味がありません。トリガーを作成するときに行を追加するだけで、実行時に導出する代わりにTableIDをハードコーディングすることもできます。

_dbo.AuditLog_でデータを収集すると、特定のタイプのlastアクションに関する情報を簡単に取得でき、テーブル、ユーザー、または個々のエンティティ。これらのクエリの作成に支援が必要な場合は、スキーマ、いくつかのサンプル行、および期待される/期待される結果を使用して、新しい質問を開始してください。 SQLFiddle を使用すると非常に役立ちます。

6
Aaron Bertrand