campos de SQL Server de actualización se activa, obtener sólo modificados
-
12-09-2019 - |
Pregunta
Soy consciente de COLUMNS_UPDATED
, así que necesito un poco de acceso directo rápido (si alguien ha hecho, ya estoy haciendo uno, pero si alguien puede salvar a mi vez, me appriciate)
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
Solución
En el interior del gatillo, puede utilizar COLUMNS_UPDATED()
como esto con el fin de obtener el valor actualizado
-- 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.Overflow ...
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
Otros consejos
No 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 tiempo de diseño, pero eso es otra historia.)
Básicamente se empieza con las tablas insertadas y eliminadas , UNPIVOT cada uno de ellos por lo que se acaba de dejar con la clave única, el valor del campo y de campo columnas de nombres para cada uno. A continuación, se une a las dos y filtrar para cualquier cosa que ha cambiado.
Aquí está 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','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;
Así que no jugar un poco con campos de bits bigint y problemas de desbordamiento de Arth. Si conoces las columnas que desea comparar en tiempo de diseño, entonces no es necesario ningún SQL dinámico.
En el lado negativo, la salida es en un formato diferente y todos los valores de campo se convierten a sql_variant, la primera podría ser fijada por el giro de la salida de nuevo, y el segundo podría ser fijada por la refundición de nuevo a los tipos requeridos en función de su conocimiento del diseño de la tabla, pero ambos podrían requerir algún SQL dinámico complejo. Ambos de estos podría no ser un problema en su salida XML. Esta pregunta hace algo similar a obtener la salida de nuevo en el mismo formato .
Editar: 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. Sólo tiene que añadir una columna que se rellena por defecto con un GUID utilizando la función NEWID (). A continuación, utiliza esta columna en lugar de la clave primaria.
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 que no podría obtener empleadas y puede tener un efecto negativo en el rendimiento.
Lo he hecho tan simple "de una sola línea". Sin usar, pivote, bucles, etc. muchas variables que hace que el aspecto de la programación de procedimiento. SQL debe utilizarse para procesar los 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);
COLUMNS_UPDATED , se hace cargo de más de ocho columnas -. maneja tantas columnas como desee
Se ocupa en las columnas apropiadas orden que debería ser conseguir usando COLUMNPROPERTY .
Se basa en COLUMNAS rel="nofollow"> vista por lo que puede incluir o excluir sólo las columnas específicas.
La única manera que se me ocurre que se podría lograr esto sin codificar duro nombres de columna 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 devuelven una lista de columnas delimitado en función de si se hacen o no se correspondan. Es cierto, el siguiente es elaborada.
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
El siguiente código 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
he transformado la respuesta aceptada para obtener la lista de nombres de columnas separadas por comas (según la recomendación del autor). Salida - "COLUMNS_UPDATED" como 'la columna 1, columna 2, Column5'
-- get names of updated columns
DECLARE @idTable INT
declare @ColumnName nvarchar(300)
declare @ColId int
SELECT @idTable = T.id
FROM sysobjects P JOIN sysobjects T ON P.parent_obj = T.id
WHERE P.id = @@procid
DECLARE @changedProperties nvarchar(max) = ''
DECLARE @Columns_Updated VARCHAR(2000) = ''
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 BEGIN
SET @ColId = cast( CONVERT(VARCHAR, 8 * (@curByteCU - 1) + @curBit) as int)
select @ColumnName = [Name]
FROM syscolumns
WHERE id = @idTable and colid = @ColId
SET @Columns_Updated = @Columns_Updated + ',' + @ColumnName
END
SET @curBit = @curBit + 1
END
SET @curByteCU = @curByteCU + 1
END
El código de ejemplo proporcionado por Rick carecen de manipulación para actualizar varias filas.
Por favor, hágamelo mejoro versión de Rick como a continuación:
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;