SQL Server Update Trigger, Get Only modified fields - sql

I am aware of COLUMNS_UPDATED, well I need some quick shortcut (if anyone has made, I am already making one, but if anyone can save my time, I will appriciate it)
I need basicaly an XML of only updated column values, I need this for replication purpose.
SELECT * FROM inserted gives me each column, but I need only updated ones.
something like following...
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

I've another completely different solution that doesn't use COLUMNS_UPDATED at all, nor does it rely on building dynamic SQL at runtime. (You might want to use dynamic SQL at design time but thats another story.)
Basically you start with the inserted and deleted tables, unpivot each of them so you are just left with the unique key, field value and field name columns for each. Then you join the two and filter for anything that's changed.
Here is a full working example, including some test calls to show what is logged.
-- -------------------- 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;
So no messing around with bigint bitfields and arth overflow problems. If you know the columns you want to compare at design time then you don't need any dynamic SQL.
On the downside the output is in a different format and all the field values are converted to sql_variant, the first could be fixed by pivoting the output again, and the second could be fixed by recasting back to the required types based on your knowledge of the design of the table, but both of these would require some complex dynamic sql. Both of these might not be an issue in your XML output. This question does something similar to getting the output back in the same format.
Edit: Reviewing the comments below, if you have a natural primary key that could change then you can still use this method. You just need to add a column that is populated by default with a GUID using the NEWID() function. You then use this column in place of the primary key.
You may want to add an index to this field, but as the deleted and inserted tables in a trigger are in memory it might not get used and may have a negative effect on performance.

Inside the trigger, you can use COLUMNS_UPDATED() like this in order to get updated value
-- 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
But this snipet of code fails when you have a table with more than 62 columns.. Arth.Overflow...
Here is the final version which handles more than 62 columns but give only the number of the updated columns. It's easy to link with 'syscolumns' to get the name
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

I've done it as simple "one-liner". Without using, pivot, loops, many variables etc. that makes it looking like procedural programming. SQL should be used to process data sets :-), the solution is:
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);
It uses COLUMNS_UPDATED, takes care of more than eight columns - it handles as many columns as you want.
It takes care on proper columns order which should be get using COLUMNPROPERTY.
It is based on view COLUMNS so it may include or exclude only specific columns.

The below code works for over 64 columns and logs only the updated columns. Follow the instruction in the comments and all should be well.
/*******************************************************************************************
* 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

I transformed the accepted answer to get list of column names separated by comma (according to author's recommendation). Output - "Columns_Updated" as 'Column1,Column2,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

The only way that occurs to me that you could accomplish this without hard coding column names would be to drop the contents of the deleted table to a temp table, then build a query based on the table definition to to compare the contents of your temp table and the actual table, and return a delimited column list based on whether they do or do not match. Admittedly, the below is elaborate.
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

The sample code provided by Rick lack handling for multiple rows update.
Please let me enhance Rick's version as below:
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;

This is perfect example for track log of updated columnwise value with unique records and UpdatedBy user.
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

Related

SQL Server - Instead of Insert Trigger - override truncation error?

Scenario: using an "Instead of Insert" trigger to a different table (overriding inserting into the current table) throws a truncation error.
Try to insert data into TableA
Instead Of Trigger is setup to run on TableA
This trigger instead inserts into TableB
TableA is not written to
Issue:
TableA has a nvarchar(10) desc column and TableB has a nvarchar(200) desc column. An insert trigger is setup on TableA where the data for the desc column is 50 characters long.
With SET ANSI_WARNINGS ON (the default), TableA causes a truncation error.
With SET ANSI_WARNINGS OFF (dangerous), truncation is ignored on TableB. So if the desc came in at 400 characters, it would be truncated to 200 characters with no errors.
Setup
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[results]
(
[id] [int] IDENTITY(1,1) NOT NULL,
[causefailure] [nvarchar](5) NULL,
[type] [nvarchar](50) NOT NULL,
[description] [nvarchar](200) NULL,
[rundate] [datetime] NOT NULL DEFAULT (getdate())
) ON [PRIMARY]
GO
CREATE TABLE [dbo].[test_table]
(
[id] [int] IDENTITY(1,1) NOT NULL,
[description] [nvarchar](10) NULL,
[rundate] [datetime] NOT NULL DEFAULT (getdate())
) ON [PRIMARY]
GO
CREATE TRIGGER [dbo].[InsteadTrigger]
ON [dbo].[test_table]
INSTEAD OF INSERT
AS
SET NOCOUNT ON
SET ANSI_WARNINGS ON
BEGIN
INSERT INTO results([type], [description])
SELECT
(CASE SUBSTRING([description], 1, 1)
WHEN 'a'
THEN 'causes failure or truncation'
ELSE ''
END AS [causefailure],
'Instead Of Trigger' AS [type],
[description]
FROM inserted
END;
GO
ALTER TABLE [dbo].[test_table] ENABLE TRIGGER [InsteadTrigger]
GO
First attempt:
SET ANSI_WARNINGS OFF
INSERT INTO [dbo].[test_table]([description])
SELECT 'atest12345678910' AS [description]
Second attempt:
SET ANSI_WARNINGS ON
INSERT INTO [dbo].[test_table]([description])
SELECT 'btest12345678910' AS [description]
INSERT INTO [dbo].[test_table]([description])
SELECT 'atest12345678910' AS [description]
But when I run the the first code snippet, causefailure gets truncated. But there is no error. Running the second snippet inserts one record in [results]. But it throws an exception on the second.
Is there a way to ignore: constraints, conversion issues, etc., exceptions with the initial write to [test_table]? But have exceptions for any work done in the trigger (e.g. inserting into [results] and maybe even the actual work to [test_tables])?
Edit: I do not want to change the column size of nvarchar(10) desc
on TableA. The ultimate goal might be if the "instead of trigger"
fails to insert on TableA, it saves to TableB. Or it might even change
the size of desc on failure dynamically inside the trigger and retry
the insert.
Hopefully helpful!
Below is an incomplete idea how to work-around the above problem. It kind of evasive. But interesting way test how far you can rework a database.
The below changes the schema around when a table is created or altered. On creating or altering a table, it will be swapped out for an indexed view after the table gets renamed.
ALTER TRIGGER trigger_CreateTable
ON DATABASE
AFTER CREATE_TABLE, ALTER_TABLE
AS
BEGIN
--SELECT EVENTDATA()
DECLARE #Prefix AS nvarchar(256) = N'PleaseUseView_'
DECLARE #Event AS XML = EVENTDATA()
DECLARE #SchemaName AS nvarchar(255) = (#Event.value('(/EVENT_INSTANCE/SchemaName)[1]', 'NVARCHAR(255)'))
DECLARE #TableName AS nvarchar(255) = (#Event.value('(/EVENT_INSTANCE/ObjectName)[1]', 'NVARCHAR(255)'))
DECLARE #ObjectType AS nvarchar(255) = (#Event.value('(/EVENT_INSTANCE/ObjectType)[1]', 'NVARCHAR(255)'))
DECLARE #TableWithSchema AS nvarchar(512) = '[' + #SchemaName + '].[' + #TableName + ']'
CREATE TABLE #SchemaBindingDependencies
(
[id] int NOT NULL IDENTITY,
[schema] nvarchar(256) NOT NULL,
[name] nvarchar(256) NOT NULL
)
INSERT INTO #SchemaBindingDependencies([schema], [name])
SELECT DISTINCT s.name AS [schema], o.name
FROM sys.objects AS o
INNER JOIN sysdepends AS d
ON d.id = o.[object_id]
INNER JOIN sys.schemas AS s
ON s.[schema_id] = o.[schema_id]
WHERE o.type ='V' AND d.depid = OBJECT_ID(#TableWithSchema)
AND SUBSTRING(#TableName, LEN(#Prefix) + 1, 256) LIKE o.[name]
IF (EXISTS(SELECT 1 FROM #SchemaBindingDependencies))
BEGIN
DECLARE #Index AS int = (SELECT MAX(id) FROM #SchemaBindingDependencies)
WHILE (#Index > 0)
BEGIN
DECLARE #ViewName1 AS nvarchar(256) = (SELECT [name] FROM #SchemaBindingDependencies WHERE id = #Index)
IF (#ViewName1 IS NOT NULL)
BEGIN
DECLARE #SchemaName1 AS nvarchar(256) = (SELECT [schema] FROM #SchemaBindingDependencies WHERE id = #Index)
DECLARE #DropSchemaBoundViewQuery AS nvarchar(1000) = 'DROP VIEW [' + #SchemaName + '].[' + #ViewName1 + ']'
EXEC(#DropSchemaBoundViewQuery)
END
SET #Index = #Index - 1
END
END
IF (SUBSTRING(#TableName, 1, LEN(#Prefix)) <> #Prefix)
BEGIN
DECLARE #NewTableName AS nvarchar(512) = #Prefix + #TableName + ''
DECLARE #NewTableWithSchema AS nvarchar(512) = '[' + #SchemaName + '].[' + #NewTableName + ']'
EXEC sp_rename #TableWithSchema, #NewTableName
SET #TableName = #NewTableName
SET #TableWithSchema = '[' + #SchemaName + '].[' + #NewTableName + ']'
END
DECLARE #Columns AS nvarchar(max) = (STUFF((SELECT ',[' + x.[name] + ']' FROM (
SELECT c.[name]
FROM sys.columns AS c
INNER JOIN sys.tables AS t
ON t.[object_id] = c.[object_id]
INNER JOIN sys.schemas AS s
ON s.[schema_id] = t.[schema_id]
WHERE t.[name] = #TableName AND s.[name] = #SchemaName) AS x FOR XML PATH('')), 1, 1, ''))
DECLARE #ViewName AS nvarchar(256) = SUBSTRING(#TableName, LEN(#Prefix) + 1, 256)
DECLARE #ViewWithSchema AS nvarchar(512) = '[' + #SchemaName + '].[' + #ViewName + ']'
DECLARE #Query AS nvarchar(max) =
N'CREATE VIEW ' + #ViewWithSchema + N' ' + CHAR(10) + CHAR(13) +
N'WITH SCHEMABINDING ' + CHAR(10) + CHAR(13) +
N'AS ' + CHAR(10) + CHAR(13) +
N' SELECT ' + #Columns + ' ' + CHAR(10) + CHAR(13) +
N' FROM ' + #TableWithSchema + N' '
--SELECT #Query
EXEC(#Query)
SET #Query =
N'CREATE UNIQUE CLUSTERED INDEX [CIX_' + #ViewName + N'] ' + CHAR(10) + CHAR(13) +
N'ON ' + #ViewWithSchema + N'(' + #Columns + N')'
EXEC(#Query)
-- TODO: Use the below double commented to build a variable insert statement for the "Instead of TRIGGER"
--
----DECLARE #tv_source TABLE (id int)
----declare #XML xml;
----set #XML =
---- (
---- select top(0) *
---- from #tv_source
---- for XML RAW, ELEMENTS, XMLSCHEMA
---- );
----SELECT T.c.query('.'), T.c.value('#name', 'nvarchar(256)')
----FROM #XML.nodes('/*/*/*/*/*') AS T(c)
--
--SET #Query =
-- N'CREATE TRIGGER [Trigger_' + #ViewName + N'] ' + CHAR(10) + CHAR(13) +
-- N'ON ' + #ViewWithSchema + N' ' + CHAR(10) + CHAR(13) +
-- N'INSTEAD OF INSERT ' + CHAR(10) + CHAR(13) +
-- N'AS BEGIN ' + CHAR(10) + CHAR(13) +
-- N'BEGIN TRY ' + CHAR(10) + CHAR(13) +
-- N' INSERT INTO ' + #TableWithSchema + N'(' + #Columns + N')'
-- N' SELECT ' + #Columns +
--EXEC(#Query)
END
Ideally, you would use a different schema for tables. And use dbo or
the default for the view.
Once the Instead of TRIGGER is working, you could wrap a TRY/CATCH around it. On the catch, check schema for truncation. And expand the column sizes if needed.
Incomplete solution. But it is the answer I will stick with for now.
If anyone has any better answers or a complete solution, please add it!
tl;dr
One interesting thing is the below query
DECLARE #tv_source TABLE (id int)
declare #XML xml;
select top(0) *
from #tv_source
for XML RAW, ELEMENTS, XMLSCHEMA
You can return schema. SOAP for the above XML. Or JsonSchema or Avro if using the SQL 2016+ Json version to build Restful API's with schema awareness. And with schema awareness, an application gateway could auto-grab many dispersed Micro-Rest API's into one seemly large Rest API.

SQL Server 2012 - Trigger: Operand type clash: int is incompatible with unique identifier

I am writing an audit trigger which saves every change occurred in the table in another table. But I receive this error when I am trying to alter the Users table:
Operand type clash: int is incompatible with unique identifier
The Users table contains a uniqueidentifier column, among others.
Here is the part of the code where the error occurs:
ALTER TRIGGER [dbo].[UsersLogger]
ON [dbo].[Users]
FOR INSERT, DELETE, UPDATE
AS
BEGIN
DECLARE #audit_oldvalue sql_variant;
DECLARE #audit_value sql_variant;
DECLARE #audit_field varchar(100);
DECLARE #sql nvarchar(max);
DECLARE #ParmDefinition nvarchar(max) ;
DECLARE #OutString varchar(max) ;
DECLARE #converted_uid nvarchar(50);
DECLARE #qid int ;
DECLARE #Cinfo VARBINARY(128)
SELECT #Cinfo = Context_Info()
IF #Cinfo = 0x55555
RETURN
DECLARE #Action as char(1);
SET #Action = (CASE WHEN EXISTS(SELECT * FROM INSERTED)
AND EXISTS(SELECT * FROM DELETED)
THEN 'U' -- Set Action to Updated.
ELSE NULL -- Skip. It may have been a "failed delete".
END)
SET #OutString = ''
SELECT *
INTO #tempTrigT
FROM
(SELECT *
FROM deleted
WHERE #Action IN ( 'U', 'D')) A
UNION
(SELECT *
FROM inserted
WHERE #Action = 'I')
SET #sql = ''
if #Action = 'U'
BEGIN
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 = 'Users' and column_name <>'rowguid' and column_name <>'modifieddate'
set #ParmDefinition = '#OutString varchar(max) OUTPUT'
set #sql = 'Select #OutString = '
+ Substring(#sql,1 , len(#sql) -1) +
' From dbo.Users i '
+ ' inner join #tempTrigT d on
i.id = d.id'
exec sp_executesql #sql, #ParmDefinition, #OutString OUT
END
DECLARE #Items VARCHAR(max)
set #Items = #OutString;
DECLARE #Item VARCHAR(50)
DECLARE #Pos INT
DECLARE #Loop BIT
SELECT #Loop = CASE WHEN LEN(#OutString) > 0 THEN 1 ELSE 0 END
WHILE (SELECT #Loop) = 1
BEGIN
SELECT #Pos = CHARINDEX(',', #OutString, 1)
IF #Pos > 0
BEGIN
SELECT #Item = SUBSTRING(#OutString, 1, #Pos - 1)
SELECT #OutString = SUBSTRING(#OutString, #Pos + 1, LEN(#OutString) - #Pos)
IF (TRY_CONVERT(UNIQUEIDENTIFIER, #Item) is not null)
begin
select #Item = convert(nvarchar(50), #Item)
end
If I had to guess, I'd blame it on
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 = 'Users' and column_name <>'rowguid' and column_name <>'modifieddate'
It's because you're doing an ISNULL on a UNIQUEIDENTIIFIER column and setting the value to an INT. You could change from ISNULL(i.[ColumnName], 0) to ISNULL(i.[ColumnName], '') however, that is assuming all the columns in your tables are string applicable datatypes. Otherwise, you could do some conditional logic on the replacement value for the ISNULL function by using the [DATA_TYPE] column in that table.

Log record changes in SQL server in an audit table

The table :
CREATE TABLE GUESTS (
GUEST_ID int IDENTITY(1,1) PRIMARY KEY,
GUEST_NAME VARCHAR(50),
GUEST_SURNAME VARCHAR(50),
ADRESS VARCHAR(100),
CITY VARCHAR(50),
CITY_CODE VARCHAR(10),
COUNTRY VARCHAR(50),
STATUS VARCHAR(20),
COMMENT nvarchar(max);
For the logging :
CREATE TABLE AUDIT_GUESTS (
ID int IDENTITY(1,1) PRIMARY KEY,
GUEST_ID int,
OLD_GUEST_NAME VARCHAR(50),
NEW_GUEST_NAME VARCHAR(50),
OLD_GUEST_SURNAME VARCHAR(50),
NEW_GUEST_SURNAME VARCHAR(50),
OLD_ADRESS VARCHAR(100),
NEW_ADRESS VARCHAR(100),
OLD_CITY VARCHAR(50),
NEW_CITY VARCHAR(50),
OLD_CITY_CODE VARCHAR(10),
NEW_CITY_CODE VARCHAR(10),
OLD_COUNTRY VARCHAR(50),
NEW_COUNTRY VARCHAR(50),
OLD_STATUS VARCHAR(20),
NEW_STATUS VARCHAR(20),
OLD_COMMENT nvarchar(max),
NEW_COMMENT nvarchar(max),
AUDIT_ACTION varchar(100),
AUDIT_TIMESTAMP datetime);
I would like to create a trigger on my GUESTS table to log all changes in my AUDIT_GUESTS table. How can I do that in SQL Server 2014 Express ?
I tried :
create TRIGGER trgAfterUpdate ON [dbo].[GUESTS]
FOR UPDATE
AS
declare #GUEST_ID int;
declare #GUEST_NAME varchar(50);
declare #GUEST_SURNAME VARCHAR(50);
declare #ADRESS VARCHAR(100);
declare #CITY VARCHAR(50);
declare #CITY_CODE VARCHAR(10);
declare #COUNTRY VARCHAR(50);
declare #STATUS VARCHAR(20);
declare #COMMENT nvarchar(max);
declare #AUDIT_ACTION varchar(100);
declare #AUDIT_TIMESTAMP datetime;
select #GUEST_ID=i.GUEST_ID from inserted i;
select #GUEST_NAME=i.GUEST_NAME from inserted i;
select #GUEST_SURNAME=i.GUEST_SURNAME from inserted i;
select #ADRESS=i.ADRESS from inserted i;
select #CITY=i.CITY from inserted i;
select #CITY_CODE=i.CITY_CODE from inserted i;
select #COUNTRY=i.COUNTRY from inserted i;
select #STATUS=i.STATUS from inserted i;
select #COMMENT=i.COMMENT from inserted i;
if update(GUEST_NAME)
set #audit_action='Updated Record -- After Update Trigger.';
if update(GUEST_SURNAME)
set #audit_action='Updated Record -- After Update Trigger.';
if update(ADRESS)
set #audit_action='Updated Record -- After Update Trigger.';
if update(CITY)
set #audit_action='Updated Record -- After Update Trigger.';
if update(CITY_CODE)
set #audit_action='Updated Record -- After Update Trigger.';
if update(COUNTRY)
set #audit_action='Updated Record -- After Update Trigger.';
if update(STATUS)
set #audit_action='Updated Record -- After Update Trigger.';
if update(COMMENT)
set #audit_action='Updated Record -- After Update Trigger.';
insert into AUDIT_GUESTS
(GUEST_ID,GUEST_NAME,GUEST_SURNAME,ADRESS,CITY,CITY_CODE,COUNTRY,STATUS,COMMENT,audit_action,AUDIT_TIMESTAMP)
values(#GUEST_ID,#GUEST_NAME,#GUEST_SURNAME,#ADRESS,#CITY,#CITY_CODE,#COUNTRY,#STATUS,#COMMENT,#audit_action,getdate());
GO
Works kind of ok but I would like to see old-new values.
In SQLite I had :
CREATE TRIGGER [LOG_UPDATE]
AFTER UPDATE OF [GUEST_NAME], [GUEST_SURNAME], [ADRESS], [CITY], [CITY_CODE], [COUNTRY], [STATUS], [COMMENT]
ON [GUESTS]
BEGIN
INSERT INTO GUESTS_LOG
( GUEST_ID,
NAME_OLD,NAME_NEW,
SURNAME_OLD,SURNAME_NEW,
ADRESS_OLD,ADRESS_NEW,
CITY_OLD,CITY_NEW,
CITY_CODE_OLD,CITY_CODE_NEW,
COUNTRY_OLD,COUNTRY_NEW,
STATUS_OLD,STATUS_NEW,
COMMENT_OLD,COMMENT_NEW,sqlAction,DATE_TIME)
VALUES
(OLD.GUEST_ID,
OLD.GUEST_NAME,NEW.GUEST_NAME,
OLD.GUEST_SURNAME,NEW.GUEST_SURNAME,
OLD.ADRESS,NEW.ADRESS,
OLD.CITY,NEW.CITY,
OLD.CITY_CODE,NEW.CITY_CODE,
OLD.COUNTRY,NEW.COUNTRY,
OLD.STATUS,NEW.STATUS,
OLD.COMMENT,NEW.COMMENT,'record changed',datetime('now','localtime'));
END
and it worked OK. Just dont know how to pass this to SQL server. Just begun learning it.
Take a look at this article on Simple-talk.com by Pop Rivett. It walks you through creating a generic trigger that will log the OLDVALUE and the NEWVALUE for all updated columns. The code is very generic and you can apply it to any table you want to audit, also for any CRUD operation i.e. INSERT, UPDATE and DELETE. The only requirement is that your table to be audited should have a PRIMARY KEY (which most well designed tables should have anyway).
Here's the code relevant for your GUESTS Table.
Create AUDIT Table.
IF NOT EXISTS
(SELECT * FROM sysobjects WHERE id = OBJECT_ID(N'[dbo].[Audit]')
AND OBJECTPROPERTY(id, N'IsUserTable') = 1)
CREATE TABLE Audit
(Type CHAR(1),
TableName VARCHAR(128),
PK VARCHAR(1000),
FieldName VARCHAR(128),
OldValue VARCHAR(1000),
NewValue VARCHAR(1000),
UpdateDate datetime,
UserName VARCHAR(128))
GO
CREATE an UPDATE Trigger on the GUESTS Table as follows.
CREATE TRIGGER TR_GUESTS_AUDIT ON GUESTS 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)
--You will need to change #TableName to match the table to be audited.
-- Here we made GUESTS for your example.
SELECT #TableName = 'GUESTS'
-- date and user
SELECT #UserName = SYSTEM_USER ,
#UpdateDate = CONVERT (NVARCHAR(30),GETDATE(),126)
-- 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+'+','')
+ '''<' + COLUMN_NAME
+ '=''+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 Audit ( Type,
TableName,
PK,
FieldName,
OldValue,
NewValue,
UpdateDate,
UserName)
select ''' + #Type + ''','''
+ #TableName + ''',' + #PKSelect
+ ',''' + #fieldname + ''''
+ ',convert(varchar(1000),d.' + #fieldname + ')'
+ ',convert(varchar(1000),i.' + #fieldname + ')'
+ ',''' + #UpdateDate + ''''
+ ',''' + #UserName + ''''
+ ' 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
This is the code with two bug fixes. The first bug fix was mentioned by Royi Namir in the comment on the accepted answer to this question. The bug is described on StackOverflow at Bug in Trigger Code. The second one was found by #Fandango68 and fixes columns with multiples words for their names.
ALTER TRIGGER [dbo].[TR_person_AUDIT]
ON [dbo].[person]
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)
--You will need to change #TableName to match the table to be audited.
-- Here we made GUESTS for your example.
SELECT #TableName = 'PERSON'
SELECT #UserName = SYSTEM_USER,
#UpdateDate = CONVERT(NVARCHAR(30), GETDATE(), 126)
-- 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 + '+', '')
+ '''<[' + COLUMN_NAME
+ ']=''+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(COLUMN_NAME)
#maxfield = -- FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = #TableName
MAX(
COLUMNPROPERTY(
OBJECT_ID(TABLE_SCHEMA + '.' + #TableName),
COLUMN_NAME,
'ColumnID'
)
)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = #TableName
WHILE #field < #maxfield
BEGIN
SELECT #field = MIN(
COLUMNPROPERTY(
OBJECT_ID(TABLE_SCHEMA + '.' + #TableName),
COLUMN_NAME,
'ColumnID'
)
)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = #TableName
AND COLUMNPROPERTY(
OBJECT_ID(TABLE_SCHEMA + '.' + #TableName),
COLUMN_NAME,
'ColumnID'
) > #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 COLUMNPROPERTY(
OBJECT_ID(TABLE_SCHEMA + '.' + #TableName),
COLUMN_NAME,
'ColumnID'
) = #field
SELECT #sql =
'
insert into Audit ( Type,
TableName,
PK,
FieldName,
OldValue,
NewValue,
UpdateDate,
UserName)
select ''' + #Type + ''','''
+ #TableName + ''',' + #PKSelect
+ ',''' + #fieldname + ''''
+ ',convert(varchar(1000),d.' + #fieldname + ')'
+ ',convert(varchar(1000),i.' + #fieldname + ')'
+ ',''' + #UpdateDate + ''''
+ ',''' + #UserName + ''''
+ ' 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
I know this is old, but maybe this will help someone else.
Do not log "new" values. Your existing table, GUESTS, has the new values. You'll have double entry of data, plus your DB size will grow way too fast that way.
I cleaned this up and minimized it for this example, but here is the tables you'd need for logging off changes:
CREATE TABLE GUESTS (
GuestID INT IDENTITY(1,1) PRIMARY KEY,
GuestName VARCHAR(50),
ModifiedBy INT,
ModifiedOn DATETIME
)
CREATE TABLE GUESTS_LOG (
GuestLogID INT IDENTITY(1,1) PRIMARY KEY,
GuestID INT,
GuestName VARCHAR(50),
ModifiedBy INT,
ModifiedOn DATETIME
)
When a value changes in the GUESTS table (ex: Guest name), simply log off that entire row of data, as-is, to your Log/Audit table using the Trigger. Your GUESTS table has current data, the Log/Audit table has the old data.
Then use a select statement to get data from both tables:
SELECT 0 AS 'GuestLogID', GuestID, GuestName, ModifiedBy, ModifiedOn FROM [GUESTS] WHERE GuestID = 1
UNION
SELECT GuestLogID, GuestID, GuestName, ModifiedBy, ModifiedOn FROM [GUESTS_LOG] WHERE GuestID = 1
ORDER BY ModifiedOn ASC
Your data will come out with what the table looked like, from Oldest to Newest, with the first row being what was created & the last row being the current data. You can see exactly what changed, who changed it, and when they changed it.
Optionally, I used to have a function that looped through the RecordSet (in Classic ASP), and only displayed what values had changed on the web page. It made for a GREAT audit trail so that users could see what had changed over time.
From SQL Server 2016 and onwards there's built-in support for this: use System-Versioned Temporal Tables
In both Shiva's and dwilli's answer, instead of defining the table name statically (i.e. SELECT #TableName = 'PERSON'), here is a way to get it dynamically:
SELECT #TableName = OBJECT_NAME(parent_object_id) FROM sys.objects
WHERE sys.objects.name = OBJECT_NAME(##PROCID)
Clarification: This is not my code, i have found it somewhere on the internet, it was quite a while ago though, so i can't remember where. Also, since i can't comment here, i'm posting this as an answer.
There are some great answers here to turn the source code into a better experience for the community. If any of you are like me and are looking for the full set of code with test tables ready for copy, paste (RAW on GitHub), and execute I've assembled it below:
/*this is a journey...
Source links:
https://www.red-gate.com/simple-talk/databases/sql-server/database-administration-sql-server/pop-rivetts-sql-server-faq-no-5-pop-on-the-audit-trail/
https://stackoverflow.com/questions/19737723/log-record-changes-in-sql-server-in-an-audit-table
https://chat.stackexchange.com/transcript/message/34774768#34774768
http://jsbin.com/lafayiluri/1/edit?html,output
*/
-- #region | Set up the tables
/*Firstly, we create the audit table.
There will only need to be one of these in a database*/
DROP TABLE IF EXISTS dbo.audit_trigger;
CREATE TABLE dbo.audit_trigger (
Type CHAR(1),
TableName VARCHAR(128),
PK VARCHAR(1000),
FieldName VARCHAR(128),
OldValue VARCHAR(1000),
NewValue VARCHAR(1000),
UpdateDate datetime,
UserName VARCHAR(128)
);
GO
/*now we will illustrate the use of this tool
by creating a dummy test table called TrigTest.*/
DROP TABLE IF EXISTS dbo.trigtest;
CREATE TABLE trigtest (
i INT NOT NULL,
j INT NOT NULL,
s VARCHAR(10),
t VARCHAR(10)
);
GO
/*note that for this system to work there must be a primary key
to the table but then a table without a primary key
isn’t really a table is it?*/
ALTER TABLE trigtest ADD CONSTRAINT pk PRIMARY KEY (i, j);
GO
-- #endregion
/*and now create the trigger itself. This has to be created for every
table you want to monitor*/
-- #region | trigger with bug fix
DROP TRIGGER IF EXISTS dbo.tr_audit_trigtest;
GO
CREATE TRIGGER tr_audit_trigtest ON dbo.trigtest
FOR
/*uncomment INSERT if you want. The insert data is on the source table
but sometimes your end users wanna see ALL the data in the audit table
and hey, storage is cheap-ish now /shrug
*/
-- INSERT,
UPDATE,
DELETE
AS
SET NOCOUNT ON;
/*declare all the variables*/
DECLARE #bit INT;
DECLARE #field INT;
DECLARE #maxfield INT;
DECLARE #char INT;
DECLARE #fieldname VARCHAR(128);
DECLARE #TableName VARCHAR(128);
DECLARE #PKCols VARCHAR(1000);
DECLARE #sql VARCHAR(2000);
DECLARE #UpdateDate VARCHAR(21);
DECLARE #UserName VARCHAR(128);
DECLARE #Type CHAR(1);
DECLARE #PKSelect VARCHAR(1000);
/*now set some of these variables*/
SET #TableName = (
SELECT
OBJECT_NAME(parent_object_id)
FROM sys.objects
WHERE
sys.objects.name = OBJECT_NAME(##PROCID)
);
SET #UserName = SYSTEM_USER;
SET #UpdateDate = CONVERT(NVARCHAR(30), GETDATE(), 126);
/*Action*/
IF EXISTS (SELECT * FROM INSERTED)
IF EXISTS (SELECT * FROM DELETED)
SET #Type = 'U'
ELSE SET #Type = 'I'
ELSE SET #Type = 'D'
;
/*get list of columns*/
SELECT *
INTO #ins
FROM INSERTED;
SELECT *
INTO #del
FROM DELETED;
/*set #PKCols and #PKSelect via SELECT statement.*/
SELECT #PKCols = /*Get primary key columns for full outer join*/
COALESCE(#PKCols + ' and', ' on')
+ ' i.[' + c.COLUMN_NAME + '] = d.[' + c.COLUMN_NAME + ']'
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS pk
INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS c ON (
c.TABLE_NAME = pk.TABLE_NAME
AND c.CONSTRAINT_NAME = pk.CONSTRAINT_NAME
)
WHERE pk.TABLE_NAME = #TableName
AND CONSTRAINT_TYPE = 'PRIMARY KEY'
;
SELECT #PKSelect = /*Get primary key select for insert*/
COALESCE(#PKSelect + '+', '')
+ '''<[' + COLUMN_NAME + ']=''+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
SET #field = 0;
SET #maxfield = (
SELECT
MAX(
COLUMNPROPERTY(
OBJECT_ID(TABLE_SCHEMA + '.' + #TableName),
COLUMN_NAME,
'ColumnID'
)
)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE
TABLE_NAME = #TableName
);
WHILE #field < #maxfield
BEGIN
SET #field = (
SELECT
MIN(
COLUMNPROPERTY(
OBJECT_ID(TABLE_SCHEMA + '.' + #TableName),
COLUMN_NAME,
'ColumnID'
)
)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE
TABLE_NAME = #TableName
AND COLUMNPROPERTY(
OBJECT_ID(TABLE_SCHEMA + '.' + #TableName),
COLUMN_NAME,
'ColumnID'
) > #field
);
SET #bit = (#field - 1)% 8 + 1;
SET #bit = POWER(2, #bit - 1);
SET #char = ((#field - 1) / 8) + 1;
IF (
SUBSTRING(COLUMNS_UPDATED(), #char, 1) & #bit > 0
OR #Type IN ('I', 'D')
)
BEGIN
SET #fieldname = (
SELECT
COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE
TABLE_NAME = #TableName
AND COLUMNPROPERTY(
OBJECT_ID(TABLE_SCHEMA + '.' + #TableName),
COLUMN_NAME,
'ColumnID'
) = #field
);
SET #sql = ('
INSERT INTO audit_trigger (
Type,
TableName,
PK,
FieldName,
OldValue,
NewValue,
UpdateDate,
UserName
)
SELECT '''
+ #Type + ''','''
+ #TableName + ''','
+ #PKSelect + ','''
+ #fieldname + ''''
+ ',convert(varchar(1000),d.' + #fieldname + ')'
+ ',convert(varchar(1000),i.' + #fieldname + ')'
+ ',''' + #UpdateDate + ''''
+ ',''' + #UserName + '''' +
' FROM #ins AS i FULL OUTER JOIN #del AS 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
SET NOCOUNT OFF;
GO
-- #endregion
-- #region | now we can test the trigger out
INSERT trigtest SELECT 1,1,'hi', 'bye';
INSERT trigtest SELECT 2,2,'hi', 'bye';
INSERT trigtest SELECT 3,3,'hi', 'bye';
SELECT * FROM dbo.audit_trigger;
SELECT * FROM trigtest;
UPDATE trigtest SET s = 'hibye' WHERE i <> 1;
UPDATE trigtest SET s = 'bye' WHERE i = 1;
UPDATE trigtest SET s = 'bye' WHERE i = 1;
UPDATE trigtest SET t = 'hi' WHERE i = 1;
SELECT * FROM dbo.audit_trigger;
SELECT * FROM dbo.trigtest;
DELETE dbo.trigtest;
SELECT * FROM dbo.audit_trigger;
SELECT * FROM dbo.trigtest;
GO
DROP TABLE dbo.audit_trigger;
GO
DROP TABLE dbo.trigtest ;
GO
-- #endregion
Hey It's very simple see this
#OLD_GUEST_NAME = d.GUEST_NAME from deleted d;
this variable will store your old deleted value and then you can insert it where you want.
for example-
Create trigger testupdate on test for update, delete
as
declare #tableid varchar(50);
declare #testid varchar(50);
declare #newdata varchar(50);
declare #olddata varchar(50);
select #tableid = count(*)+1 from audit_test
select #testid=d.tableid from inserted d;
select #olddata = d.data from deleted d;
select #newdata = i.data from inserted i;
insert into audit_test (tableid, testid, olddata, newdata) values (#tableid, #testid, #olddata, #newdata)
go

Ignore Two Columns in Update Trigger

I am using the code below in a trigger. I have two columns that are updated by a windows service every 60 minutes in the table I am monitoring. How can I ignore the two columns from writing to the audit table.
ALTER trigger [dbo].[tMonitors_ChangeTracking] on [dbo].[tMonitors] for insert, update, delete
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),
#PKFieldSelect varchar(1000),
#PKValueSelect varchar(1000)
select #TableName = 'tMonitors'
-- date and user
select #UserName = system_user ,
#UpdateDate = convert(varchar(8), getdate(), 112) + ' ' + convert(varchar(12), getdate(), 114)
-- 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 fields select for insert
select #PKFieldSelect = coalesce(#PKFieldSelect+'+','') + '''' + 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
select #PKValueSelect = coalesce(#PKValueSelect+'+','') + '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 tMonitors_Audit (Type, TableName, PrimaryKeyField, PrimaryKeyValue, FieldName, OldValue, NewValue, UpdateDate, UserName)'
select #sql = #sql + ' select ''' + #Type + ''''
select #sql = #sql + ',''' + #TableName + ''''
select #sql = #sql + ',' + #PKFieldSelect
select #sql = #sql + ',' + #PKValueSelect
select #sql = #sql + ',''' + #fieldname + ''''
select #sql = #sql + ',convert(varchar(1000),d.' + #fieldname + ')'
select #sql = #sql + ',convert(varchar(1000),i.' + #fieldname + ')'
select #sql = #sql + ',''' + #UpdateDate + ''''
select #sql = #sql + ',''' + #UserName + ''''
select #sql = #sql + ' from #ins i full outer join #del d'
select #sql = #sql + #PKCols
select #sql = #sql + ' where i.' + #fieldname + ' <> d.' + #fieldname
select #sql = #sql + ' or (i.' + #fieldname + ' is null and d.' + #fieldname + ' is not null)'
select #sql = #sql + ' or (i.' + #fieldname + ' is not null and d.' + #fieldname + ' is null)'
exec (#sql)
end
end
The simple approach, assuming these columns to ignore are only updated during this hourly process:
IF NOT (UPDATE(col_to_ignore_1) AND UPDATE(col_to_ignore_2))
BEGIN
...
EXEC(#sql);
END
Otherwise you can build a similar list based on INFORMATION_SCHEMA.COLUMNS, so that the logging only happens when at least one of the other columns is updated, that would end up like this:
IF (UPDATE(col_to_audit_1) OR UPDATE(col_to_audit_2) ... )
BEGIN
...
EXEC(#sql);
END
It is not clear from your question whether you want to ignore these two columns always, or only when this hourly process runs. Perhaps another idea is to use a transaction and disable the trigger for this update... it will obviously make the trigger much less complicated.
You can specify which columns you want to pay attention to by using the "UPDATE(column_name)" syntax, like so:
CREATE TRIGGER trigger_name ON tablename
FOR insert, update, delete
AS
SET NOCOUNT ON
IF ( UPDATE(Column1) OR UPDATE(Column2))
BEGIN
--your sql here
END
Other options are available at the official documentation.
You can access the complete list of columns changed using COLUMNS_UPDATED() and compare it to pre-defined bit patterns. For tables with many columns, calculating the bit positions can be easier than listing out UPDATED(every) OR UPDATED(other) OR UPDATED(column).
For example, for a table:
CREATE TABLE Users
(
UserID INT NOT NULL PRIMARY KEY IDENTITY,
Name nvarchar(255) NOT NULL,
EmailAddress nvarchar(255) NOT NULL,
LastPageViewDate datetime NULL,
GroupID INT NOT NULL
);
where you want to ignore updates to LastPageViewDate. You can determine the bitmask via:
SELECT name, column_id FROM sys.columns WHERE object_id = OBJECT_ID('dbo.Users')
name column_id
-------------------- -----------
UserID 1
Name 2
EmailAddress 3
LastPageViewDate 4
GroupID 5
The trigger can then check for the little-endian bitmask 0x08 ignoring trailing 0's:
CREATE TRIGGER dbo.TR_IUD_Users_LogEdit
ON dbo.Users
AFTER INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON;
-- Ignore page view logging caused by
-- UPDATE Users SET LastPageViewDate = GETDATE() WHERE UserID = #UserID
-- Bit 4 == LastPageViewDate
IF COLUMNS_UPDATED() = 0x08 RETURN;
-- For forward compatibility, you can ignore unchanged new columns added
-- by RTRIM'ing the '0's (required since 0x08 != 0x0800)
--
--IF REPLACE(RTRIM(REPLACE(CONVERT(varchar(max), COLUMNS_UPDATED(), 2), '0', ' ')), ' ', '0') IN ('08')
-- RETURN;
/* Snipped trigger body */
END
GO

Creating A Script To Replicate A Table And Its Contents?

I know you can create a script to replicate a table using:
right click table > script table as > create to > new query editor window
But how can I generate a script that contains a bunch of insert commands for each row in the table?
Table1
Id1, Row1
Id2, Row2
Id3, Row3
Insert into Table1 values(Row1);
Insert into Table1 values(Row2);
Insert into Table1 values(Row3);
I ended up doing this
right click database > Tasks > Generate Scripts ... > selected the tables > in the advanced options I set "Types of data to script" to "Schema and data"
Select
'Insert into Table (
IntField1
StringField2
Column3)
values (' +
IntField1 + ',' +
+ '''' + StringField2 + ''',' +
Column2 + ')' as InsertQuery
From Table
Something like this, just remember if your string contains a single quote you will need to make sure you replace it like this replace(stringfield, '''', '''''')
So this isnt super pretty cuz I kind of took one of my sp's and hacked it up for this. But basically this will take any table and print a series of insert statements into a table called tbl_text (which you would need to create)
The arguments are the table name and the table ID from sysobjects
--this is how you get the tbl_id
SELECT id FROM sysobjects WHERE type = 'U' AND name = 'tablename'
CREATE PROCEDURE dbo.sp_export_table
#tblhdr varchar(100),
#tblID varchar(100)
AS
SET NOCOUNT ON
IF object_id('tempdb..##temptable') IS NOT NULL
BEGIN
DROP TABLE ##temptable
END
DECLARE #identity bit
DECLARE #typestmt nvarchar(100)
DECLARE #typeval int
DECLARE #rowstmt nvarchar(1000)
DECLARE #rowID varchar(50)
DECLARE #orderby nvarchar(100)
DECLARE #clmnstmt varchar(200)
DECLARE #clmnhdr varchar(50)
DECLARE #clmnstring varchar(1000)
DECLARE #valuestmt nvarchar(200)
DECLARE #valuestring nvarchar(3000)
DECLARE #value nvarchar(1000)
DECLARE #insertstmt varchar(1000)
DECLARE #params nvarchar(100)
DECLARE #param2 nvarchar(100)
SELECT #rowstmt = N'SELECT TOP 1 #inside_var = name FROM syscolumns WHERE id = ' + #tblID + ' ORDER BY colorder'
SELECT #params = N'#inside_var NVARCHAR(1000) OUTPUT'
EXEC sp_executesql #rowstmt, #params, #inside_var = #orderby OUTPUT
SELECT #rowstmt = 'SELECT *, ROW_NUMBER() OVER (ORDER BY ' + #orderby + ') AS row INTO ##temptable FROM ' + #tblhdr
exec(#rowstmt)
IF object_id('tempdb..##temptable') IS NOT NULL
BEGIN
DECLARE row_cursor CURSOR FOR
SELECT row FROM ##temptable
OPEN row_cursor
FETCH NEXT FROM row_cursor
INTO #rowID
--if table has identity and has records write identity_insert on
SET #identity = 0
IF EXISTS(SELECT * FROM INFORMATION_SCHEMA.TABLES
WHERE OBJECTPROPERTY(OBJECT_ID(TABLE_NAME),
'TableHasIdentity') = 1 AND TABLE_TYPE = 'BASE TABLE'
AND TABLE_NAME = #tblhdr) AND EXISTS(SELECT * FROM ##temptable)
BEGIN
SET #identity = 1
INSERT INTO dbo.tbl_text VALUES('SET IDENTITY_INSERT dbo.' + #tblhdr + ' ON')
END
WHILE ##FETCH_STATUS = 0
BEGIN
SELECT #clmnstmt = 'DECLARE column_cursor CURSOR FOR SELECT name FROM syscolumns WHERE id = ' + #tblID + ' ORDER BY colorder'
exec(#clmnstmt)
OPEN column_cursor
FETCH NEXT FROM column_cursor
INTO #clmnhdr
SELECT #clmnstring = '('
SELECT #valuestring = '('
WHILE ##FETCH_STATUS = 0
BEGIN
IF #clmnhdr <> 'row'
BEGIN
SELECT #clmnstring = #clmnstring + #clmnhdr + ','
SELECT #valuestmt = N'SELECT #inside_var = ' + #clmnhdr + ' FROM ##temptable WHERE row = ' + #rowID
EXEC sp_executesql #valuestmt, #params, #inside_var = #value OUTPUT
SELECT #typestmt = N'SELECT #inside_var2 = xtype FROM syscolumns WHERE name = ''' + #clmnhdr + ''' AND id = ' + #tblID
SELECT #param2 = N'#inside_var2 INT OUTPUT'
EXEC sp_executesql #typestmt, #param2, #inside_var2 = #typeval OUTPUT
IF #typeval NOT IN (48,52,56,59,60,62,104,108,122,127)
BEGIN
SET #value = REPLACE(#value,'''','''''')
SET #value = '''' + #value + ''''
SET #value = ISNULL(#value, '''''')
END
IF NOT (#typeval = 34)
BEGIN
SELECT #valuestring = #valuestring + #value + ','
END
ELSE
BEGIN
SELECT #valuestring = #valuestring + '''''' + ','
END
END
FETCH NEXT FROM column_cursor
INTO #clmnhdr
END
SET #clmnstring = LEFT(#clmnstring, LEN(#clmnstring) - 1)
SET #valuestring = LEFT(#valuestring, LEN(#valuestring) - 1)
INSERT INTO dbo.tbl_text VALUES('INSERT INTO dbo.' + #tblhdr + ' ' + #clmnstring + ') VALUES' + #valuestring + ')')
FETCH NEXT FROM row_cursor
INTO #rowID
CLOSE column_cursor
DEALLOCATE column_cursor
END
--if it wrote identity_insert on, turn it off
IF (#identity = 1)
BEGIN
INSERT INTO dbo.tbl_text VALUES('SET IDENTITY_INSERT dbo.' + #tblhdr + ' OFF')
END
CLOSE row_cursor
DEALLOCATE row_cursor
END
IF object_id('tempdb..##temptable') IS NOT NULL
BEGIN
DROP TABLE ##temptable
END
GO
If you've got an account on SSC, you can use the script I published last year. It works without cursors an it enables custom filtering.
http://www.sqlservercentral.com/scripts/Script+Data/65998/
Hope this helps
Assuming Row is an INT NOT NULL. You could write a SELECT statement that outputs SQL;
SELECT N'INSERT INTO Table1 VALUES (' + CAST(Row AS NVARCHAR(10)) + N');'
FROM Table1
Then output your results to text.