2009-08-10 19 views
12

Conozco COLUMNS_UPDATED, bien, necesito un atajo rápido (si alguien lo ha hecho, ya estoy haciendo uno, pero si alguien puede ahorrarme mi tiempo lo apruebo)Activación de actualización del Servidor SQL, Obtener solo campos modificados

necesito básicamente, un XML de valores de columna única actualizados, necesito esto para fines de replicación.

SELECT * FROM insertada me da cada columna, pero necesito solamente actualizados queridos.

algo como siguiente ...

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 

Respuesta

13

Dentro del gatillo, puede utilizar COLUMNS_UPDATED() como esto con el fin de recibir información actualizada valor

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

Pero esto snipet de código falla cuando se tiene una tabla con más de 62 columnas .. Arth. desbordamiento ...

Esta es la versión final que se ocupa de más de 62 columnas sino dar sólo el número de las columnas actualizadas. Es fácil enlazar con '' syscolumns para obtener el nombre

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

Gracias por la respuesta, sin embargo, la unión es muy costosa a este nivel teniendo en cuenta los cambios enormes, así que no voy a utilizarlo como estoy tratando de descubrir algo en el seguimiento de cambios, pero gracias de todos modos. –

+0

Bueno, pero para información, cuando hay una brecha en column_id en sys.columns (por ejemplo, de columna eliminada), el código anterior no lo detecta. Ej. Con una tabla de 26 columnas, el resultado es [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12] [13] [14] [15] [16] [17] [18] [23] [24] [25] [26] [27] mientras que no hay columna con column_id = 9 – Yahia

+0

no está funcionando como se esperaba. En algún momento funciona para 1 campo, a veces funciona, a veces falla por completo (en función de la consulta, pero no de la salida aleatoria para la misma consulta). ¡Estoy usando SQL server 2014 (120)! – SKLTFZ

1

La única manera que se me ocurre que se podría lograr esto sin nombres de columna de codificación duro sería dejar caer el contenido de la tabla eliminada a una tabla temporal, a continuación, crear una consulta basada en la definición de tabla para comparar el contenido de su tabla temporal y la tabla real, y devolver una lista de columnas delimitadas en función de si coinciden o no. Es cierto que lo siguiente es elaborado.

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

tengo otra solución completamente diferente que no utiliza COLUMNS_UPDATED en absoluto, ni se basa en la construcción de SQL dinámico en tiempo de ejecución. (Es posible que desee utilizar SQL dinámico en el momento del diseño, pero esa es otra historia).

Básicamente, comience con the inserted and deleted tables, desvincule cada uno de ellos para que quede con la clave única, el valor del campo y las columnas del nombre del campo para cada uno. Luego te unes a los dos y filtra por cualquier cosa que haya cambiado.

Aquí es un ejemplo de trabajo completo, incluyendo algunas llamadas de prueba para mostrar lo que se registra.

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

Así que no jugar un poco con campos de bits bigint y problemas de desbordamiento de Arth. Si conoce las columnas que desea comparar en tiempo de diseño, entonces no necesita ningún SQL dinámico.

En la parte inferior, la salida está en un formato diferente y todos los valores de campo se convierten a sql_variant, el primero se puede arreglar girando nuevamente la salida y el segundo se puede arreglar volviendo a los tipos necesarios en función de su conocimiento del diseño de la tabla, pero ambos requerirían algunos sql dinámicos complejos. Ambos pueden no ser un problema en su salida XML.

Edición: Revisión de los comentarios a continuación, si tiene una clave principal natural que podría cambiar a continuación, puede seguir utilizando este método. Solo necesita agregar una columna que se completa de manera predeterminada con un GUID que usa la función NEWID(). A continuación, usa esta columna en lugar de la clave principal.

Es posible que desee añadir un índice en este campo, pero a medida que las tablas eliminadas e insertadas en un disparador están en la memoria no puede acostumbrarse y puede tener un efecto negativo en el rendimiento.

+0

Buena solución. Tenga en cuenta que, dado que se llama a un desencadenante después de las instrucciones individuales de inserción/actualización/eliminación, realmente solo necesita lidiar con el escenario de actualización. Me preguntaba si podría haber simplificaciones (para la velocidad). –

+1

@HermanSchoenfeld ¿Tiene un problema de rendimiento? Recuerde: "... la optimización prematura es la raíz de todo mal". (Donald Knuth) –

+0

Creo que hay un problema. Su desencadenador asume que la clave principal no ha cambiado, cuando puede. Por lo tanto, debe separar los casos de inserción/actualización/eliminación de todos modos, lo que significa que puede ignorar por completo los casos de inserción/eliminación. Para el caso de actualización, deberá unir insert/deleted mediante el índice de fila. –

3

lo he hecho tan simple "de una sola línea". Sin usar, pivotar, bucles, muchas variables, etc., que lo hace parecerse a la programación de procedimientos. SQL se debe utilizar para procesar conjuntos de datos :-), la solución es:

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

Utiliza COLUMNS_UPDATED, se encarga de más de ocho columnas - que maneja tantas columnas como desee.

Se ocupa en las columnas apropiadas orden que se debe conseguir mediante COLUMNPROPERTY.

Se basa en vista COLUMNS por lo que puede incluir o excluir solamente las columnas específicas.

+1

Esto funcionaría, pero sql dinámico no tiene acceso a las tablas 'Inserted' o' Deleted'. – Fowl

+0

insertadas/suprimidas son tablas residentes en memoria seudo no está disponible durante la ejecución de SQL dinámico así que seleccionar * en #tmpInserted desde insertada select * en #tmpDeleted de borrado y utilizar el # tmpInserted, # tmpDeleted en su dinámica sql –

+0

Este enlace [MS support] (https://support.microsoft.com/en-us/kb/232195) indica que COLUMNS_UPDATED no funciona para más de 32 columnas. Su código funciona con 2008 R2 en al menos 62 columnas. – Maa421s

1

El código siguiente funciona desde hace más de 64 columnas y los registros sólo las columnas actualizadas. Siga las instrucciones en los comentarios y todo debería estar bien.

/******************************************************************************************* 
*   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 
Cuestiones relacionadas