Triggers-Implementation Issue - sql

I am making 2 tables for implementing triggers –
create table emp(id int primary key identity(1,1),
name char(40),
salary varchar(50),
gender char(40),
departmentid int);
insert into emp(name,salary,gender,departmentid) values ('jimmi',4800,'Male',4);
create table emp_audit
(
id int primary key identity(1,1),
audit varchar(60)
);
alter trigger trigger_em_update on emp for update
as begin
Declare #Id int
Declare #oldname char(40),#newname char(40)
Declare #oldsal int,#newsal int
Declare #oldgen char(40),#newgen char(40)
Declare #olddeptid int,#newdeptid int
Declare #auditstring nvarchar(max)
--select * from deleted;
select * into #temptable from inserted;
while(Exists(select id from #temptable)) --boolean condition if there are rows are not
Begin
set #auditstring =''
--if there are many rows we still select the first one
select Top 1 #Id =id,#newname=name,#newgen=gender,#newsal=salary,#newdeptid=departmentid
from #temptable;
select Top 1 #Id =id,#oldname=name,#oldgen=gender,#oldsal=salary,#olddeptid=departmentid
from deleted where #Id=id;
set #auditstring=' Employee with id= '+CAST(#Id as varchar(20))+ ' changed '
if(#oldname<>#newname)
set #auditstring=#auditstring + 'name from '+ #oldname +' to' +#newname
if(#oldsal<>#newsal)
set #auditstring=#auditstring + ' salary from '+ #oldsal +' to ' +#newsal
if(#oldgen<>#newgen)
set #auditstring=#auditstring + ' gender from ' + #oldgen + ' to ' + #newgen
-- if(#olddeptid<>#newdeptid)
--set #auditstring=#auditstring + ' departmentid from ' + cast(#olddeptid as nvarchar(5))+' to '
insert into emp_audit values(#auditstring)
delete from #temptable where id=#Id
end
end
when i use update query
update emp set name='vishi',gender='male',salary='4000',departmentid=3 where id=3;
It gives an error
"Conversion failed when converting the nvarchar value ' Employee with id= 3 changed name from james tovishi salary from ' to data type int.
"
i don't know how to solve this..can you solve this..

The problem is in line:
if(#oldsal<>#newsal)
set #auditstring=#auditstring + ' salary from '+ #oldsal +' to ' +#newsal
Should be:
if(#oldsal<>#newsal)
set #auditstring=#auditstring+ ' salary from '+CAST(#oldsal AS NVARCHAR(100))
+' to ' +CAST(#newsal AS NVARCHAR(100))
A couple of thoughts:
It is a good practice to end each statement with semicolon
#oldsal<>#newsal won't detect changing from NULL to value or value to NULL
Row-by-row processing is not best practice from performance perspective(especially inside trigger's body)
set #auditstring=#auditstring +.... could be replaced with set #auditstring += ...
If any value that you are using to concatenate auditstring is NULL variable will be set to NULL.
I do not recommend to store name/variable as CHAR(size), it is better to use VARCHAR(size)
Tracking what has changed as single string will require parsing in the future(unless you only want to display it).

The problem is that one of the columns used with + is a number, probably salary. But your trigger is way too complicated.
SQL is a set-based language. So use the sets when you can:
alter trigger trigger_em_update on emp for update
as
begin
insert into emp_audit (auditstring) -- guessing the name
select ('Employee with id k= ' + CAST(#Id as varchar(20))+ ' changed ' +
(case when #oldname <> #newname
then 'name from '+ #oldname +' to ' + #newname + ' '
else ''
end) +
(case when #oldsal <> #oldsal
then 'salary from '+ convert(varchar(255), #oldsal) +' to ' + concvert(varchar(255), #newsal) + ' '
else ''
end) +
(case when #oldgen <> #newgen
then 'gender from '+ convert(varchar(255), #oldgen) +' to ' + concvert(varchar(255), #newgen) + ' '
else ''
end) +
(case when #olddeptid <> #newdeptid
then 'departmentid from '+ convert(varchar(255), #olddeptid) +' to ' + concvert(varchar(255), #newdeptid) + ' '
else ''
end)
) as auditstring
from inserted i join
deleted d
on i.id = d.id;
end;

Related

Add new column to all tables in database

I'm trying to figure out if there's a quick way or single query to add a new column to all tables in database.
Right now I'm doing this for each table
ALTER TABLE [dbo].[%TABLE_NAME%] ADD %COLUMN_NAME% DATATYPE NOT NULL DEFAULT %VALUE%;
Is there a procedure or query I can make in AzureDataStudio to add a new column to all tables with the same name and default value.
select 'ALTER TABLE ' + QUOTENAME(SCHEMA_NAME([schema_id])) + '.' + QUOTENAME([name])
+ ' ADD %COLUMN_NAME% DATATYPE NOT NULL DEFAULT %VALUE%;'
from sys.tables
Create the statements you need with the above then run them.
I'd personally create a loop with dynamic SQL which gets executed as it is ran. The code below creates a temp table which is utilized for the loop which will iterate through each table listed in the temp table based on a calculated row number. The dynamic SQL is then set and executed.
Once you make the necessary changes, putting in your database name, column name, data type, and default value and you are satisfied with the results that get printed, you can un-comment the EXECUTE(#SQL) and re-run the script and it will add the new column to all your tables.
USE [INSERT DATABASE NAME HERE]
GO
IF OBJECT_ID(N'tempdb..#TempSysTableNames') IS NOT NULL
BEGIN
DROP TABLE #TempSysTableNames
END;
DECLARE #ColumnName VARCHAR(250) = 'INSERT COLUMN NAME HERE'
,#DataType VARCHAR(250) = 'INSERT DATA TYPE HERE'
,#DefaultValue VARCHAR(250) = 'INSERT DEFAULT VALUE HERE'
,#SQL VARCHAR(8000)
,#MaxRowNum INT
,#I INT = 1;
SELECT '[' + DB_NAME() + '].[' + OBJECT_SCHEMA_NAME([object_id],DB_ID()) + '].[' + name + ']' AS [name]
,ROW_NUMBER() OVER (ORDER BY [create_date]) AS RowNum
INTO #TempSysTableNames
FROM sys.tables
WHERE [type] = 'U';
SET #MaxRowNum = (SELECT MAX(RowNum)
FROM #TempSysTableNames);
WHILE (#I <= #MaxRowNum)
BEGIN
SET #SQL = (SELECT 'ALTER TABLE ' + [name] + ' ADD ' + #ColumnName + ' ' + #DataType + ' NOT NULL DEFAULT ' + #DefaultValue + ';'
FROM #TempSysTableNames
WHERE RowNum = #I);
PRINT(#SQL);
--EXECUTE(#SQL);
SET #I += 1;
END;

Variable value was lost when while loop finished

I want to set some default values to some table by using dynamic SQL in SQL Server, so I write 2 while loop, one is for tables and one is for columns in that table. so the outer loop is used to iterate table and the inner loop is used to iterate columns according to different data types the default will vary from one to other. So I need to catenate strings to build the dynamic SQL, please see my code below:
DECLARE #V_TABLE_LIST TABLE (TABLE_NAME VARCHAR(300))
DECLARE #V_COLUMN_LIST TABLE (TABLE_NAME VARCHAR(300), COLUMN_NAME VARCHAR(300), DATA_TYPE VARCHAR(300))
DECLARE #V_TABLE_NAME VARCHAR(300)
DECLARE #V_TABLE_NAME2 VARCHAR(300)
DECLARE #V_COLUMN_NAME VARCHAR(300)
DECLARE #V_DATA_TYPE VARCHAR(300)
DECLARE #V_SQL_ENABLE_IDENTITY_INSERT VARCHAR(200)
DECLARE #V_SQL_INSERT VARCHAR(3500)
DECLARE #V_SQL_COLUMN_LIST_NAME VARCHAR(3000)
DECLARE #V_SQL_COLUMN_LIST_VALUE VARCHAR(3000)
DECLARE #V_SQL_DISABLE_IDENTITY_INSERT VARCHAR(200)
INSERT INTO #V_TABLE_LIST
(TABLE_NAME)
SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_NAME LIKE 'DIM%' AND TABLE_TYPE = 'BASE TABLE'
AND TABLE_NAME NOT IN ('DIM_DATE') AND TABLE_NAME = 'DIM_ASSET'
--loop through each table
WHILE (SELECT COUNT(*) FROM #V_TABLE_LIST) > 0
BEGIN
SELECT TOP 1
#V_TABLE_NAME = TABLE_NAME
FROM #V_TABLE_LIST
--PRINT(#V_TABLE_NAME)-------------
SET #V_SQL_ENABLE_IDENTITY_INSERT = 'SET IDENTITY_INSERT ' + #V_TABLE_NAME + ' ON'
SET #V_SQL_DISABLE_IDENTITY_INSERT = 'SET IDENTITY_INSERT ' + #V_TABLE_NAME + ' OFF'
--load column info into #v_column_list table variable for each table
INSERT INTO #V_COLUMN_LIST
(TABLE_NAME, COLUMN_NAME, DATA_TYPE)
SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE
FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = #V_TABLE_NAME
SET #V_SQL_INSERT = ''
SET #V_SQL_COLUMN_LIST_NAME = ''
SET #V_SQL_COLUMN_LIST_VALUE = ''
--loop through each column for each table
WHILE (SELECT COUNT(*) FROM #V_COLUMN_LIST) > 0
BEGIN
SELECT TOP 1
#V_TABLE_NAME2 = TABLE_NAME
,#V_COLUMN_NAME = COLUMN_NAME
,#V_DATA_TYPE = DATA_TYPE
FROM #V_COLUMN_LIST
SET #V_SQL_COLUMN_LIST_NAME = #V_SQL_COLUMN_LIST_NAME + #V_COLUMN_NAME + ' --' + #V_DATA_TYPE +CHAR(10) + ','
SET #V_SQL_COLUMN_LIST_VALUE = #V_SQL_COLUMN_LIST_VALUE +
CASE WHEN #V_DATA_TYPE IN ('VARCHAR','NVARCHAR','CHAR', 'NCHAR') THEN '''UNKNOWN'''
WHEN #V_DATA_TYPE IN ('bigint', 'INT', 'smallint', 'DECIMAL','NUMERIC','MONEY','SMALLMONEY') THEN '-1'
WHEN #V_DATA_TYPE IN ('BIT', 'TINYINT') THEN NULL
WHEN #V_DATA_TYPE IN ('DATE', 'DATETIME','SMALLDATETIME','DATETIMEOFFSET','DATETIME2') THEN '''1957-01-01'''
ELSE ''
END + ' --' + #V_COLUMN_NAME + CHAR(10) + ','
DELETE FROM #V_COLUMN_LIST WHERE TABLE_NAME = #V_TABLE_NAME2 AND COLUMN_NAME = #V_COLUMN_NAME
--PRINT(#V_SQL_COLUMN_LIST_VALUE)
END
PRINT(#V_SQL_COLUMN_LIST_NAME)
PRINT(#V_SQL_COLUMN_LIST_VALUE)
--PRINT(#V_SQL_ENABLE_IDENTITY_INSERT)
SET #V_SQL_INSERT = 'INSERT INTO ' + #V_TABLE_NAME + CHAR(10)
+ '('
+ #V_SQL_COLUMN_LIST_NAME
+ ')'
+ ' VALUES ' + CHAR(10)
+ '(' + CHAR(10)
+ #V_SQL_COLUMN_LIST_VALUE
+ ')'
--PRINT(#V_SQL_INSERT)
--PRINT(#V_SQL_DISABLE_IDENTITY_INSERT)
DELETE FROM #V_COLUMN_LIST
DELETE FROM #V_TABLE_LIST WHERE TABLE_NAME = #V_TABLE_NAME
END
I added 2 print statements:
PRINT(#V_SQL_COLUMN_LIST_NAME) ---the concatenated field list can be printed out normally
PRINT(#V_SQL_COLUMN_LIST_VALUE) ---cannot print concatenated default value list , why?
as you can see the two print statements are the next step for the finishing of inner loop, but the first print statement can print out the something and the second one is empty, I checked the code a long time, I cannot find why the second print statement output empty string. Any logic errors in the code above?
This row sets the entire result to NULL if any column of BIT or TINYINT type is met.
WHEN #V_DATA_TYPE IN ('BIT', 'TINYINT') THEN NULL
Should be
WHEN #V_DATA_TYPE IN ('BIT', 'TINYINT') THEN 'NULL'
the same way as any other constant in a dynamic sql.

How to find the query for creating a table in SQL Server 2008

Is there a way to display the query for creating a table? For example, there is a table called CLIENT, and I want to see the query for creating this table. How can I do this?
Right Click on the table
Script Table as
CREATE to
New Query Editor Window
EDIT: Damn you have to be quick here for the easy rep! haha.
Right click the table in SSMS and select Script Table > As Create
If you want to do it from some script then here it is
declare #table varchar(100)
set #table = 'client_table' -- set table name here
declare #sql table(s varchar(1000), id int identity)
-- create statement
insert into #sql(s) values ('create table [' + #table + '] (')
-- column list
insert into #sql(s)
select
' ['+column_name+'] ' +
data_type + coalesce('('+cast(character_maximum_length as varchar)+')','') + ' ' +
case when exists (
select id from syscolumns
where object_name(id)=#table
and name=column_name
and columnproperty(id,name,'IsIdentity') = 1
) then
'IDENTITY(' +
cast(ident_seed(#table) as varchar) + ',' +
cast(ident_incr(#table) as varchar) + ')'
else ''
end + ' ' +
( case when IS_NULLABLE = 'No' then 'NOT ' else '' end ) + 'NULL ' +
coalesce('DEFAULT '+COLUMN_DEFAULT,'') + ','
from information_schema.columns where table_name = #table
order by ordinal_position
-- primary key
declare #pkname varchar(100)
select #pkname = constraint_name from information_schema.table_constraints
where table_name = #table and constraint_type='PRIMARY KEY'
if ( #pkname is not null ) begin
insert into #sql(s) values(' PRIMARY KEY (')
insert into #sql(s)
select ' ['+COLUMN_NAME+'],' from information_schema.key_column_usage
where constraint_name = #pkname
order by ordinal_position
-- remove trailing comma
update #sql set s=left(s,len(s)-1) where id=##identity
insert into #sql(s) values (' )')
end
else begin
-- remove trailing comma
update #sql set s=left(s,len(s)-1) where id=##identity
end
-- closing bracket
insert into #sql(s) values( ')' )
-- result!
select s from #sql order by id
this will give you the output like
create table [client_table] (
[colA] varchar(250) NOT NULL DEFAULT (''),
[colB] int NOT NULL DEFAULT(0)
)

How do I extract Sybase (12.5) table DDL via SQL?

I've scanned similar questions but they seem to be referring to other databases and/or external languages.
I'm looking to programatically extract table DDL via SQL, with a result that's "good enough" to re-import and reconstruct the table.
DBArtisan produces the exact result I'm looking for, but I have a dynamic list of a few dozen tables that I need to work with, and was hoping for a programatic solution.
I figure DBArtisan has to be doing calling the API somehow. Are they just ripping against the systables or is there a system installed stored proc (similar to the one that yields stored proc text) that I'm missing?
Best solution would be to wrap this into a nice stored procedure, but you should get the idea from the code below. Just replace:
SELECT #OnlyTableName = 'my_table_name'
with your table name and execute the code, you should get all DDL statements in #rtn table at the end of this code:
DECLARE #TableName varchar(50)
DECLARE #ObjectID int
DECLARE #IndexID int
DECLARE #IndexStatus int
DECLARE #IndexName varchar(30)
DECLARE #msg varchar(255)
DECLARE #OnlyTableName varchar(50)
DECLARE #LastColumnId int
DECLARE #i int
SELECT #OnlyTableName = 'my_table_name'
CREATE TABLE #columns (
column_name char(30) NULL,
type_name char(30) NULL,
length char(10) NULL,
iden_flag char(10) NULL,
null_flag char(20) NULL,
flag char(1) NULL
)
CREATE TABLE #rtn (
msg varchar(255) NULL
)
SELECT #TableName = name,
#ObjectID = id
FROM sysobjects
WHERE type = 'U'
AND name = #OnlyTableName
ORDER BY name
SELECT #LastColumnId = MAX(colid) FROM syscolumns WHERE id = #ObjectID
INSERT #columns
SELECT col.name,
typ.name,
CASE WHEN typ.name in ('decimal','numeric') THEN '(' +
convert(varchar, col.prec) + ',' + convert(varchar, col.scale) + ')'
WHEN typ.name like '%char%'THEN
'('+CONVERT(varchar,col.length)+')'
ELSE '' END,
CASE WHEN col.status = 0x80 THEN 'IDENTITY' ELSE '' END,
CASE WHEN convert(bit, (col.status & 8)) = 0 THEN "NOT NULL"
ELSE "NULL" END + CASE WHEN col.colid = #LastColumnId THEN ')' ELSE
',' END,
NULL
FROM syscolumns col, systypes typ
WHERE col.id = #ObjectID
AND col.usertype = typ.usertype
ORDER BY col.colid
INSERT #rtn
SELECT "CREATE TABLE " + #TableName + " ("
UNION ALL
SELECT ' '+
column_name + replicate(' ',30- len(column_name)) +
type_name + length + replicate(' ',20 -
len(type_name+length)) +
iden_flag + replicate(' ',10 - len(iden_flag))+
null_flag
FROM #columns
SELECT name, indid, status, 'N' as flag INTO #indexes
FROM sysindexes WHERE id = #ObjectID
SET ROWCOUNT 1
WHILE 1=1
BEGIN
SELECT #IndexName = name, #IndexID = indid, #IndexStatus =
status FROM #indexes WHERE flag = 'N'
IF ##ROWCOUNT = 0
BREAK
SELECT #i = 1
SELECT #msg = ''
WHILE 1=1
BEGIN
IF index_col(#TableName, #IndexID, #i) IS NULL
BREAK
SELECT #msg = #msg + index_col(#TableName, #IndexID, #i) +
CASE WHEN index_col(#TableName, #IndexID, #i+1) IS NOT NULL THEN ','
END
SELECT #i = #i+1
END
IF #IndexStatus & 2048 = 2048 --PRIMARY KEY
INSERT #rtn
SELECT "ALTER TABLE " + #TableName +
" ADD CONSTRAINT " + #IndexName +
" primary key "+
CASE WHEN #IndexID != 1 THEN 'nonclustered ' END +
'('+ #msg +')'
ELSE
IF (#IndexStatus & 2048 = 0 AND #IndexID NOT IN (0, 255))
--NOT PRIMARY KEY
INSERT #rtn
SELECT 'CREATE '+
CASE WHEN #IndexStatus & 2 = 2 THEN 'UNIQUE ' ELSE '' END +
CASE WHEN #IndexID = 1 THEN 'CLUSTERED ' ELSE 'NONCLUSTERED ' END +
'INDEX ' + #IndexName + ' ON ' + #TableName + ' ('+ #msg +')'
UPDATE #indexes SET flag = 'Y' WHERE indid = #IndexID
END
SET ROWCOUNT 0
SELECT * FROM #rtn
DROP TABLE #columns
DROP TABLE #rtn
let me know if it helped.
(credits go to ROCKY for this one ;-)
IIRC there's a tool called DBSchema ( peppler.org/downloads/dbschema-2_4_2.zip is the best URL I was able to find ) - in case the URL doesn't ring any bells, Mike Peppller is the author of sybperl. You can likely reverse engineer the code for that script if you prefer to roll your own.
As far as SQL-wise, the table info is in sysobjects table and the column info is in syscolumns in Sybase.
You can also use stored procs: http://www.razorsql.com/articles/sybase_admin_queries.html
Yeah, but more to it than tables names and columns.
You need constraints, indexes, keys, defaults, partitions, permissions ......
Remarkable how thin on the ground resources are for sybase code that will do it
(sp_help does not cover it all - to test, use something like DBArtisan Extract DDL tool and you will see how comprehensive THAt is!)
ASE ships in with DDL Script Generator Utility - ddlgen
The utility can be used to create backup of scripts for an entire database, tables, etc. Sample commands are provided in Sybase help site.
Under Windows, the utilty can be found at %sybase%/ASE-15_0/bin

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