2009-08-10 10 views
12

Jestem świadomy COLUMNS_UPDATED, potrzebuję szybkiego skrótu (jeśli ktoś już zrobił, już go wykonuję, ale jeśli ktoś może zaoszczędzić mój czas, to ja go aplikuję)Wywołanie aktualizacji SQL Server, tylko zmodyfikowane zmienne

Potrzebuję w zasadzie XML tylko zaktualizowanych wartości kolumn, potrzebuję tego do celów replikacji.

SELECT * FROM inserted podaje mi każdą kolumnę, ale potrzebuję tylko zaktualizowanych.

coś następujących ...

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 

Odpowiedz

13

Wewnątrz spustu, można użyć COLUMNS_UPDATED() tak, aby uzyskać zaktualizowane wartość

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

Ale to snipet kodu nie powiedzie się, gdy masz tabelę z ponad 62 kolumn .. Arth. Przepełnienie ...

Oto ostateczna wersja, która obsługuje więcej niż 62 kolumny, ale podaje tylko liczbę zaktualizowanych kolumn. Jest to łatwe do połączenia z „tabeli syscolumns”, aby uzyskać nazwę

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 
+0

Dzięki za odpowiedź, jednak JOIN jest naprawdę kosztowne na tym poziomie, biorąc pod uwagę ogromne zmiany, więc nie będę go używać jako Próbuję dowiedzieć się czegoś w śledzeniu zmian, ale dzięki tak. –

+0

Fajnie, ale dla informacji, gdy istnieje luka w kolumnie id_kolumny w sys.columns (ex dropped column), powyższy kod go nie wykrywa. Np. Z tabelą 26 kolumn, wynikiem jest [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12] [13] [14] [15] [16] [17] [18] [19] [20] [21] [22] [23] [24] [25] [26] [27], podczas gdy nie ma kolumny z column_id = 9 – Yahia

+0

nie działa zgodnie z oczekiwaniami. Czasami działa dla 1 pól, czasem działa, czasem nie działa całkowicie (jest oparty na zapytaniu, ale nie jest wynikiem randomizacji dla tego samego zapytania). Używam serwera SQL 2014 (120)! – SKLTFZ

1

Jedynym sposobem, który przychodzi mi do głowy, że można tego dokonać bez twardych kodowania nazw kolumn byłby spadek zawartości usuniętej tabeli do tabeli temp, a następnie zbuduj zapytanie oparte na definicji tabeli, aby porównać zawartość tabeli temp z tabelą rzeczywistą i zwrócić rozdzielaną listę kolumn na podstawie tego, czy są one zgodne, czy nie. Prawdą jest, że poniżej jest dopracowany.

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 
14

mam inną zupełnie inne rozwiązanie, które nie używają COLUMNS_UPDATED w ogóle, ani nie polegać na budowaniu dynamicznych SQL w czasie wykonywania. (Możesz chcieć użyć dynamicznego SQL w czasie projektowania, ale to już inna historia.)

Zasadniczo zaczynasz od the inserted and deleted tables, rozpakowujesz każdą z nich, dzięki czemu po prostu pozostawiasz unikalne kolumny klucza, kolumny wartości pola i nazwy pól dla każdego z nich. Potem dołączasz do dwóch i filtrujesz wszystko, co się zmieniło.

Oto pełny przykład działania, w tym niektóre wywołania testowe pokazujące, co jest rejestrowane.

-- -------------------- 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','[email protected]',24); 
INSERT INTO Sample_Table VALUES (2,'Alice','Brown','2255','[email protected]',32); 
INSERT INTO Sample_Table VALUES (3,'Reg','Jones','2280','[email protected]',19); 
INSERT INTO Sample_Table VALUES (4,'Mary','Doe','2216','[email protected]',28); 
INSERT INTO Sample_Table VALUES (5,'Peter','Nash','2214','[email protected]',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','[email protected]',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; 

Nie ma problemu z dużymi bitfieldami i problemami z przepełnieniem arth. Jeśli znasz kolumny, które chcesz porównać w czasie projektowania, nie potrzebujesz dynamicznego SQL.

Z drugiej strony dane wyjściowe mają inny format, a wszystkie wartości pól są konwertowane na sql_variant, pierwsze można naprawić, ponownie obracając dane wyjściowe, a drugie można naprawić, przekształcając z powrotem na wymagane typy na podstawie znajomość projektu tabeli, ale oba te elementy wymagałyby złożonego dynamicznego sql. Oba mogą nie być problemem w wynikach XML.

Edytuj: Przeglądając poniższe komentarze, jeśli masz naturalny klucz podstawowy, który może ulec zmianie, nadal możesz korzystać z tej metody. Trzeba tylko dodać kolumnę, która jest domyślnie zapełniona za pomocą identyfikatora GUID za pomocą funkcji NEWID(). Następnie używasz tej kolumny zamiast klucza podstawowego.

Możesz dodać indeks do tego pola, ale ponieważ usunięte i wstawione tabele w wyzwalaczu są w pamięci, mogą nie zostać użyte i mogą mieć negatywny wpływ na wydajność.

+0

Ładne rozwiązanie. Uwaga: ponieważ wyzwalacz jest wywoływany po poszczególnych instrukcjach insert/update/delete, tak naprawdę musisz poradzić sobie ze scenariuszem aktualizacji. Zastanawiałeś się, czy mogą być jakieś uproszczenia (dla prędkości). –

+1

@HermanSchoenfeld Czy masz problem z wydajnością? Pamiętaj: "... przedwczesna optymalizacja jest źródłem wszelkiego zła". (Donald Knuth) –

+0

Myślę, że jest problem. Twój wyzwalacz zakłada, że ​​klucz podstawowy się nie zmienił, kiedy tylko może. W związku z tym należy oddzielić przypadki wstawiania/aktualizacji/usunięcia, co oznacza, że ​​można po prostu zignorować przypadki wstawiania/usuwania. W przypadku aktualizacji konieczne jest dołączenie wstawione/usunięte według indeksu wiersza. –

3

Zrobiłem to tak proste "one-liner". Bez użycia, obrotu, pętli, wielu zmiennych itp. Sprawia, że ​​wygląda on jak programowanie proceduralne. SQL powinny być wykorzystywane do przetwarzania zbiorów danych :-), rozwiązaniem jest:

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

Wykorzystuje COLUMNS_UPDATED, troszczy się o więcej niż ośmiu kolumnach - to zajmuje tyle kolumn, ile chcesz.

Dba o odpowiednich kolumnach zamówić które należy uzyskać stosując COLUMNPROPERTY.

Jest on oparty na widoku COLUMNS więc może włączyć lub wyłączyć tylko konkretne kolumny.

+1

To działałoby, ale dynamiczny sql nie ma dostępu do tabel "Wstawiony" lub "Usunięty". – Fowl

+0

Wstawiony/Usunięte są tablice pamięci rezydentami pseudo niedostępna podczas wykonywania dynamicznego SQL więc chciałbym select * w #tmpInserted od włożona select * w #tmpDeleted z usuniętym i użyć # tmpInserted, # tmpDeleted w dynamicznym sql –

+0

Ten link [wsparcie MS] (https://support.microsoft.com/en-us/kb/232195) wskazuje, że COLUMNS_UPDATED nie działa dla więcej niż 32 kolumn. Twój kod działa z 2008 R2 na co najmniej 62 kolumny. – Maa421s

1

Poniższy kod działa od ponad 64 słupów i bali tylko zaktualizowane kolumn. Postępuj zgodnie z instrukcjami w komentarzach i wszystko powinno być dobrze.

/******************************************************************************************* 
*   Add the below table to your database to track data changes using the trigger * 
*   below. Remember to change the variables in the trigger to match the table that * 
*   will be firing the trigger              * 
*******************************************************************************************/ 
SET ANSI_NULLS ON; 
GO 

SET QUOTED_IDENTIFIER ON; 
GO 

CREATE TABLE [dbo].[AuditDataChanges] 
(
    [RecordId] [INT] IDENTITY(1, 1) 
        NOT NULL , 
    [TableName] [VARCHAR](50) NOT NULL , 
    [RecordPK] [VARCHAR](50) NOT NULL , 
    [ColumnName] [VARCHAR](50) NOT NULL , 
    [OldValue] [VARCHAR](50) NULL , 
    [NewValue] [VARCHAR](50) NULL , 
    [ChangeDate] [DATETIME2](7) NOT NULL , 
    [UpdatedBy] [VARCHAR](50) NOT NULL , 
    CONSTRAINT [PK_AuditDataChanges] PRIMARY KEY CLUSTERED 
    ([RecordId] ASC) 
    WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, 
      IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, 
      ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] 
) 
ON [PRIMARY]; 

GO 

ALTER TABLE [dbo].[AuditDataChanges] ADD CONSTRAINT [DF_AuditDataChanges_ChangeDate] DEFAULT (GETDATE()) FOR [ChangeDate]; 
GO 



/************************************************************************************************ 
* Add the below trigger to any table you want to audit data changes on. Changes will be saved * 
* in the AuditChangesTable.                 * 
************************************************************************************************/ 


ALTER TRIGGER trg_Survey_Identify_Updated_Columns ON Survey --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 = 'Survey'; ---Change to your table name 
SELECT @UpdatedBy = UpdatedBy --Change to your column name for the user update field 
FROM inserted; 
SELECT @RecordPk = SurveyId --Change to the table primary key field 
FROM inserted; 
SET @RecordPkName = 'SurveyId'; 
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; 
     IF (LTRIM(RTRIM(@NewValue)) != LTRIM(RTRIM(@OldValue))) 
      BEGIN 
       SET @sql = 'INSERT INTO [dbo].[AuditDataChanges] 
               ([TableName] 
               ,[RecordPK] 
               ,[ColumnName] 
               ,[OldValue] 
               ,[NewValue] 
               ,[UpdatedBy]) 
             VALUES 
               (' + QUOTENAME(@TABLE_NAME, '''') + ' 
               ,' + QUOTENAME(@RecordPk, '''') + ' 
               ,' + QUOTENAME(@COLUMN_NAME, '''') + ' 
               ,' + QUOTENAME(@OldValue, '''') + ' 
               ,' + QUOTENAME(@NewValue, '''') + ' 
               ,' + QUOTENAME(@UpdatedBy, '''') + ')'; 


       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; 
DROP TABLE #Inserted; 
DROP TABLE #deleted; 

GO