20

私は知っていますがCOLUMNS_UPDATED、簡単なショートカットが必要です (誰かが作成した場合、私はすでに作成していますが、誰かが時間を節約できる場合は、それを高く評価します)

基本的に、更新された列の値のみの XML が必要です。これはレプリケーションの目的で必要です。

SELECT * FROM insert で各列が得られますが、必要なのは更新された列だけです。

次のようなもの...

CREATE TRIGGER DBCustomers_Insert
    ON DBCustomers
    AFTER UPDATE
AS
BEGIN
    DECLARE @sql as NVARCHAR(1024);
    SET @sql = 'SELECT ';


    I NEED HELP FOR FOLLOWING LINE ...., I can manually write every column, but I need 
    an automated routin which can work regardless of column specification
    for each column, if its modified append $sql = ',' + columnname...

    SET @sql = $sql + ' FROM inserted FOR XML RAW';

    DECLARE @x as XML;
    SET @x = CAST(EXEC(@sql) AS XML);


    .. use @x

END
4

8 に答える 8

24

COLUMNS_UPDATED をまったく使用せず、実行時に動的 SQL を構築することに依存しない別の完全に異なるソリューションがあります。(設計時に動的 SQL を使用したい場合もありますが、それは別の話です。)

基本的に、挿入されたテーブルと削除されたテーブルから始めて、それぞれのテーブルをアンピボットして、それぞれに一意のキー、フィールド値、およびフィールド名の列が残るようにします。次に、2 つを結合して、変更されたものをフィルター処理します。

以下は、ログに記録される内容を表示するためのテスト呼び出しを含む、完全に機能する例です。

-- -------------------- Setup tables and some initial data --------------------
CREATE TABLE dbo.Sample_Table (ContactID int, Forename varchar(100), Surname varchar(100), Extn varchar(16), Email varchar(100), Age int );
INSERT INTO Sample_Table VALUES (1,'Bob','Smith','2295','bs@example.com',24);
INSERT INTO Sample_Table VALUES (2,'Alice','Brown','2255','ab@example.com',32);
INSERT INTO Sample_Table VALUES (3,'Reg','Jones','2280','rj@example.com',19);
INSERT INTO Sample_Table VALUES (4,'Mary','Doe','2216','md@example.com',28);
INSERT INTO Sample_Table VALUES (5,'Peter','Nash','2214','pn@example.com',25);

CREATE TABLE dbo.Sample_Table_Changes (ContactID int, FieldName sysname, FieldValueWas sql_variant, FieldValueIs sql_variant, modified datetime default (GETDATE()));

GO

-- -------------------- Create trigger --------------------
CREATE TRIGGER TriggerName ON dbo.Sample_Table FOR DELETE, INSERT, UPDATE AS
BEGIN
    SET NOCOUNT ON;
    --Unpivot deleted
    WITH deleted_unpvt AS (
        SELECT ContactID, FieldName, FieldValue
        FROM 
           (SELECT ContactID
                , cast(Forename as sql_variant) Forename
                , cast(Surname as sql_variant) Surname
                , cast(Extn as sql_variant) Extn
                , cast(Email as sql_variant) Email
                , cast(Age as sql_variant) Age
           FROM deleted) p
        UNPIVOT
           (FieldValue FOR FieldName IN 
              (Forename, Surname, Extn, Email, Age)
        ) AS deleted_unpvt
    ),
    --Unpivot inserted
    inserted_unpvt AS (
        SELECT ContactID, FieldName, FieldValue
        FROM 
           (SELECT ContactID
                , cast(Forename as sql_variant) Forename
                , cast(Surname as sql_variant) Surname
                , cast(Extn as sql_variant) Extn
                , cast(Email as sql_variant) Email
                , cast(Age as sql_variant) Age
           FROM inserted) p
        UNPIVOT
           (FieldValue FOR FieldName IN 
              (Forename, Surname, Extn, Email, Age)
        ) AS inserted_unpvt
    )

    --Join them together and show what's changed
    INSERT INTO Sample_Table_Changes (ContactID, FieldName, FieldValueWas, FieldValueIs)
    SELECT Coalesce (D.ContactID, I.ContactID) ContactID
        , Coalesce (D.FieldName, I.FieldName) FieldName
        , D.FieldValue as FieldValueWas
        , I.FieldValue AS FieldValueIs 
    FROM 
        deleted_unpvt d

            FULL OUTER JOIN 
        inserted_unpvt i
            on      D.ContactID = I.ContactID 
                AND D.FieldName = I.FieldName
    WHERE
         D.FieldValue <> I.FieldValue --Changes
        OR (D.FieldValue IS NOT NULL AND I.FieldValue IS NULL) -- Deletions
        OR (D.FieldValue IS NULL AND I.FieldValue IS NOT NULL) -- Insertions
END
GO
-- -------------------- Try some changes --------------------
UPDATE Sample_Table SET age = age+1;
UPDATE Sample_Table SET Extn = '5'+Extn where Extn Like '221_';

DELETE FROM Sample_Table WHERE ContactID = 3;

INSERT INTO Sample_Table VALUES (6,'Stephen','Turner','2299','st@example.com',25);

UPDATE Sample_Table SET ContactID = 7 where ContactID = 4; --this will be shown as a delete and an insert
-- -------------------- See the results --------------------
SELECT *, SQL_VARIANT_PROPERTY(FieldValueWas, 'BaseType') FieldBaseType, SQL_VARIANT_PROPERTY(FieldValueWas, 'MaxLength') FieldMaxLength from Sample_Table_Changes;

-- -------------------- Cleanup --------------------
DROP TABLE dbo.Sample_Table; DROP TABLE dbo.Sample_Table_Changes;

したがって、bigint ビットフィールドやアート オーバーフローの問題をいじる必要はありません。設計時に比較する列がわかっている場合は、動的 SQL は必要ありません。

欠点は、出力が異なる形式であり、すべてのフィールド値が sql_variant に変換されることです。1 つ目は出力を再度ピボットすることで修正でき、2 つ目はデータの知識に基づいて必要な型に再キャストすることで修正できます。テーブルの設計が必要ですが、どちらも複雑な動的 SQL を必要とします。これらは両方とも、XML 出力では問題にならない場合があります。この質問は、出力を同じ形式に戻すことに似ています。

編集:以下のコメントを確認すると、変更される可能性のある自然な主キーがある場合でも、このメソッドを使用できます。NEWID() 関数を使用して、デフォルトで GUID が設定されている列を追加するだけです。その後、主キーの代わりにこの列を使用します。

このフィールドにインデックスを追加することもできますが、トリガーで削除および挿入されたテーブルはメモリ内にあるため、使用されず、パフォーマンスに悪影響を及ぼす可能性があります。

于 2011-11-05T13:29:02.743 に答える
20

トリガー内では、COLUMNS_UPDATED()更新された値を取得するためにこのように使用できます

-- Get the table id of the trigger
--
DECLARE @idTable      INT

SELECT  @idTable = T.id 
FROM    sysobjects P JOIN sysobjects T ON P.parent_obj = T.id 
WHERE   P.id = @@procid

-- Get COLUMNS_UPDATED if update
--
DECLARE @Columns_Updated VARCHAR(50)

SELECT  @Columns_Updated = ISNULL(@Columns_Updated + ', ', '') + name 
FROM    syscolumns 
WHERE   id = @idTable   
AND     CONVERT(VARBINARY,REVERSE(COLUMNS_UPDATED())) & POWER(CONVERT(BIGINT, 2), colorder - 1) > 0

しかし、このコード スニペットは、62 列を超えるテーブルがある場合に失敗します.. Arth.Overflow...

これは、62 を超える列を処理する最終バージョンですが、更新された列の数のみを示しています。「syscolumns」とリンクして名前を取得するのは簡単です

DECLARE @Columns_Updated VARCHAR(100)
SET     @Columns_Updated = ''   

DECLARE @maxByteCU INT
DECLARE @curByteCU INT
SELECT  @maxByteCU = DATALENGTH(COLUMNS_UPDATED()), 
        @curByteCU = 1

WHILE @curByteCU <= @maxByteCU BEGIN
    DECLARE @cByte INT
    SET     @cByte = SUBSTRING(COLUMNS_UPDATED(), @curByteCU, 1)

    DECLARE @curBit INT
    DECLARE @maxBit INT
    SELECT  @curBit = 1, 
            @maxBit = 8
    WHILE @curBit <= @maxBit BEGIN
        IF CONVERT(BIT, @cByte & POWER(2,@curBit - 1)) <> 0 
            SET @Columns_Updated = @Columns_Updated + '[' + CONVERT(VARCHAR, 8 * (@curByteCU - 1) + @curBit) + ']'
        SET @curBit = @curBit + 1
    END
    SET @curByteCU = @curByteCU + 1
END
于 2009-12-17T14:49:16.483 に答える
5

私は単純な「ワンライナー」としてそれを行いました。ピボット、ループ、多くの変数などを使用しないと、手続き型プログラミングのように見えます。データセットの処理にはSQLを使用する必要があります:-)、解決策は次のとおりです。

DECLARE @sql as NVARCHAR(1024);

select @sql = coalesce(@sql + ',' + quotename(column_name), quotename(column_name))
from INFORMATION_SCHEMA.COLUMNS
where substring(columns_updated(), columnproperty(object_id(table_schema + '.' + table_name, 'U'), column_name, 'columnId') / 8 + 1, 1) & power(2, -1 + columnproperty(object_id(table_schema + '.' + table_name, 'U'), column_name, 'columnId') % 8 ) > 0
    and table_name = 'DBCustomers'
    -- and column_name in ('c1', 'c2') -- limit to specific columns
    -- and column_name not in ('c3', 'c4') -- or exclude specific columns

SET @sql = 'SELECT ' + @sql + ' FROM inserted FOR XML RAW';

DECLARE @x as XML;
SET @x = CAST(EXEC(@sql) AS XML);

COLUMNS_UPDATEDを使用し、8 つ以上の列を処理します。必要な数の列を処理します。

COLUMNPROPERTYを使用して取得する必要がある適切な列の順序に注意します。

ビューCOLUMNSに基づいているため、特定の列のみを含めたり除外したりできます。

于 2013-03-19T11:55:57.613 に答える
1

列名をハードコーディングせずにこれを達成できる唯一の方法は、削除されたテーブルの内容を一時テーブルにドロップし、テーブル定義に基づいてクエリを作成して、一時テーブルの内容を比較することですおよび実際のテーブルを検索し、それらが一致するかどうかに基づいて、区切られた列リストを返します。確かに、以下は精巧です。

Declare @sql nvarchar(4000)
DECLARE @ParmDefinition nvarchar(500)
Declare @OutString varchar(8000)
Declare @tbl sysname

Set @OutString = ''
Set @tbl = 'SomeTable' --The table we are interested in
--Store the contents of deleted in temp table
Select * into #tempDelete from deleted 
--Build sql string based on definition 
--of table 
--to retrieve the column name
--or empty string
--based on comparison between
--target table and temp table
set @sql = ''
Select @sql = @sql + 'Case when IsNull(i.[' + Column_Name + 
'],0) = IsNull(d.[' + Column_name + '],0) then '''' 
 else ' + quotename(Column_Name, char(39)) + ' + '',''' + ' end +'
from information_schema.columns 
where table_name = @tbl
--Define output parameter
set @ParmDefinition = '@OutString varchar(8000) OUTPUT'
--Format sql
set @sql = 'Select @OutString = ' 
+ Substring(@sql,1 , len(@sql) -1) + 
' From SomeTable i  ' --Will need to be updated for target schema
+ ' inner join #tempDelete d on
i.PK = d.PK ' --Will need to be updated for target schema
--Execute sql and retrieve desired column list in output parameter
exec sp_executesql @sql, @ParmDefinition, @OutString OUT
drop table  #tempDelete
--strip trailing column if a non-zero length string 
--was returned
if Len(@Outstring) > 0 
    Set @OutString = Substring(@OutString, 1, Len(@Outstring) -1)
--return comma delimited list of changed columns. 
Select @OutString 
End
于 2009-08-10T14:11:11.380 に答える
0

これは、一意のレコードと UpdatedBy ユーザーを使用して更新された列ごとの値のログを追跡するのに最適な例です。

IF NOT EXISTS
      (SELECT * FROM sysobjects WHERE id = OBJECT_ID(N'[dbo].[ColumnAuditLogs]') 
               AND OBJECTPROPERTY(id, N'IsUserTable') = 1)
       CREATE TABLE ColumnAuditLogs
               (Type CHAR(1), 
               TableName VARCHAR(128), 
               PK VARCHAR(1000), 
               FieldName VARCHAR(128), 
               OldValue VARCHAR(1000), 
               NewValue VARCHAR(1000), 
               UpdateDate datetime, 
               UserName VARCHAR(128),
               UniqueId uniqueidentifier,
               UpdatedBy int
               )
GO

create TRIGGER TR_ABCTable_AUDIT ON ABCTable FOR UPDATE
AS

DECLARE @bit INT ,
       @field INT ,
       @maxfield INT ,
       @char INT ,
       @fieldname VARCHAR(128) ,
       @TableName VARCHAR(128) ,
       @PKCols VARCHAR(1000) ,
       @sql VARCHAR(2000), 
       @UpdateDate VARCHAR(21) ,
       @UserName VARCHAR(128) ,
       @Type CHAR(1) ,
       @PKSelect VARCHAR(1000),
       @UniqueId varchar(100),
       @UpdatedBy VARCHAR(50) 


--You will need to change @TableName to match the table to be audited. 
-- Here we made ABCTable for your example.
SELECT @TableName = 'ABCTable' -- change table name accoring your table name

-- use for table unique records for everytime updation.
set @UniqueId = CONVERT(varchar(100),newID())
-- date and user
SELECT         @UserName = SYSTEM_USER ,
       @UpdateDate = CONVERT (NVARCHAR(30),GETDATE(),126)

 SELECT  @UpdatedBy = ModifiedBy --Change to your column name for the user update field
FROM    inserted;


-- Action
IF EXISTS (SELECT * FROM inserted)
       IF EXISTS (SELECT * FROM deleted)
               SELECT @Type = 'U'
       ELSE
               SELECT @Type = 'I'
ELSE
       SELECT @Type = 'D'

-- get list of columns
SELECT * INTO #ins FROM inserted
SELECT * INTO #del FROM deleted

-- Get primary key columns for full outer join
SELECT @PKCols = COALESCE(@PKCols + ' and', ' on') 
               + ' i.' + c.COLUMN_NAME + ' = d.' + c.COLUMN_NAME
       FROM    INFORMATION_SCHEMA.TABLE_CONSTRAINTS pk ,

              INFORMATION_SCHEMA.KEY_COLUMN_USAGE c
       WHERE   pk.TABLE_NAME = @TableName
       AND     CONSTRAINT_TYPE = 'PRIMARY KEY'
       AND     c.TABLE_NAME = pk.TABLE_NAME
       AND     c.CONSTRAINT_NAME = pk.CONSTRAINT_NAME


-- Get primary key select for insert
SELECT @PKSelect = COALESCE(@PKSelect+'+','') 
       + 'convert(varchar(100),
coalesce(i.' + COLUMN_NAME +',d.' + COLUMN_NAME + '))' 
       FROM    INFORMATION_SCHEMA.TABLE_CONSTRAINTS pk ,
               INFORMATION_SCHEMA.KEY_COLUMN_USAGE c
       WHERE   pk.TABLE_NAME = @TableName
       AND     CONSTRAINT_TYPE = 'PRIMARY KEY'
       AND     c.TABLE_NAME = pk.TABLE_NAME
       AND     c.CONSTRAINT_NAME = pk.CONSTRAINT_NAME

IF @PKCols IS NULL
BEGIN
       RAISERROR('no PK on table %s', 16, -1, @TableName)
       RETURN
END

SELECT         @field = 0, 
       @maxfield = MAX(ORDINAL_POSITION) 
       FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = @TableName
WHILE @field < @maxfield
BEGIN
       SELECT @field = MIN(ORDINAL_POSITION) 
               FROM INFORMATION_SCHEMA.COLUMNS 
               WHERE TABLE_NAME = @TableName 
               AND ORDINAL_POSITION > @field
       SELECT @bit = (@field - 1 )% 8 + 1
       SELECT @bit = POWER(2,@bit - 1)
       SELECT @char = ((@field - 1) / 8) + 1
       IF SUBSTRING(COLUMNS_UPDATED(),@char, 1) & @bit > 0
                                       OR @Type IN ('I','D')
       BEGIN
               SELECT @fieldname = COLUMN_NAME 
                       FROM INFORMATION_SCHEMA.COLUMNS 
                       WHERE TABLE_NAME = @TableName 
                       AND ORDINAL_POSITION = @field
               SELECT @sql = '
insert ColumnAuditLogs (    Type, 
               TableName, 
               PK, 
               FieldName, 
               OldValue, 
               NewValue, 
               UpdateDate, 
               UserName,
               UniqueId,
               [UpdatedBy])
select ''' + @Type + ''',''' 
       + @TableName + ''',' + @PKSelect
       + ',''' + @fieldname + ''''
       + ',convert(varchar(1000),d.' + @fieldname + ')'
       + ',convert(varchar(1000),i.' + @fieldname + ')'
       + ',''' + @UpdateDate + ''''
       + ',''' + @UserName + ''''
       + ',''' + @UniqueId + ''''
        + ',' + QUOTENAME(@UpdatedBy, '''')
       + ' from #ins i full outer join #del d'
       + @PKCols
       + ' where i.' + @fieldname + ' <> d.' + @fieldname 
       + ' or (i.' + @fieldname + ' is null and  d.'
                                + @fieldname
                                + ' is not null)' 
       + ' or (i.' + @fieldname + ' is not null and  d.' 
                                + @fieldname
                                + ' is null)' 
               EXEC (@sql)
       END
END

GO
于 2019-09-28T06:40:10.930 に答える
0

Rick が提供するサンプル コードには、複数行の更新の処理がありません。

Rick のバージョンを次のように拡張させてください。

USE [AFC]
GO

/****** Object:  Trigger [dbo].[trg_Survey_Identify_Updated_Columns]    Script Date: 27/7/2018 14:08:49 ******/
SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

ALTER TRIGGER [dbo].[trg_Survey_Identify_Updated_Columns] ON [dbo].[Sample_Table] --Change to match your table name
FOR INSERT
	,UPDATE
AS
SET NOCOUNT ON;

DECLARE @sql VARCHAR(5000)
	,@sqlInserted NVARCHAR(500)
	,@sqlDeleted NVARCHAR(500)
	,@NewValue NVARCHAR(100)
	,@OldValue NVARCHAR(100)
	,@UpdatedBy VARCHAR(50)
	,@ParmDefinitionD NVARCHAR(500)
	,@ParmDefinitionI NVARCHAR(500)
	,@TABLE_NAME VARCHAR(100)
	,@COLUMN_NAME VARCHAR(100)
	,@modifiedColumnsList NVARCHAR(4000)
	,@ColumnListItem NVARCHAR(500)
	,@Pos INT
	,@RecordPk VARCHAR(50)
	,@RecordPkName VARCHAR(50);

SELECT *
INTO #deleted
FROM deleted;

SELECT *
INTO #Inserted
FROM inserted;

SET @TABLE_NAME = 'Sample_Table';---Change to your table name

DECLARE t_cursor CURSOR
FOR
SELECT ContactID 
FROM inserted

OPEN t_cursor

FETCH NEXT
FROM t_cursor
INTO @RecordPk 

WHILE @@FETCH_STATUS = 0
BEGIN
	--SELECT @UpdatedBy = Surname --Change to your column name for the user update field
	--FROM inserted;
	--SELECT @RecordPk = ContactID --Change to the table primary key field
	--FROM inserted;
	SET @RecordPkName = 'ContactID';
	SET @modifiedColumnsList = STUFF((
				SELECT ',' + name
				FROM sys.columns
				WHERE object_id = OBJECT_ID(@TABLE_NAME)
					AND SUBSTRING(COLUMNS_UPDATED(), ((column_id - 1) / 8 + 1), 1) & (POWER(2, ((column_id - 1) % 8 + 1) - 1)) = POWER(2, (column_id - 1) % 8)
				FOR XML PATH('')
				), 1, 1, '');

	WHILE LEN(@modifiedColumnsList) > 0
	BEGIN
		SET @Pos = CHARINDEX(',', @modifiedColumnsList);

		IF @Pos = 0
		BEGIN
			SET @ColumnListItem = @modifiedColumnsList;
		END;
		ELSE
		BEGIN
			SET @ColumnListItem = SUBSTRING(@modifiedColumnsList, 1, @Pos - 1);
		END;

		SET @COLUMN_NAME = @ColumnListItem;
		SET @ParmDefinitionD = N'@OldValueOut NVARCHAR(100) OUTPUT';
		SET @ParmDefinitionI = N'@NewValueOut NVARCHAR(100) OUTPUT';
		SET @sqlDeleted = N'SELECT @OldValueOut=' + @COLUMN_NAME + ' FROM #deleted where ' + @RecordPkName + '=' + CONVERT(VARCHAR(50), @RecordPk);
		SET @sqlInserted = N'SELECT @NewValueOut=' + @COLUMN_NAME + ' FROM #Inserted where ' + @RecordPkName + '=' + CONVERT(VARCHAR(50), @RecordPk);

		EXECUTE sp_executesql @sqlDeleted
			,@ParmDefinitionD
			,@OldValueOut = @OldValue OUTPUT;

		EXECUTE sp_executesql @sqlInserted
			,@ParmDefinitionI
			,@NewValueOut = @NewValue OUTPUT;

		--PRINT @newvalue
		--PRINT @oldvalue

		IF (LTRIM(RTRIM(@NewValue)) != LTRIM(RTRIM(@OldValue)))
		BEGIN
			SET @sql = 'INSERT INTO [dbo].[AuditDataChanges]
                                               ([TableName]
                                               ,[RecordPK]
                                               ,[ColumnName]
                                               ,[OldValue]
                                               ,[NewValue] )
                                         VALUES
                                               (' + QUOTENAME(@TABLE_NAME, '''') + '
                                               ,' + QUOTENAME(@RecordPk, '''') + '
                                               ,' + QUOTENAME(@COLUMN_NAME, '''') + '
                                               ,' + QUOTENAME(@OldValue, '''') + '
                                               ,' + QUOTENAME(@NewValue, '''') + '
                                               '  + ')';

			EXEC (@sql);
		END;

		SET @COLUMN_NAME = '';
		SET @NewValue = '';
		SET @OldValue = '';

		IF @Pos = 0
		BEGIN
			SET @modifiedColumnsList = '';
		END;
		ELSE
		BEGIN
			-- start substring at the character after the first comma
			SET @modifiedColumnsList = SUBSTRING(@modifiedColumnsList, @Pos + 1, LEN(@modifiedColumnsList) - @Pos);
		END;
	END;

	FETCH NEXT
	FROM t_cursor
	INTO @RecordPk 
END

DROP TABLE #Inserted;

DROP TABLE #deleted;

CLOSE t_cursor;

DEALLOCATE t_cursor;

于 2018-07-27T06:51:24.303 に答える