Please start any new threads on our new site at https://forums.sqlteam.com. We've got lots of great SQL Server experts to answer whatever question you can come up with.

 All Forums
 General SQL Server Forums
 New to SQL Server Programming
 Using triggers for a tracked changes table

Author  Topic 

im1dermike
Posting Yak Master

222 Posts

Posted - 2008-10-27 : 12:15:35
I need to create a table that logs changes (updates) made in 5 other tables. The data I need to track includes:
  • user who made change
  • table that was changed
  • column changed
  • original value
  • new value
I've never used triggers before, but I believe I have to create a series of triggers. I'm trying to read about triggers, but I'm having trouble wrapping my head around them. Can someone give me a starting place?

visakh16
Very Important crosS Applying yaK Herder

52326 Posts

Posted - 2008-10-27 : 12:37:33
you need to create a series of triggers one for each table. it will be like

CREATE TRIGGER yourtrigger ON yourtable
AFTER INSERT,UPDATE
AS
BEGIN
INSERT INTO Log_Table (table_name,user_name,modified_date,column_changed,original_value,newvalue)
SELECT 'YourTable',
SUSER_SNAME(),
GETDATE(),
CASE WHEN i.col1<>COALESCE(d.col1.'') THEN 'col1' ELSE NULL END,
d.col1,i.col1,CASE WHEN i.col2<>COALESCE(d.col2.'') THEN 'col2' ELSE NULL END,
d.col2,i.col2
...
FROM INSERTED 1
LEFT JOIN DELETED d
ON i.PK=d.PK
END

PK is primary key
Go to Top of Page

im1dermike
Posting Yak Master

222 Posts

Posted - 2008-10-27 : 13:56:31
Thanks, visakh16.

I understand most of what you entered. My only issue is your code that gets the original and new values. What exactly are you doing there? Do you have a static case statement for each column to determine which columns' original and new values are different? Can you delve into that part of your example a little more and/or give another example?

Thanks
Go to Top of Page

visakh16
Very Important crosS Applying yaK Herder

52326 Posts

Posted - 2008-10-27 : 14:02:33
quote:
Originally posted by im1dermike

Thanks, visakh16.

I understand most of what you entered. My only issue is your code that gets the original and new values. What exactly are you doing there? Do you have a static case statement for each column to determine which columns' original and new values are different? Can you delve into that part of your example a little more and/or give another example?

Thanks


the case statements are to return the column names which changed and NULL if no change. There's no direct way to get column name of column which changed in trigger code. thats why i have given static case statements for each column in table. original and new values are obtained from INSERTED & DELETED internal temporary tables for each column.
Go to Top of Page

im1dermike
Posting Yak Master

222 Posts

Posted - 2008-10-27 : 14:11:33
So if I understand you correctly, you're returning a string of column names which changed? I believe what I need is a row for each change meaning that if the value of two fields in a table was changed, two records would then appear in the log table, each with the value and column reference of a single change.

Is this possible or do you have to have any/all changes in a single record?
Go to Top of Page

blindman
Master Smack Fu Yak Hacker

2365 Posts

Posted - 2008-10-27 : 15:19:44
1dermike, why are you not just using a archive tables to record all historical record values?
The method you appear to be using in neither the easiest to code, nor the easiest to query.


If it is not practically useful, then it is practically useless.
Go to Top of Page

im1dermike
Posting Yak Master

222 Posts

Posted - 2008-10-27 : 15:45:50
blindman: Never heard of archive tables. Can you provide me with some info on them please? A google search of "sql archive tables" didn't seem to return things that were as fruitful as I expected them to be.
Go to Top of Page

blindman
Master Smack Fu Yak Hacker

2365 Posts

Posted - 2008-10-28 : 10:11:26
An archive table is identical in design to the production table, with the addition of a time stamp column, a flag indicating whether the record was deleted, and possibly a column for recording who modified the data. It stores a complete history of all the records in the production table, including the current record.
Triggers on the production tables copie all inserts, update, and deletes to the archive tables.
Most queries can be run directly against the production table, but point-in-time queries can be run against the archive tables, and of course they can also be used for auditing.

I have a script that will generate archive tables and triggers for each production table in your database, if you are interested.

If it is not practically useful, then it is practically useless.
Go to Top of Page

im1dermike
Posting Yak Master

222 Posts

Posted - 2008-10-29 : 14:58:32
Yes, that would be great.
Go to Top of Page

Lamprey
Master Smack Fu Yak Hacker

4614 Posts

Posted - 2008-10-29 : 18:01:50
I assume you are not using Sql 2008, but if you are, then you could make use of the Change Data Capture functionality. Or maybe lookas how they do it in 2008 and apply some of that to your history tables.
Go to Top of Page

blindman
Master Smack Fu Yak Hacker

2365 Posts

Posted - 2008-10-30 : 09:27:39
1dermike, here you go....

set nocount on
--Auto archive script
--blindman, 3/2/2006
--blindman, 11/7/2007: Enhanced to handle inserts/updates/deletes

--creates a script to create archive tables and triggers for saving deleted records.

declare @TableNameMask varchar(50)
declare @ArchiveSuffix varchar(50)
declare @DeletedColumnName varchar(50)
set @TableNameMask = '' --Filters specific tables by name.
set @ArchiveSuffix = '_arc' --Suffix to append to table names.
set @DeletedColumnName = 'DeletedFlag' --Will indicate whether archive record was the result of a delete operation.

declare @TableName varchar(500)
declare @FieldDefs varchar(max)
declare @FieldList varchar(max)
declare @DeletedList varchar(max)
declare @SQLStringTABLECREATE varchar(max)
declare @SQLStringTRIGGERHEADER varchar(max)
declare @SQLStringTRIGGERINSERT varchar(max)
declare @SQLStringTRIGGERDELETE varchar(max)
declare @KeyList varchar(max)
declare @FirstKey varchar(100)


/*
select '/*'
select name as 'The following tables will be scripted:'
from sysobjects
where type = 'U'
and name not like 'dt%'
and name not like '%' + @ArchiveSuffix
and name not like '%staging%'
order by name
select '*/'
*/

declare TableList cursor for
select name
from sysobjects
where type = 'U'
and (name like @TableNameMask or nullif(@TableNameMask, '') is null)
and name not like 'dt%'
and name not like '%' + @ArchiveSuffix
and name not like '%staging%'
and name not in ('sysdiagrams')
order by name

open TableList
fetch next from TableList into @TableName

while @@fetch_status = 0
begin
set @FieldDefs = null
set @FieldList = null
set @DeletedList = null
set @SQLStringTABLECREATE = null
set @SQLStringTRIGGERHEADER = null
set @SQLStringTRIGGERINSERT = null
set @SQLStringTRIGGERDELETE = null
set @KeyList = null
set @FirstKey = null

select @FieldDefs = isnull(@FieldDefs + ',' +char(10) + ' ', '')
+ syscolumns.name
+ ' ' + systypes.name
+ isnull(' (' + case systypes.name
when 'timestamp' then Null
when 'smalldatetime' then Null
when 'datetime' then Null
when 'int' then Null
when 'bigint' then Null
when 'smallint' then Null
when 'tinyint' then Null
when 'real' then Null
when 'bit' then Null
when 'money' then Null
when 'uniqueidentifier' then Null
when 'xml' then Null
when 'decimal' then cast(syscolumns.prec as varchar(10)) + ', ' + cast(syscolumns.scale as varchar(10))
when 'numeric' then cast(syscolumns.prec as varchar(10)) + ', ' + cast(syscolumns.scale as varchar(10))
else case when syscolumns.length = -1 then 'max' else cast(syscolumns.length as varchar(10)) end
end + ')', '')
+ ' ' + case syscolumns.isnullable when 0 then 'NOT NULL' else 'NULL' end
from syscolumns
inner join sysobjects on syscolumns.id = sysobjects.id
inner join systypes on syscolumns.xusertype = systypes.xusertype
where sysobjects.name = @TableName
and systypes.name not in ('text', 'ntext', 'image') --These cannot be referenced in the deleted table
and syscolumns.name not in ('Modified', 'Modifier')
order by syscolumns.colid

select @FieldList = isnull(@FieldList + ',' +char(10) + ' ', '')
+ syscolumns.name
from syscolumns
inner join sysobjects on syscolumns.id = sysobjects.id
inner join systypes on syscolumns.xusertype = systypes.xusertype
where sysobjects.name = @TableName
and systypes.name not in ('text', 'ntext', 'image') --These cannot be referenced in the deleted table
and syscolumns.name not in ('Modified', 'Modifier')
order by syscolumns.colid

select @DeletedList = isnull(@DeletedList + ',' +char(10) + ' ', '')
+ 'deleted.' + syscolumns.name
from syscolumns
inner join sysobjects on syscolumns.id = sysobjects.id
inner join systypes on syscolumns.xusertype = systypes.xusertype
where sysobjects.name = @TableName
and systypes.name not in ('text', 'ntext', 'image') --These cannot be referenced in the deleted table
and syscolumns.name not in ('Modified', 'Modifier')
order by syscolumns.colid

select @KeyList = isnull(@KeyList + char(10) + ' and ', '') + 'deleted.' + columns.name + ' = inserted.' + columns.name
from sys.objects objects
inner join sys.indexes indexes on objects.object_id = indexes.object_id
inner join sys.index_columns index_columns
on indexes.object_id = index_columns.object_id
and indexes.index_id = index_columns.index_id
inner join sys.columns columns
on index_columns.object_id = columns.object_id
and index_columns.column_id = columns.column_id
where is_primary_key = 1
and objects.name = @TableName

set @FirstKey =
(select top 1 columns.name
from sys.objects objects
inner join sys.indexes indexes on objects.object_id = indexes.object_id
inner join sys.index_columns index_columns
on indexes.object_id = index_columns.object_id
and indexes.index_id = index_columns.index_id
inner join sys.columns columns
on index_columns.object_id = columns.object_id
and index_columns.column_id = columns.column_id
where is_primary_key = 1
and objects.name = @TableName
order by columns.column_id)

set @SQLStringTABLECREATE = '--<<[' + @TableName + ']>>--' + char(10)
+ 'Create table [' + @TableName + @ArchiveSuffix + ']' + char(10)
+ ' (' + @FieldDefs + ',' + char(10)
+ ' ' + @DeletedColumnName + ' bit NOT NULL,' + char(10)
+ ' Modified datetime NULL,' + char(10)
+ ' Modifier varchar (50) NULL)' + char(10)
+ 'GO' + char(10)
+ char(10)
+ 'ALTER TABLE [' + @TableName + @ArchiveSuffix + '] ADD ' + char(10)
+ ' CONSTRAINT [DF_' + @TableName + @ArchiveSuffix + '_Deleted] DEFAULT (0) FOR [' + @DeletedColumnName + '],' + char(10)
+ ' CONSTRAINT [DF_' + @TableName + @ArchiveSuffix + '_Modified] DEFAULT (getdate()) FOR [Modified],' + char(10)
+ ' CONSTRAINT [DF_' + @TableName + @ArchiveSuffix + '_Modifier] DEFAULT (suser_sname()) FOR [Modifier]' + char(10)
+ 'GO' + char(10)

set @SQLStringTRIGGERHEADER ='CREATE TRIGGER [TR_' + @TableName + '_Archiver] ON [' + @TableName + ']' + char(10)
+ 'FOR INSERT, UPDATE, DELETE' + char(10)
+ 'AS' + char(10)
+ 'set nocount on' + char(10)
+ 'if exists (select * from dbo.sysobjects where id = object_id(N''[' + @TableName + @ArchiveSuffix + ']'') and OBJECTPROPERTY(id, N''IsUserTable'') = 1)' + char(10)
+ 'begin'

set @SQLStringTRIGGERINSERT = 'insert into [' + @TableName + @ArchiveSuffix + ']' + char(10)
+ ' (' + @FieldList + ',' + char(10)
+ ' ' + @DeletedColumnName + ')' + char(10)
+ 'select ' + @FieldList + ',' + char(10)
+ ' 0' + char(10)
+ 'from inserted' + char(10)
+ 'where TRIGGER_NESTLEVEL() = 1' + char(10)

set @SQLStringTRIGGERDELETE = 'insert into [' + @TableName + @ArchiveSuffix + ']' + char(10)
+ ' (' + @FieldList + ',' + char(10)
+ ' ' + @DeletedColumnName + ')' + char(10)
+ 'select ' + @DeletedList + ',' + char(10)
+ ' 1' + char(10)
+ 'from deleted' + char(10)
+ 'left outer join inserted on ' + @KeyList + char(10)
+ 'where inserted.' + @FirstKey + ' is null' + char(10)
+ 'end' + char(10)
+ 'GO'

if @FirstKey is not null
begin
print @SQLStringTABLECREATE
print @SQLStringTRIGGERHEADER
print @SQLStringTRIGGERINSERT
print @SQLStringTRIGGERDELETE
end
else select '--NO PRIMARY KEY ON TABLE ' + @TableName + '!'

fetch next from TableList into @TableName
end

close tablelist
deallocate tablelist


If it is not practically useful, then it is practically useless.
Go to Top of Page

im1dermike
Posting Yak Master

222 Posts

Posted - 2008-11-10 : 10:14:46
Thanks blindman. So all I need to do is alter the TableList query to select only the tables I'd like to archive? Do I need to create the _arc tables before too?
Go to Top of Page

blindman
Master Smack Fu Yak Hacker

2365 Posts

Posted - 2008-11-10 : 10:37:47
Yes, and it creates a script that you can edit before executing if you want to customize it.

If it is not practically useful, then it is practically useless.
Go to Top of Page

im1dermike
Posting Yak Master

222 Posts

Posted - 2008-11-10 : 15:12:15
Do my tables have to have primary keys because they don't.
Go to Top of Page

im1dermike
Posting Yak Master

222 Posts

Posted - 2008-11-10 : 15:21:55
Also, is there any way to have the archive tables in a different, centralized database than the one where the tables are located?
Go to Top of Page

blindman
Master Smack Fu Yak Hacker

2365 Posts

Posted - 2008-11-10 : 20:45:49
Yes. You need a unique key on each table.
If you don't have that, you have real problems.

No, you should not use triggers to communicate between databases. Bad idea.

If it is not practically useful, then it is practically useless.
Go to Top of Page

im1dermike
Posting Yak Master

222 Posts

Posted - 2008-11-13 : 10:55:33
Well, yes I would have real problems with regard to your functionality, but surely tables can exist that don't have a primary key.

I think what I'm just going to have to do is create a stored procedure and call it whenever changes are made. They changes I'm trying to track are made via an ASP website I've created. This will also allow me to store the changes in a centralized database.

Thanks for your help
Go to Top of Page

blindman
Master Smack Fu Yak Hacker

2365 Posts

Posted - 2008-11-14 : 09:42:15
quote:
Originally posted by im1dermike

Well, yes I would have real problems with regard to your functionality, but surely tables can exist that don't have a primary key.

Of course. And you can have a car without brakes as well. Just let me know when you are going to be on the road so I can plan accordingly.

If it is not practically useful, then it is practically useless.
Go to Top of Page

im1dermike
Posting Yak Master

222 Posts

Posted - 2008-11-14 : 09:48:15
Can you explain to me why every table should have a primary key?
Go to Top of Page

blindman
Master Smack Fu Yak Hacker

2365 Posts

Posted - 2008-11-14 : 11:15:09
Every table should have a method to uniquely identify each record.
That is just database design 101.
Otherwise, you cannot issue statements against it with predictable results.

If it is not practically useful, then it is practically useless.
Go to Top of Page

im1dermike
Posting Yak Master

222 Posts

Posted - 2008-11-14 : 11:16:59
Obviously there shouldn't be any duplicate records, but a table doesn't need a primary key.
Go to Top of Page
    Next Page

- Advertisement -