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

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.

Related

Need all columns on one row

I wrote the following query:
IF OBJECT_ID ('tempdb..#ColumnsType') IS NOT NULL DROP TABLE #ColumnsType
DECLARE #vQuery NVARCHAR(MAX) =''
IF OBJECT_ID ('tempdb..#random') IS NOT NULL DROP TABLE #random
CREATE TABLE #random (
ColumnID INT PRIMARY KEY IDENTITY(1,1) NOT NULL
, randomname VARCHAR(50)
, randomvalue INT)
INSERT INTO #random (randomname, randomvalue)
VALUES ('a3', 123)
, ('bla', 4325)
, ('another_bla', 5643)
, ('end_here', 3)
select *
from #random
CREATE TABLE #ColumnsType (
ColumnID INT PRIMARY KEY IDENTITY(1,1) NOT NULL
, ColumnName sysname
, DataType sysname
)
INSERT INTO #ColumnsType (ColumnName, DataType)
SELECT [name],
system_type_id
FROM Tempdb.Sys.Columns
WHERE Object_ID = Object_ID('tempdb..#random')
AND system_type_id = 56
DECLARE #i INT = (SELECT MIN(ColumnID) FROM #random);
DECLARE #maxId INT = (SELECT MAX(ColumnID) FROM #random);
DECLARE #ColumnName VARCHAR(200);
DECLARE #DataType VARCHAR(200);
WHILE #i <= #maxId
BEGIN
SET #ColumnName = (SELECT ColumnName FROM #ColumnsType WHERE ColumnId = #i)
-- SET #DataType = (SELECT DataType FROM #ColumnsType WHERE ColumnId = #i)
SELECT #vQuery =
'SELECT
MIN(TRY_CONVERT(NUMERIC(30, 4), ' +#ColumnName+ ')) AS ' +#ColumnName+ '_MinValue
, MAX(TRY_CONVERT(NUMERIC(30, 4), ' +#ColumnName+ ')) AS ' +#ColumnName+ '_MaxValue
, AVG(TRY_CONVERT(NUMERIC(30, 4), ' +#ColumnName+ ')) AS ' +#ColumnName + '_AvgValue
, STDEV(TRY_CONVERT(NUMERIC(30, 4), ' +#ColumnName+ ')) AS ' +#ColumnName+ '_StandardDeviation
, SUM(TRY_CONVERT(NUMERIC(30, 4), ' +#ColumnName+ ')) AS ' +#ColumnName+ '_TotalSum
FROM tempdb..#random' -- +#Schema+'.'+#Table+ ''
EXEC sp_executesql #vQuery
PRINT #vQuery
SET #i = #i + 1
END
For the sake of demonstration I create temp table with random values. I perform profiling on part of the columns which are consisting only of numeric values. To filter the columns I get their names and filter by type, using Tempdb.Sys.Columns. In normal case with my original data, I use INFORMATION_SCHEMA.COLUMNS but I think this is not that important.
The query returns the following:
The result is presented on two rows. What I'd like to do is to have this result on one row. The idea is to pivot the one row result after and to receive the following result:
As I mentioned, you need to not use a loop, use a set based method and UNION ALL your dynamic statements. I assume here, as well, that you are using a recent version of SQL Server. If not, you'll need to replace STRING_AGG with the old FOR XML PATH (and STUFF) method.
This should be enough to get you started:
USE Sandbox;
GO
CREATE TABLE dbo.YourTable (Col1 int,
Col2 varchar(10));
GO
DECLARE #SchemaName sysname = N'dbo',
#TableName sysname = N'YourTable';
DECLARE #SQL nvarchar(MAX),
#CRLF nchar(2) = NCHAR(13) + NCHAR(10);
DECLARE #Delimiter nvarchar(50) = #CRLF + N'UNION ALL' + #CRLF;
SELECT #SQL = STRING_AGG(CONVERT(nvarchar(MAX),N'SELECT MIN(') + QUOTENAME(c.[name]) + N') AS ' + QUOTENAME(c.[name] + N'_MIN') + N',' + #CRLF +
N' MAX(' + QUOTENAME(c.[name]) + N') AS ' + QUOTENAME(c.[name] + N'_MAX') + #CRLF +
N'FROM ' + QUOTENAME(s.[name]) + N'.' + QUOTENAME(t.[name])
,#Delimiter) WITHIN GROUP (ORDER BY c.column_id)
FROM sys.schemas s
JOIN sys.tables t ON s.schema_id = t.schema_id
JOIN sys.columns c ON t.object_id = c.object_id
WHERE s.[name] = #SchemaName
AND t.[name] = #TableName
PRINT #SQL;
EXEC sys.sp_executesql #SQL;
GO
DROP TABLE dbo.YourTable;
Here is a very clean and nice solution which is what I was looking for:
I choose the columns, from specific table, scan it once and perform multiple calculations on it
I use dynamic query and make it one statement for all columns.
It works really fast. It took a little over 5 minutes to return result for table with 50 mil rows.
The only thing that is left to do is to UNPIVOT in order to insert results in a table that I want.
DECLARE
#q1 NVARCHAR(MAX)
, #q2 NVARCHAR(MAX)
, #q3 NVARCHAR(500)
, #schema VARCHAR(50) = '' -- choose schema
, #table VARCHAR(200) = '' -- choose table
SET #Q1 = 'SELECT ' + '''' + #table + '''' + ' as tableName, '
SET #Q3 = ' FROM ' + #schema + '.' + #table
SELECT #q2 = COALESCE(#q2 + ', ', '')
+ ' max(' + columnName + ') as ' + columnName + '_max, '
+ ' min(' + columnName + ') as ' + columnName + '_min, '
+ ' avg(' + columnName + ') as ' + columnName + '_avg, '
+ ' stdev(' + columnName + ') as ' + columnName + '_stdev, '
+ ' sum(' + columnName + ') as ' + columnName + '_sum '
FROM (
SELECT s.[name] as schemaName, t.[name] as tableName, c.[name] as columnName, st.[name] as typeName
FROM sys.schemas s
INNER JOIN sys.tables t ON s.schema_id = t.schema_id
INNER JOIN sys.columns c ON t.object_id = c.object_id
INNER JOIN sys.types st ON st.user_type_id = c.user_type_id
WHERE 1=1
AND s.[name] = #schema
AND t.[name] = #table
AND st.[name] IN ('') -- choose columns of specific data type, that you want to profile
) data
SELECT #q1 = #q1 + #q2 + #q3
EXEC sys.sp_executesql #Q1
Enjoy!

Get all UNIQUEIDENTIFIER values based on variables in query

Goal is to get all UNIQUEIDENTIFIER values from all columns in database.
Code which is supposed to load all those values:
DECLARE #TableNames TABLE
(
ID INT NOT NULL IDENTITY(0, 1),
TableName NVARCHAR(50) NOT NULL,
ColName NVARCHAR(50) NOT NULL
);
DECLARE #Guids TABLE
(
ID INT NOT NULL IDENTITY(0, 1),
FoundGuid UNIQUEIDENTIFIER NOT NULL
);
DECLARE #Local NVARCHAR(50);
WHILE #Counter < 500
BEGIN
SELECT #Local = TableName FROM #TableNames WHERE Id = #Counter;
INSERT INTO #Guids EXEC('SELECT Id FROM [' + #Local + ']');
SET #Counter = #Counter + 1;
END;
Is this safe thing to do so? Eventually, what is the way to get those values?
I would use the system views to generate dynamic sql. This is 100% accurate and not limited to only those columns named Id. It won't matter what schema or column name is used. This will get you all those values with no looping at all.
declare #SQL nvarchar(max) = ''
select #SQL = #SQL + 'select ' + QUOTENAME(c.name) + ' = ' + QUOTENAME(c.name)
+ ' FROM ' + QUOTENAME(s.name) + '.' + QUOTENAME(t.name)
+ ' UNION ALL '
from sys.tables t
join sys.columns c on c.object_id = t.object_id
join sys.types ty on ty.user_type_id = c.user_type_id
join sys.schemas s on s.schema_id = t.schema_id
where ty.name = 'uniqueidentifier'
--removes the last UNION ALL
select #SQL = left(#SQL, len(#SQL) - 10)
select #SQL
--uncomment below to execute the dynamic sql when you are comfortable it is correct
--exec sp_executesql #SQL

Search all databases for value

Consider have a table like named : People
| Id | Name | Code |
| 1 | John | 857 |
| 2 | Mike | 893 |
| 3 | Sara | 935 |
This table is in PeopleDb Database
Now I want to find 'Mike' keyword. Situation :
I don't know to search in which database, tables.
I need a query that searches in all databases and tables and shows me this :
| Id | DatabaseName | TableName | ColumnName | Pk | SearchValue
| 1 | 'PeopleDb' | 'People' | 'Name' | 2 | 'Mike'
I don't know how to write the query to search in all databases and tables.
Any help will be greatly appreciated.
Edit :
Speed is not an issue here and I need to do this.
I tried this query, I want the same but that searches all databases.
CREATE PROC SearchAllTables
(
#SearchStr nvarchar(100)
)
AS
BEGIN
-- Copyright © 2002 Narayana Vyas Kondreddi.All rights reserved.
-- Purpose: To search all columns of all tables for a given search string
-- Written by: Narayana Vyas Kondreddi
-- Site: http://vyaskn.tripod.com
-- Tested on: SQL Server 7.0 and SQL Server 2000
-- Date modified: 28th July 2002 22:50 GMT
DECLARE #Results TABLE(ColumnName nvarchar(370), ColumnValue nvarchar(3630))
SET NOCOUNT ON
DECLARE #TableName nvarchar(256), #ColumnName nvarchar(128), #SearchStr2 nvarchar(110)
SET #TableName = ''
SET #SearchStr2 = QUOTENAME('%' + #SearchStr + '%', '''')
WHILE #TableName IS NOT NULL
BEGIN
SET #ColumnName = ''
SET #TableName =
(
SELECT MIN(QUOTENAME(TABLE_SCHEMA) + '.' + QUOTENAME(TABLE_NAME))
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_TYPE = 'BASE TABLE'
AND QUOTENAME(TABLE_SCHEMA) + '.' + QUOTENAME(TABLE_NAME) > #TableName
AND OBJECTPROPERTY(
OBJECT_ID(
QUOTENAME(TABLE_SCHEMA) + '.' + QUOTENAME(TABLE_NAME)
), 'IsMSShipped'
) = 0
)
WHILE(#TableName IS NOT NULL) AND(#ColumnName IS NOT NULL)
BEGIN
SET #ColumnName =
(
SELECT MIN(QUOTENAME(COLUMN_NAME))
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = PARSENAME(#TableName, 2)
AND TABLE_NAME = PARSENAME(#TableName, 1)
AND DATA_TYPE IN ('char', 'varchar', 'nchar', 'nvarchar')
AND QUOTENAME(COLUMN_NAME) > #ColumnName
)
IF #ColumnName IS NOT NULL
BEGIN
INSERT INTO #Results
EXEC
(
'SELECT ''' + #TableName + '.' + #ColumnName + ''', LEFT(' + #ColumnName + ', 3630)
FROM ' + #TableName + ' (NOLOCK) ' +
' WHERE ' + #ColumnName + ' LIKE ' + #SearchStr2
)
END
END
END
SELECT ColumnName, ColumnValue FROM #Results
END
Update :
I need a working T-Sql that searches all databases, tables, columns, all type of variables.
Current answers will not work in some situations like Connection String in nvarchar field.
I have updated your Logic as per below and it works fine, please have a look:
DECLARE #SearchStr VARCHAR(50)='Surat'
DECLARE #Results TABLE(DatabaseName NVARCHAR(500), TableName nvarchar(370),ColumnName nvarchar(370), ColumnValue nvarchar(3630),PrimaryKey nvarchar(200), PrimaryKeyValue nvarchar(4000))
SET NOCOUNT ON
DECLARE #TableList AS Table
(
TableName VARCHAR(500),
RowNo INT
)
DECLARE #ColumnList AS Table
(
ColumnName VARCHAR(500),
RowNo INT
)
DECLARE #PrimaryKeyList AS Table
(
PrimaryKeyName VARCHAR(500)
)
DECLARE #TableName nvarchar(256), #ColumnName nvarchar(128), #SearchStr2 nvarchar(110), #PrimaryKey nvarchar(200), #CurrentTableName nvarchar(256)
SET #TableName = ''
SET #SearchStr2 = QUOTENAME('%' + #SearchStr + '%', '''')
DECLARE #DatabaseCount INT=0, #Index INT=0, #DatabaseName NVARCHAR(500), #TotalColumnCount INT, #ColumnIndex INT=0, #TotalTableCount INT, #TableIndex INT=0
SELECT
*,
ROW_NUMBER() OVER (ORDER BY name) AS RowNo
INTO #tblDatabases
FROM Sys.Databases
WHERE name NOT IN ('master','model','msdb','tempdb')
SELECT #DatabaseCount=COUNT (*) FROM #tblDatabases
WHILE #Index<#DatabaseCount
BEGIN
SET #Index=#Index+1
SELECT #DatabaseName='',#TableIndex=0,#ColumnIndex=0,#TableName='',#ColumnName=''
SELECT #DatabaseName=name FROM #tblDatabases WHERE RowNo=#Index
DELETE FROM #TableList
INSERT INTO #TableList
EXEC('
SELECT QUOTENAME(TABLE_SCHEMA) + ''.'' + QUOTENAME(TABLE_NAME),
ROW_NUMBER() OVER (ORDER BY TABLE_NAME)
FROM ['+#DatabaseName+'].INFORMATION_SCHEMA.TABLES
WHERE TABLE_TYPE = ''BASE TABLE''
')
SELECT #TotalTableCount=COUNT(*) FROM #TableList
WHILE #TableIndex<#TotalTableCount
BEGIN
SET #TableIndex=#TableIndex+1
SELECT #ColumnName = '',#ColumnIndex=0
SELECT #TableName=TableName FROM #TableList WHERE RowNo=#TableIndex
SET #CurrentTableName=REPLACE(REPLACE(REPLACE(#TableName,'[dbo].',''),'[',''),']','')
DELETE FROM #ColumnList
INSERT INTO #ColumnList
EXEC('SELECT
COLUMN_NAME,ROW_NUMBER() OVER (ORDER BY COLUMN_NAME)
FROM ['+#DatabaseName+'].INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = PARSENAME('''+#TableName+''', 2)
AND TABLE_NAME = PARSENAME('''+#TableName+''', 1)
AND DATA_TYPE IN (''char'', ''varchar'', ''nchar'', ''nvarchar'')
')
SELECT #TotalColumnCount=COUNT(*) FROM #ColumnList
WHILE #ColumnIndex<#TotalColumnCount
BEGIN
SET #ColumnIndex=#ColumnIndex+1
SET #ColumnName=''
SELECT #ColumnName=ColumnName FROM #ColumnList WHERE RowNo=#ColumnIndex
DELETE FROM #PrimaryKeyList
INSERT INTO #PrimaryKeyList
EXEC('
SELECT Col.Column_Name from
['+#DatabaseName+'].INFORMATION_SCHEMA.TABLE_CONSTRAINTS Tab,
['+#DatabaseName+'].INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE Col
WHERE
Col.Constraint_Name = Tab.Constraint_Name
AND Col.Table_Name = Tab.Table_Name
AND Constraint_Type = ''PRIMARY KEY''
AND Col.Table_Name= '''+#CurrentTableName+'''
')
SELECT #PrimaryKey=''
SELECT #PrimaryKey=PrimaryKeyName FROM #PrimaryKeyList
SET #PrimaryKey=ISNULL(#PrimaryKey,'')
IF #ColumnName IS NOT NULL AND #PrimaryKey<>''
BEGIN
INSERT INTO #Results
EXEC
(
'SELECT '''+#DatabaseName+''','''+#CurrentTableName+''',''' + #ColumnName + ''', LEFT(' + #ColumnName + ', 3630) , '''+#PrimaryKey+''', [' + #PrimaryKey + ']
FROM ['+#DatabaseName+'].' + #TableName + ' (NOLOCK) ' +
' WHERE ' + #ColumnName + ' LIKE ' + #SearchStr2
)
END
END
END
END
SELECT ROW_NUMBER() OVER (ORDER BY DatabaseName) AS Id,DatabaseName,TableName,ColumnName, ColumnValue AS SearchValue,PrimaryKeyValue AS Pk, PrimaryKey FROM #Results
DROP TABLE #tblDatabases
SELECT * FROM all_objects where object_name = 'XXXX';
you can use this Store Procedure
CREATE PROCEDURE dbo.SearchAllDatabases
#SearchTerm NVARCHAR(255) = NULL
AS
BEGIN
SET NOCOUNT ON;
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
IF #SearchTerm IS NULL OR #SearchTerm NOT LIKE N'%[^%^_]%'
BEGIN
RAISERROR(N'Please enter a valid search term.', 11, 1);
RETURN;
END
CREATE TABLE #results
(
[database] SYSNAME,
[schema] SYSNAME,
[table] SYSNAME,
[column] SYSNAME,
ExampleValue NVARCHAR(1000)
);
DECLARE
#DatabaseCommands NVARCHAR(MAX) = N'',
#ColumnCommands NVARCHAR(MAX) = N'';
SELECT #DatabaseCommands = #DatabaseCommands + N'
EXEC ' + QUOTENAME(name) + '.sys.sp_executesql
#ColumnCommands, N''#SearchTerm NVARCHAR(MAX)'', #SearchTerm;'
FROM sys.databases
WHERE database_id > 4 -- non-system databases
AND [state] = 0 -- online
AND user_access = 0; -- multi-user
SET #ColumnCommands = N'DECLARE #q NCHAR(1),
#SearchCommands NVARCHAR(MAX);
SELECT #q = NCHAR(39),
#SearchCommands = N''DECLARE #VSearchTerm VARCHAR(255) = #SearchTerm;'';
SELECT #SearchCommands = #SearchCommands + CHAR(10) + N''
SELECT TOP (1)
[db] = DB_NAME(),
[schema] = N'' + #q + s.name + #q + '',
[table] = N'' + #q + t.name + #q + '',
[column] = N'' + #q + c.name + #q + '',
ExampleValue = LEFT('' + QUOTENAME(c.name) + '', 1000)
FROM '' + QUOTENAME(s.name) + ''.'' + QUOTENAME(t.name) + ''
WHERE '' + QUOTENAME(c.name) + N'' LIKE #'' + CASE
WHEN c.system_type_id IN (35, 167, 175) THEN ''V''
ELSE '''' END + ''SearchTerm;''
FROM sys.schemas AS s
INNER JOIN sys.tables AS t
ON s.[schema_id] = t.[schema_id]
INNER JOIN sys.columns AS c
ON t.[object_id] = c.[object_id]
WHERE c.system_type_id IN (35, 99, 167, 175, 231, 239)
AND c.max_length >= LEN(#SearchTerm);
PRINT #SearchCommands;
EXEC sys.sp_executesql #SearchCommands,
N''#SearchTerm NVARCHAR(255)'', #SearchTerm;';
INSERT #Results
(
[database],
[schema],
[table],
[column],
ExampleValue
)
EXEC [master].sys.sp_executesql #DatabaseCommands,
N'#ColumnCommands NVARCHAR(MAX), #SearchTerm NVARCHAR(255)',
#ColumnCommands, #SearchTerm;
SELECT [Searched for] = #SearchTerm;
SELECT [database],[schema],[table],[column],ExampleValue
FROM #Results
ORDER BY [database],[schema],[table],[column];
END
GO
This seems like a crazy thing to do - the search could completely kill your server. That said, if this is a one-off/occasional use manual run kind of thing then you can create a script to generate the SQL commands you need and then execute the result sets. I've done this sort of thing before to refactor columns in a database.
Your script would need to first run a query to get the names of all the DBs on your sql-server instance
SELECT * FROM sys.databases
Then create dynamic sql including the USE command to specify the DB and use the information_schema.tables and .columns to get the names of all the tables and columns you want to search - up to you how you decided to select the columns to be searched (is it just columns like N'%name%' for example.
Then use this information in a for loop to create individual table/column searches. For efficiency (and to help you debug if it's not working) I suggest utilise #table variables.
Finally select the contents of any #tables holding the table queries you've created.
This output can then be run on the server - providing you've included the use commands or prefixed table names with [dbname].[schema].

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

SQL Server Update Trigger, Get Only modified fields

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