アプリのSQLServerバックエンドで、一連のキーテーブルの履歴テーブルを作成します。これにより、行への変更の履歴が追跡されます。
私のアプリケーション全体はストアドプロシージャを使用しており、埋め込みSQLはありません。これらのテーブルを変更するためのデータベースへの唯一の接続は、アプリケーションとSPインターフェイスを介して行われます。従来、私が働いていたショップは、トリガーを使用してこのタスクを実行していました。
ストアドプロシージャとトリガーのどちらかを選択できる場合、どちらが良いですか?どちらが速いですか?
トリガー。
監査ログトリガーを簡単に作成/管理できるように、GUI(内部的にはRed Matrix Reloadedと呼ばれます)を作成しました。
使用されているもののいくつかのDDLは次のとおりです。
CREATE TABLE [AuditLog] (
[AuditLogID] [int] IDENTITY (1, 1) NOT NULL ,
[ChangeDate] [datetime] NOT NULL CONSTRAINT [DF_AuditLog_ChangeDate] DEFAULT (getdate()),
[RowGUID] [uniqueidentifier] NOT NULL ,
[ChangeType] [varchar] (50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL ,
[TableName] [varchar] (128) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL ,
[FieldName] [varchar] (128) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL ,
[OldValue] [varchar] (8000) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ,
[NewValue] [varchar] (8000) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ,
[Username] [varchar] (128) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL ,
[Hostname] [varchar] (50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL ,
[AppName] [varchar] (128) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ,
[UserGUID] [uniqueidentifier] NULL ,
[TagGUID] [uniqueidentifier] NULL ,
[Tag] [varchar] (8000) COLLATE SQL_Latin1_General_CP1_CI_AS NULL
)
CREATE TRIGGER LogInsert_Nodes ON dbo.Nodes
FOR INSERT
AS
/* Load the saved context info UserGUID */
DECLARE @SavedUserGUID uniqueidentifier
SELECT @SavedUserGUID = CAST(context_info as uniqueidentifier)
FROM master.dbo.sysprocesses
WHERE spid = @@SPID
DECLARE @NullGUID uniqueidentifier
SELECT @NullGUID = '{00000000-0000-0000-0000-000000000000}'
IF @SavedUserGUID = @NullGUID
BEGIN
SET @SavedUserGUID = NULL
END
/*We dont' log individual field changes Old/New because the row is new.
So we only have one record - INSERTED*/
INSERT INTO AuditLog(
ChangeDate, RowGUID, ChangeType,
Username, HostName, AppName,
UserGUID,
TableName, FieldName,
TagGUID, Tag,
OldValue, NewValue)
SELECT
getdate(), --ChangeDate
i.NodeGUID, --RowGUID
'INSERTED', --ChangeType
USER_NAME(), Host_NAME(), APP_NAME(),
@SavedUserGUID, --UserGUID
'Nodes', --TableName
'', --FieldName
i.ParentNodeGUID, --TagGUID
i.Caption, --Tag
null, --OldValue
null --NewValue
FROM Inserted i
CREATE TRIGGER LogUpdate_Nodes ON dbo.Nodes
FOR UPDATE AS
/* Load the saved context info UserGUID */
DECLARE @SavedUserGUID uniqueidentifier
SELECT @SavedUserGUID = CAST(context_info as uniqueidentifier)
FROM master.dbo.sysprocesses
WHERE spid = @@SPID
DECLARE @NullGUID uniqueidentifier
SELECT @NullGUID = '{00000000-0000-0000-0000-000000000000}'
IF @SavedUserGUID = @NullGUID
BEGIN
SET @SavedUserGUID = NULL
END
/* ParentNodeGUID uniqueidentifier */
IF UPDATE (ParentNodeGUID)
BEGIN
INSERT INTO AuditLog(
ChangeDate, RowGUID, ChangeType,
Username, HostName, AppName,
UserGUID,
TableName, FieldName,
TagGUID, Tag,
OldValue, NewValue)
SELECT
getdate(), --ChangeDate
i.NodeGUID, --RowGUID
'UPDATED', --ChangeType
USER_NAME(), Host_NAME(), APP_NAME(),
@SavedUserGUID, --UserGUID
'Nodes', --TableName
'ParentNodeGUID', --FieldName
i.ParentNodeGUID, --TagGUID
i.Caption, --Tag
d.ParentNodeGUID, --OldValue
i.ParentNodeGUID --NewValue
FROM Inserted i
INNER JOIN Deleted d
ON i.NodeGUID = d.NodeGUID
WHERE (d.ParentNodeGUID IS NULL AND i.ParentNodeGUID IS NOT NULL)
OR (d.ParentNodeGUID IS NOT NULL AND i.ParentNodeGUID IS NULL)
OR (d.ParentNodeGUID <> i.ParentNodeGUID)
END
/* Caption varchar(255) */
IF UPDATE (Caption)
BEGIN
INSERT INTO AuditLog(
ChangeDate, RowGUID, ChangeType,
Username, HostName, AppName,
UserGUID,
TableName, FieldName,
TagGUID, Tag,
OldValue, NewValue)
SELECT
getdate(), --ChangeDate
i.NodeGUID, --RowGUID
'UPDATED', --ChangeType
USER_NAME(), Host_NAME(), APP_NAME(),
@SavedUserGUID, --UserGUID
'Nodes', --TableName
'Caption', --FieldName
i.ParentNodeGUID, --TagGUID
i.Caption, --Tag
d.Caption, --OldValue
i.Caption --NewValue
FROM Inserted i
INNER JOIN Deleted d
ON i.NodeGUID = d.NodeGUID
WHERE (d.Caption IS NULL AND i.Caption IS NOT NULL)
OR (d.Caption IS NOT NULL AND i.Caption IS NULL)
OR (d.Caption <> i.Caption)
END
...
/* ImageGUID uniqueidentifier */
IF UPDATE (ImageGUID)
BEGIN
INSERT INTO AuditLog(
ChangeDate, RowGUID, ChangeType,
Username, HostName, AppName,
UserGUID,
TableName, FieldName,
TagGUID, Tag,
OldValue, NewValue)
SELECT
getdate(), --ChangeDate
i.NodeGUID, --RowGUID
'UPDATED', --ChangeType
USER_NAME(), Host_NAME(), APP_NAME(),
@SavedUserGUID, --UserGUID
'Nodes', --TableName
'ImageGUID', --FieldName
i.ParentNodeGUID, --TagGUID
i.Caption, --Tag
(SELECT Caption FROM Nodes WHERE NodeGUID = d.ImageGUID), --OldValue
(SELECT Caption FROM Nodes WHERE NodeGUID = i.ImageGUID) --New Value
FROM Inserted i
INNER JOIN Deleted d
ON i.NodeGUID = d.NodeGUID
WHERE (d.ImageGUID IS NULL AND i.ImageGUID IS NOT NULL)
OR (d.ImageGUID IS NOT NULL AND i.ImageGUID IS NULL)
OR (d.ImageGUID <> i.ImageGUID)
END
CREATE TRIGGER LogDelete_Nodes ON dbo.Nodes
FOR DELETE
AS
/* Load the saved context info UserGUID */
DECLARE @SavedUserGUID uniqueidentifier
SELECT @SavedUserGUID = CAST(context_info as uniqueidentifier)
FROM master.dbo.sysprocesses
WHERE spid = @@SPID
DECLARE @NullGUID uniqueidentifier
SELECT @NullGUID = '{00000000-0000-0000-0000-000000000000}'
IF @SavedUserGUID = @NullGUID
BEGIN
SET @SavedUserGUID = NULL
END
/*We dont' log individual field changes Old/New because the row is new.
So we only have one record - DELETED*/
INSERT INTO AuditLog(
ChangeDate, RowGUID, ChangeType,
Username, HostName, AppName,
UserGUID,
TableName, FieldName,
TagGUID, Tag,
OldValue,NewValue)
SELECT
getdate(), --ChangeDate
d.NodeGUID, --RowGUID
'DELETED', --ChangeType
USER_NAME(), Host_NAME(), APP_NAME(),
@SavedUserGUID, --UserGUID
'Nodes', --TableName
'', --FieldName
d.ParentNodeGUID, --TagGUID
d.Caption, --Tag
null, --OldValue
null --NewValue
FROM Deleted d
また、ソフトウェアのどのユーザーが更新を行ったかを知るために、すべての接続は、ストアドプロシージャを呼び出すことにより、「SQLServerにログオン」します。
CREATE PROCEDURE dbo.SaveContextUserGUID @UserGUID uniqueidentifier AS
/* Saves the given UserGUID as the session's "Context Information" */
IF @UserGUID IS NULL
BEGIN
PRINT 'Emptying CONTEXT_INFO because of null @UserGUID'
DECLARE @BinVar varbinary(128)
SET @BinVar = CAST( REPLICATE( 0x00, 128 ) AS varbinary(128) )
SET CONTEXT_INFO @BinVar
RETURN 0
END
DECLARE @UserGUIDBinary binary(16) --a guid is 16 bytes
SELECT @UserGUIDBinary = CAST(@UserGUID as binary(16))
SET CONTEXT_INFO @UserGUIDBinary
/* To load the guid back
DECLARE @SavedUserGUID uniqueidentifier
SELECT @SavedUserGUID = CAST(context_info as uniqueidentifier)
FROM master.dbo.sysprocesses
WHERE spid = @@SPID
select @SavedUserGUID AS UserGUID
*/
メモ
「OldValue」と「NewValue」の値は、意味のある文字列を取得するために、副選択として記述される場合があります。つまり」
OldValue:{233d-ad34234 ..} NewValue:{883-sdf34 ...}
監査証跡では、以下よりも有用性が低くなります。
OldValue: Daimler Chrysler
NewValue: Cerberus Capital Management
最後の注意:私たちがしていることをしないでください。これは私たちにとって素晴らしいことですが、他の誰もが自由に使用できません。
sQL Server 2008では、CDC(Change data Capture)と呼ばれる新機能 MSDNのCDC が役立ちます。 CDCは、トリガーやその他のメカニズムを書き込まずに、テーブルデータへの変更を別のテーブルに記録する機能です。変更データキャプチャは、SQLサーバーのテーブルへの挿入、更新、削除などの変更を記録するため、変更の詳細をリレーショナルで利用できるようにします。フォーマット。
トリガーの生成に使用したサードパーティツール ApexSQL Audit があります。
トリガーがバックグラウンドでどのように表示され、データがどのように保存されるかを次に示します。うまくいけば、これがプロセスをリバースエンジニアリングするのに十分役立つと思うでしょう。これは、各列を個別に監査できるため、IanBoydが例で示したものとは少し異なります。
表1-トランザクションの詳細(誰が、いつ、アプリケーション、ホスト名など)を保持します
CREATE TABLE [dbo].[AUDIT_LOG_TRANSACTIONS](
[AUDIT_LOG_TRANSACTION_ID] [int] IDENTITY(1,1) NOT NULL,
[DATABASE] [nvarchar](128) NOT NULL,
[TABLE_NAME] [nvarchar](261) NOT NULL,
[TABLE_SCHEMA] [nvarchar](261) NOT NULL,
[AUDIT_ACTION_ID] [tinyint] NOT NULL,
[Host_NAME] [varchar](128) NOT NULL,
[APP_NAME] [varchar](128) NOT NULL,
[MODIFIED_BY] [varchar](128) NOT NULL,
[MODIFIED_DATE] [datetime] NOT NULL,
[AFFECTED_ROWS] [int] NOT NULL,
[SYSOBJ_ID] AS (object_id([TABLE_NAME])),
PRIMARY KEY CLUSTERED
(
[AUDIT_LOG_TRANSACTION_ID] ASC
)
)
表2 –前後の値を保持します。
CREATE TABLE [dbo].[AUDIT_LOG_DATA](
[AUDIT_LOG_DATA_ID] [int] IDENTITY(1,1) NOT NULL,
[AUDIT_LOG_TRANSACTION_ID] [int] NOT NULL,
[PRIMARY_KEY_DATA] [nvarchar](1500) NOT NULL,
[COL_NAME] [nvarchar](128) NOT NULL,
[OLD_VALUE_LONG] [ntext] NULL,
[NEW_VALUE_LONG] [ntext] NULL,
[NEW_VALUE_BLOB] [image] NULL,
[NEW_VALUE] AS (isnull(CONVERT([varchar](8000), [NEW_VALUE_LONG],0),CONVERT([varchar](8000),CONVERT([varbinary](8000),substring([NEW_VALUE_BLOB],(1),(8000)),0),0))),
[OLD_VALUE] AS (CONVERT([varchar](8000),[OLD_VALUE_LONG],0)),
[PRIMARY_KEY] AS ([PRIMARY_KEY_DATA]),
[DATA_TYPE] [char](1) NOT NULL,
[KEY1] [nvarchar](500) NULL,
[KEY2] [nvarchar](500) NULL,
[KEY3] [nvarchar](500) NULL,
[KEY4] [nvarchar](500) NULL,
PRIMARY KEY CLUSTERED
(
[AUDIT_LOG_DATA_ID] ASC
)
)
トリガーを挿入
更新のトリガーは非常に長く、これと同じロジックであるため、表示していません。
CREATE TRIGGER [dbo].[tr_i_AUDIT_Audited_Table]
ON [dbo].[Audited_Table]
FOR INSERT
NOT FOR REPLICATION
As
BEGIN
DECLARE
@IDENTITY_SAVE varchar(50),
@AUDIT_LOG_TRANSACTION_ID Int,
@PRIM_KEY nvarchar(4000),
@ROWS_COUNT int
SET NOCOUNT ON
Select @ROWS_COUNT=count(*) from inserted
Set @IDENTITY_SAVE = CAST(IsNull(@@IDENTITY,1) AS varchar(50))
INSERT
INTO dbo.AUDIT_LOG_TRANSACTIONS
(
TABLE_NAME,
TABLE_SCHEMA,
AUDIT_ACTION_ID,
Host_NAME,
APP_NAME,
MODIFIED_BY,
MODIFIED_DATE,
AFFECTED_ROWS,
[DATABASE]
)
values(
'Audited_Table',
'dbo',
2, -- ACTION ID For INSERT
CASE
WHEN LEN(Host_NAME()) < 1 THEN ' '
ELSE Host_NAME()
END,
CASE
WHEN LEN(APP_NAME()) < 1 THEN ' '
ELSE APP_NAME()
END,
SUSER_SNAME(),
GETDATE(),
@ROWS_COUNT,
'Database_Name'
)
Set @AUDIT_LOG_TRANSACTION_ID = SCOPE_IDENTITY()
--This INSERT INTO code is repeated for each columns that is audited.
--Below are examples for only two columns
INSERT INTO dbo.AUDIT_LOG_DATA
(
AUDIT_LOG_TRANSACTION_ID,
PRIMARY_KEY_DATA,
COL_NAME,
NEW_VALUE_LONG,
DATA_TYPE
, KEY1
)
SELECT
@AUDIT_LOG_TRANSACTION_ID,
convert(nvarchar(1500), IsNull('[PK_Column]='+CONVERT(nvarchar(4000), NEW.[PK_Column], 0), '[PK_Column] Is Null')),
'Column1',
CONVERT(nvarchar(4000), NEW.[Column1], 0),
'A'
, CONVERT(nvarchar(500), CONVERT(nvarchar(4000), NEW.[PK_Column], 0))
FROM inserted NEW
WHERE NEW.[Column1] Is Not Null
--value is inserted for each column that is selected for auditin
INSERT INTO dbo.AUDIT_LOG_DATA
(
AUDIT_LOG_TRANSACTION_ID,
PRIMARY_KEY_DATA,
COL_NAME,
NEW_VALUE_LONG,
DATA_TYPE
, KEY1
)
SELECT
@AUDIT_LOG_TRANSACTION_ID,
convert(nvarchar(1500), IsNull('[PK_Column]='+CONVERT(nvarchar(4000), NEW.[PK_Column], 0), '[PK_Column] Is Null')),
'Column2',
CONVERT(nvarchar(4000), NEW.[Column2], 0),
'A'
, CONVERT(nvarchar(500), CONVERT(nvarchar(4000), NEW.[PK_Column], 0))
FROM inserted NEW
WHERE NEW.[Column2] Is Not Null
End
免責事項:私はApexとは一切関係がありませんが、現在の仕事ではApexのツールを使用しています。
他のみんなが言ったように、トリガー。ユニットテストが簡単で、ランダムなクエリを実行するテーブルに直接予期せずアクセスするパワーユーザーに対してはるかに回復力があります。
速いのは?データベース内で何が高速であるかを判断することは、変数の数が多い場合の難しい問題です。 「両方の方法で試して比較する」以外では、どちらの方法が速いかについての有用な答えは得られません。変数には、関連するテーブルのサイズ、通常の更新パターン、サーバー内のディスクの速度、メモリの量、キャッシュ専用のメモリの量などが含まれます。このリストは無限であり、各変数はトリガーするかどうかに影響します。 SP内のカスタムSQLよりも高速です。
良い。速い。安いです。 2つ選んでください。トリガーは整合性の点では優れており、メンテナンスの点ではおそらく安価です。おそらく、それらは、一度機能すると、それで終わりになるという点でも高速です。 SPはメンテナンスの問題であり、メンテナンスへのプッシュは高速ですが、決して良いことでも安いことでもありません。
幸運を。
推奨されるアプローチは、要件によって異なります。 監査証跡の履歴テーブルがある場合は、各操作をキャプチャする必要があります。履歴テーブルがパフォーマンスの理由のみである場合は、スケジュールされたSQLエージェントデータ転送ジョブで十分です。
各操作をキャプチャするには、AFTERTRIGGERまたはChangeDataCaptureのいずれかを使用します。
トリガーの後、トリガー内で操作する2つの一時テーブルが提供されます。
これらの一時テーブルから履歴テーブルへの挿入を実行でき、履歴テーブルは常に最新の状態になります。履歴テーブルにバージョン番号、タイムスタンプ、またはその両方を追加して、単一のソース行への変更を分離することをお勧めします。
Change Data Capture(CDC)は、データウェアハウス(または履歴テーブル)にデータをロードするためのソースとして使用できるデルタテーブルを作成するために設計されています。トリガーとは異なり、CDCは非同期であり、宛先(sprocs、SSIS)にデータを入力するために任意の方法とスケジューリングを使用できます。
CDCを使用すると、元のデータと変更の両方にアクセスできます。 変更追跡 (CT)は変更された行のみを検出します。 CDCでは完全な監査証跡を作成できますが、CTでは作成できません。 CDCおよびCT は、どちらもMSSQL 2008EnterpriseおよびDeveloperEditionでのみ使用できます。
非常に注意すべき問題の1つは、このテーブルの使用目的を特定し、その目的のために適切に構築されていることを確認することです。
具体的には、利害関係者の運用監査証跡の場合、テーブルのレコード変更の前後のスナップショットとはまったく異なります。 (実際、デバッグ以外のレコード変更の適切な使用法を想像するのは困難です。)
監査証跡には通常、少なくともユーザーID、タイムスタンプ、操作コード、そしておそらく操作に関する詳細が必要です。例-発注書のラインアイテムの注文数量を変更します。
そして、このタイプの監査証跡では、トリガーを使用したいしないでください。これらのイベントの生成を埋め込むBRレイヤーが高いほど、優れています。
OTOH、レコードレベルの変更の場合、トリガーは適切に一致します。ただし、dbmsジャーナリングファイルからこれを取得する方が簡単な場合もよくあります。
これにはトリガーを使用します。これは、ソースに関係なく、変更が履歴テーブルに反映されることを意味します。これはセキュリティに役立ち、履歴テーブルを更新するためのコードを追加するのを忘れる人などの障害モードに対して回復力があります。
実行時間はI/Oによって支配されるため、このタイプの操作でも特定の速度差は発生しない可能性があります。
トリガーは、特定のストアドプロシージャを通じて呼び出される更新、挿入、および削除だけでなく、すべての更新、挿入、および削除をキャプチャできるため、監査テーブルにトリガーを使用することを好みます。
CREATE TRIGGER [dbo].[tr_Employee_rev]
ON [dbo].[Employee]
AFTER UPDATE, INSERT, DELETE
AS
BEGIN
IF EXISTS(SELECT * FROM INSERTED) AND EXISTS (SELECT * FROM DELETED)
BEGIN
INSERT INTO [EmployeeRev](EmployeeID,Firstname,Initial,Surname,Birthdate,operation, updated, updatedby) SELECT inserted.ID, inserted.Firstname,inserted.Initial,inserted.Surname,inserted.Birthdate,'u', GetDate(), SYSTEM_USER FROM INSERTED
END
IF EXISTS (SELECT * FROM INSERTED) AND NOT EXISTS(SELECT * FROM DELETED)
BEGIN
INSERT INTO [EmployeeRev](EmployeeID,Firstname,Initial,Surname,Birthdate,operation, updated, updatedby) SELECT inserted.ID, inserted.Firstname,inserted.Initial,inserted.Surname,inserted.Birthdate,'i', GetDate(), SYSTEM_USER FROM INSERTED
END
IF EXISTS(SELECT * FROM DELETED) AND NOT EXISTS(SELECT * FROM INSERTED)
BEGIN
INSERT INTO [EmployeeRev](EmployeeID,Firstname,Initial,Surname,Birthdate,operation, updated, updatedby) SELECT deleted.ID, deleted.Firstname,deleted.Initial,deleted.Surname,deleted.Birthdate,'d', GetDate(), SYSTEM_USER FROM DELETED
END
END
SQLServerを使用して、手動でコーディングする代わりに、リビジョンテーブルのSQLを生成します。このコードは https://github.com/newdigate/sqlserver-revision-tables で入手できます。
トリガー。今のところ、データを更新する唯一の方法はSPを介することであると言えるかもしれませんが、状況が変わる可能性があるか、SPの使用が面倒になる大量の挿入/更新を行う必要がある場合があります。トリガーを使用します。
トリガー。これが私のアプローチです:
この方法で監査すると、ソーステーブルの現在のステータスと監査テーブルのすべての履歴が保持され、キー列で簡単に識別できます。
これは、アプリケーションの性質とテーブル構造、インデックスの数、データサイズなど、外部キーなどによって異なります。これらが比較的単純なテーブル(日時/整数列のインデックスのようなインデックスがないか少ない)で、データが限られている場合セット(<100万行)の場合、トリガーを使用しても問題ない可能性があります。
トリガーがロックの問題の原因になる可能性があることに注意してください。履歴テーブルを一種の監査証跡として使用している場合は、後で参照できるようにインデックスを作成すると思います。インデックスが原因で挿入/更新/削除が遅い履歴テーブルがトリガーによって更新された場合、プロシージャ呼び出しはトリガーが終了するまでブロックされます。また、トリガーで更新される外部キー制約がある場合、これもパフォーマンスを妨げる可能性があります。
この場合、それはすべてテーブルインデックスに依存します。 1日あたり10万件を超える金融取引を処理する24時間年中無休のアプリにSQLServer2000を使用しています。最大/メインテーブルには1億を超える行と15のインデックスがあります(稼働時間が必要な場合、大量削除は合理的に不可能です)。すべてのSQLはストアドプロシージャで実行されますが、パフォーマンスが低下するため、トリガーや外部キーは使用しません。