Adding simple trigger-based auditing to your SQL Server database

How do you track changes to data in your database? There are a variety of supported auditing methods for SQL Server, including comprehensive C2 security auditing, but what do you do if you're solving a business rather than a security problem, and you're interested in tracking the following kinds of information:

  • What data has been updated recently
  • Which tables have not been updated recently
  • Who modified the price of Steeleye Stout to $20 / unit, and when did they do it?
  • What was the unit price for Steeleye Stout before Jon monkeyed with it?

There are a number of ways to design this into your solution from the start, for example:

  • The application is designed so that all changes are logged
  • All data changes go through a data access layer which logs all changes
  • The database is constructed in such a way that logging information is included in each table, perhaps set via a trigger

What if we're not starting from scratch?

But what do you do if you need to add lightweight auditing to an existing solution, in which data can be modified via a variety of direct access methods? When I ran into that challenge, I decided to use Nigel Rivett's SQL Server Auditing triggers. I read about some concern with the performance impact, but this database wasn't forecasted to have a high update rate. Nigel's script works by adding a trigger for INSERT, UPDATE, and DELETE on a single table. The trigger catches data changes, then saves out the information (such as table name, the primary key values, the column name that was altered, and the before and after values for that column) to an Audit table.

I needed to track every table in the database, though, and I expected the database schema to continue to change. I was able to generalize the solution a bit, because the database convention didn't use any no compound primary keys. I created the script listed below, which loops through all tables in the database with the exception of the Audit table, of course, since auditing changes to the audit table is both unnecessary and recursive. I'm also skipping sysdiagrams; you could include any other tables you don't want to track to that list as well.

The nice thing about the script I'm including below is that you can run it after making some schema changes and it will make sure that all newly added tables are included in the change tracking / audit, too.

Here's an example of what you'd see in the audit table for an Update followed by an Insert. Notice that the Update shows type U and a single column updated, while the Insert (type I) shows all columns added, one on each row:

Sample Audit Data

 

While this information is pretty unstructured, it's not difficult to run some useful reports. For instance, we can easily find things like

  • which tables were updated recently
  • which tables have not been updated in the past year
  • which tables have never been updated
  • all changes made by a specific user in a time period
  • most active tables in a time period

While it's not as easy, it's possible to backtrack from the current state to determine the state of a row in a table at a certain point in time. It's generally possible to dig out the state of an entire table at a point in time, but a change table isn't a good a fit for temporal data tracking - the right solution there is to start adding Modified By and Modified On columns to the required tables.

Note that we're only tracking data changes here. If you'd like to track schema changes, take a look at SQL Server 2005's DDL triggers.

Enough talking, give us the script!

Sure. I'll repeat that there are some disclaimers to the approach -  performance, it'll only track changes to tables with a primary key, etc. If you want to know more about the trigger itself, I'd recommend starting with Nigel's article. However, it worked great for our project.

USE MYAWESOMEDATABASE
GO

IF NOT EXISTS(SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME= 'Audit')
CREATE TABLE Audit
(
AuditID [int]IDENTITY(1,1) NOT NULL,
Type char(1),
TableName
varchar(128),
PrimaryKeyField
varchar(1000),
PrimaryKeyValue
varchar(1000),
FieldName
varchar(128),
OldValue
varchar(1000),
NewValue
varchar(1000),
UpdateDate
datetime DEFAULT (GetDate()),
UserName
varchar(128)
)
GO

DECLARE @sql varchar(8000), @TABLE_NAMEsysname
SET
NOCOUNT ON

SELECT
@TABLE_NAME= MIN(TABLE_NAME)
FROM INFORMATION_SCHEMA.Tables
WHERE
TABLE_TYPE
= 'BASE TABLE'
AND TABLE_NAME!= 'sysdiagrams'
AND TABLE_NAME!= 'Audit'

WHILE @TABLE_NAMEIS NOT NULL
 BEGIN
EXEC('IF OBJECT_ID (''' + @TABLE_NAME+ '_ChangeTracking'', ''TR'') IS NOT NULL DROP TRIGGER ' + @TABLE_NAME+ '_ChangeTracking')
SELECT @sql =
'
create trigger '
+ @TABLE_NAME+ '_ChangeTracking on ' + @TABLE_NAME+ ' for insert, update, delete
as

declare @bit int ,
@field int ,
@maxfield int ,
@char int ,
@fieldname varchar(128) ,
@TableName varchar(128) ,
@PKCols varchar(1000) ,
@sql varchar(2000),
@UpdateDate varchar(21) ,
@UserName varchar(128) ,
@Type char(1) ,
@PKFieldSelect varchar(1000),
@PKValueSelect varchar(1000)

select @TableName = '''
+ @TABLE_NAME+ '''

-- date and user
select @UserName = system_user ,
@UpdateDate = convert(varchar(8), getdate(), 112) + '' '' + convert(varchar(12), getdate(), 114)

-- Action
if exists (select * from inserted)
if exists (select * from deleted)
select @Type = ''U''
else
select @Type = ''I''
else
select @Type = ''D''

-- get list of columns
select * into #ins from inserted
select * into #del from deleted

-- Get primary key columns for full outer join
select@PKCols = coalesce(@PKCols + '' and'', '' on'') + '' i.'' + c.COLUMN_NAME + '' = d.'' + c.COLUMN_NAME
fromINFORMATION_SCHEMA.TABLE_CONSTRAINTS pk ,
INFORMATION_SCHEMA.KEY_COLUMN_USAGE c
where pk.TABLE_NAME = @TableName
andCONSTRAINT_TYPE = ''PRIMARY KEY''
andc.TABLE_NAME = pk.TABLE_NAME
andc.CONSTRAINT_NAME = pk.CONSTRAINT_NAME

-- Get primary key fields select for insert
select @PKFieldSelect = coalesce(@PKFieldSelect+''+'','''') + '''''''' + COLUMN_NAME + ''''''''
fromINFORMATION_SCHEMA.TABLE_CONSTRAINTS pk ,
INFORMATION_SCHEMA.KEY_COLUMN_USAGE c
where pk.TABLE_NAME = @TableName
andCONSTRAINT_TYPE = ''PRIMARY KEY''
andc.TABLE_NAME = pk.TABLE_NAME
andc.CONSTRAINT_NAME = pk.CONSTRAINT_NAME

select @PKValueSelect = coalesce(@PKValueSelect+''+'','''') + ''convert(varchar(100), coalesce(i.'' + COLUMN_NAME + '',d.'' + COLUMN_NAME + ''))''
from INFORMATION_SCHEMA.TABLE_CONSTRAINTS pk ,   
INFORMATION_SCHEMA.KEY_COLUMN_USAGE c  
where  pk.TABLE_NAME = @TableName  
and CONSTRAINT_TYPE = ''PRIMARY KEY''  
and c.TABLE_NAME = pk.TABLE_NAME  
and c.CONSTRAINT_NAME = pk.CONSTRAINT_NAME

if @PKCols is null
begin
raiserror(''no PK on table %s'', 16, -1, @TableName)
return
end

select @field = 0, @maxfield = max(ORDINAL_POSITION) from INFORMATION_SCHEMA.COLUMNS where TABLE_NAME = @TableName
while @field < @maxfield
begin
select @field = min(ORDINAL_POSITION) from INFORMATION_SCHEMA.COLUMNS where TABLE_NAME = @TableName and ORDINAL_POSITION > @field
select @bit = (@field - 1 )% 8 + 1
select @bit = power(2,@bit - 1)
select @char = ((@field - 1) / 8) + 1
if substring(COLUMNS_UPDATED(),@char, 1) & @bit > 0 or @Type in (''I'',''D'')
begin
select @fieldname = COLUMN_NAME from INFORMATION_SCHEMA.COLUMNS where TABLE_NAME = @TableName and ORDINAL_POSITION = @field
select @sql = ''insert Audit (Type, TableName, PrimaryKeyField, PrimaryKeyValue, FieldName, OldValue, NewValue, UpdateDate, UserName)''
select @sql = @sql + '' select '''''' + @Type + ''''''''
select @sql = @sql + '','''''' + @TableName + ''''''''
select @sql = @sql + '','' + @PKFieldSelect
select @sql = @sql + '','' + @PKValueSelect
select @sql = @sql + '','''''' + @fieldname + ''''''''
select @sql = @sql + '',convert(varchar(1000),d.'' + @fieldname + '')''
select @sql = @sql + '',convert(varchar(1000),i.'' + @fieldname + '')''
select @sql = @sql + '','''''' + @UpdateDate + ''''''''
select @sql = @sql + '','''''' + @UserName + ''''''''
select @sql = @sql + '' from #ins i full outer join #del d''
select @sql = @sql + @PKCols
select @sql = @sql + '' where i.'' + @fieldname + '' <> d.'' + @fieldname
select @sql = @sql + '' or (i.'' + @fieldname + '' is null and  d.'' + @fieldname + '' is not null)''
select @sql = @sql + '' or (i.'' + @fieldname + '' is not null and  d.'' + @fieldname + '' is null)''
exec (@sql)
end
end
'
SELECT @sql
EXEC(@sql)
SELECT @TABLE_NAME= MIN(TABLE_NAME) FROM INFORMATION_SCHEMA.Tables
WHERE TABLE_NAME> @TABLE_NAME
AND TABLE_TYPE= 'BASE TABLE'
AND TABLE_NAME!= 'sysdiagrams'
AND TABLE_NAME!= 'Audit'
END

 

Published Sunday, January 27, 2008 1:20 AM by Jon Galloway
Filed under: ,

Comments

# re: Adding simple trigger-based auditing to your SQL Server database

hi!

well if you ever do get concerned about performance you might want to try async auditing with service broker:

www.sqlteam.com/.../centralized-asynchronous-auditing-with-service-broker

www.sqlteam.com/.../centralized-asynchronous-auditing-across-instances-and-servers-with-service-broker

it's a bit of work to set up, but not that much.

works excellent on multiple db's on one server and for multiple db's on multiple servers

Sunday, January 27, 2008 8:53 AM by Mladen

# re: Adding simple trigger-based auditing to your SQL Server database

Just a foot note for anyone reading this in the future and thinking about implementing this: IMO this is one of those, "Look what we can do!" type of scripts that works theoretically but not in practice. I can think of no scenario in which anyone should implement such a trigger in the real world. Every time I see this OO-type of design posted, I have to always say that most SQL DBAs/developers disagree with this practice vehemently since there are automated tools that give you much more control over your auditing than a generic trigger (whether it be CLR or SQL-based).

Sunday, January 27, 2008 10:54 AM by Scott Whigham

# re: Adding simple trigger-based auditing to your SQL Server database

I've run into a problem using triggers. I need to log the user who deletes a record but the record is deleted before the column value for LastUser can be updated. I also have an update trigger so an update before the delete would generate an unwanted log entry.

Sunday, January 27, 2008 12:26 PM by rrobbins

# re: Adding simple trigger-based auditing to your SQL Server database

Or you could just but ApexSQL's great Audit product for a nominal price and call it a day.

Sunday, January 27, 2008 2:40 PM by John Mo

# re: Adding simple trigger-based auditing to your SQL Server database

Wow, dynamic, dynamic SQL. :-)

Sunday, January 27, 2008 4:16 PM by Duncan Smart

# re: Adding simple trigger-based auditing to your SQL Server database

@Scott Whigam - Appreciate your feedback. I used the above script in a real world business application, and it did work. It's not as flexible as an automated tool or a product, but some applications don't need a full-featured auditing solution - they just need change tracking information in a table for reporting purposes. In that scenario, the above solution worked very well.

Sunday, January 27, 2008 6:36 PM by Jon Galloway

# re: Adding simple trigger-based auditing to your SQL Server database

Just a potential gotcha with triggers. If you've got any SQL using @@IDENTITY, the inserts into the Audit can cause the value of @@IDENTITY to not reflect the value you are expecting. Make VERY sure that all you other SQL is using Scope_Identity() when you want the last identity value assigned (e.g. for inserts)

Sunday, January 27, 2008 7:47 PM by Marc Brooks

# Link Listing - January 27, 2008

MSBuild MSBuildContrib January Release [Via: Scott Dorman ] Sharepoint SharePoint Lists as DataTables...

Monday, January 28, 2008 2:40 AM by Christopher Steen

# Link Listing - January 27, 2008

Link Listing - January 27, 2008

Monday, January 28, 2008 2:40 AM by Christopher Steen

# Mind Gravy &raquo; Blog Archive &raquo; links for 2008-01-28

Pingback from  Mind Gravy  &raquo; Blog Archive   &raquo; links for 2008-01-28

Monday, January 28, 2008 7:28 AM by Mind Gravy » Blog Archive » links for 2008-01-28

# Havagan&#8217;s Mind Spew &raquo; Blog Archive &raquo; SQL 2005 Auditing With Triggers Using XML

Pingback from  Havagan&#8217;s Mind Spew  &raquo; Blog Archive   &raquo; SQL 2005 Auditing With Triggers Using XML

# 6 Links Today (2008-01-28)

Pingback from  6 Links Today (2008-01-28)

Monday, January 28, 2008 10:19 AM by 6 Links Today (2008-01-28)

# ordinal number | News of course

Pingback from  ordinal number | News of course

Tuesday, January 29, 2008 2:10 PM by ordinal number | News of course

# Versioning Databases – Views, Stored Procedures, and the Like

What started as a short brain dump is tuning in to a longer series of posts thanks to all the feedback...

Saturday, February 02, 2008 7:08 PM by K. Scott Allen

# Versioning Databases – Views, Stored Procedures, and the Like

What started as a short brain dump is tuning in to a longer series of posts thanks to all the feedback

Saturday, February 02, 2008 7:34 PM by BusinessRx Reading List

# My Space In The Net &raquo; Blog Archive &raquo; Versioning Databases

Pingback from  My Space In The Net  &raquo; Blog Archive   &raquo; Versioning Databases

# re: Adding simple trigger-based auditing to your SQL Server database

Thanks a lot..... it worked.. performance is not a problem if u filter the tables based on needs .also u can filter updates and primary key of inserts and deletes.... its the simple way to track a small DB.

Thursday, February 07, 2008 6:45 AM by Binoy Bastin.

# re: Adding simple trigger-based auditing to your SQL Server database

I used this audit idea, but i have a question. i am working with an ASP NET app, that works with windows authentication. I want that the user saved in Audit table be the one that is accesing the application. Can this be possible in a domain context?.

Example:

                 Audit Table

AuditId                           UserName

1         .. ... ... ... ... ..   DOMAIN1\user1

2                                 DOMAIN1\user2  

Thursday, February 21, 2008 3:41 PM by Orlando

# re: Adding simple trigger-based auditing to your SQL Server database

Yup, use User_id() to get the spid and then suser_sname() to get the windows name.

Monday, March 03, 2008 4:43 AM by Cees

# Stay Rustico Stay Free &raquo; Auditar en SQL Server 2000

Pingback from  Stay Rustico Stay Free &raquo; Auditar en SQL Server 2000

Monday, March 10, 2008 4:10 PM by Stay Rustico Stay Free » Auditar en SQL Server 2000

# Agregar un simple Trigger para auditar tu base de datos SQL Server

Despues de pasar muchas horas pensando, debuggiando y buscandole la vuelta a una solucion que consistia

Tuesday, March 11, 2008 11:01 AM by Esteban Zavala's Blog

# Agregar un simple Trigger para auditar tu base de datos SQL Server

Pingback from  Agregar un simple Trigger para auditar tu base de datos SQL Server

# Versioning Databases – Views, Stored Procedures, and the Like

What started as a short brain dump is tuning in to a longer series of posts thanks to all the feedback

Tuesday, April 15, 2008 1:54 PM by .Net World

# sell my house fast

Buying a house and making it your home is a dream almost everyone cherishes

Monday, May 05, 2008 7:52 PM by sell my house fast

# raiserror sql server 2005

Pingback from  raiserror sql server 2005

Monday, May 19, 2008 4:20 PM by raiserror sql server 2005

# re: Adding simple trigger-based auditing to your SQL Server database

Jon, This was a great start to what I was looking for.  However I wanted to create a unique audit table for each table.  I modified the code above to create a unique audit table. It may be a little sloppy but here it is if anyone is interest.

DECLARE

@Create_Audit_Table_SQL varchar(MAX),

@Create_Trigger_SQL varchar(MAX),

@TABLE_NAME sysname,

@Trigger_Select_Col_Current varchar(MAX),

@Create_Audit_Col_Old varchar(MAX),

@Trigger_Select_Col_Old varchar(MAX),

@COLUMN VARCHAR(MAX),

@COLUMN_NAME VARCHAR(50)

SET NOCOUNT ON

--Select all the tables in the database but the Audit tables and the sysdiagram table

SELECT @TABLE_NAME = MIN(TABLE_NAME)

FROM

INFORMATION_SCHEMA.TABLES

WHERE

TABLE_TYPE= 'BASE TABLE'

AND (TABLE_NAME != 'sysdiagrams'

AND RIGHT(TABLE_NAME, 6) != '_AUDIT')

--Loop through the tables

WHILE @TABLE_NAME IS NOT NULL

BEGIN

-- Initialize/reset variables

SET @Create_Audit_Table_SQL = 'CREATE TABLE ' + @TABLE_NAME + '_AUDIT ('

SET @Create_Audit_Col_Old = ''

SET @Trigger_Select_Col_Old = ''

SET @Trigger_Select_Col_Current = ''

-- Create a cursor for looping through the columns in the table

DECLARE ColumnInfo_cursor CURSOR FOR

SELECT

CASE

-- Creating the formatted column line for a create table column

WHEN DATA_TYPE IN ('decimal', 'numeric') THEN CONVERT(VARCHAR(MAX), COLUMN_NAME + ' ' +  DATA_TYPE + '(' + CONVERT(VARCHAR(10), NUMERIC_PRECISION) + ',' + CONVERT(VARCHAR(10), NUMERIC_SCALE)+  '), ')

WHEN CHARACTER_MAXIMUM_LENGTH IS NULL OR DATA_TYPE = 'image' OR DATA_TYPE IN ('ntext', 'image', 'text', 'xml', 'sql_variant') THEN  CONVERT(VARCHAR(MAX), COLUMN_NAME + ' ' + DATA_TYPE + ', ')

WHEN CHARACTER_MAXIMUM_LENGTH = -1 THEN CONVERT(VARCHAR(MAX), COLUMN_NAME + ' ' + DATA_TYPE + '(MAX), ')

ELSE CONVERT(VARCHAR(MAX), COLUMN_NAME + ' ' +  DATA_TYPE + '(' + CONVERT(VARCHAR(10), CHARACTER_MAXIMUM_LENGTH) + '), ')

END

, COLUMN_NAME

FROM

INFORMATION_SCHEMA.COLUMNS

Where

TABLE_NAME = @TABLE_NAME

ORDER BY ORDINAL_POSITION

OPEN ColumnInfo_cursor

FETCH NEXT FROM ColumnInfo_cursor

INTO @COLUMN, @COLUMN_NAME

WHILE @@FETCH_STATUS = 0

BEGIN

SET @Create_Audit_Table_SQL = @Create_Audit_Table_SQL + @COLUMN

SET @Create_Audit_Col_Old = @Create_Audit_Col_Old + 'old' + @COLUMN

SET @Trigger_Select_Col_Current = @Trigger_Select_Col_Current + @COLUMN_NAME + ', '

SET @Trigger_Select_Col_Old = @Trigger_Select_Col_Old + 'old' +  @COLUMN_NAME + ', '

FETCH NEXT FROM  ColumnInfo_cursor

INTO @COLUMN, @COLUMN_NAME

END

--Add to more rows to the table for a date and type stamp

SET @Create_Audit_Table_SQL = @Create_Audit_Table_SQL + @Create_Audit_Col_Old + 'AuditDate datetime DEFAULT (GetDate()), AuditType char(1))'

CLOSE ColumnInfo_cursor

DEALLOCATE ColumnInfo_cursor

-- Uncomment out the code line below if you want to drop your audit tables

-- WARNING: You will lose all history of the audit table if you do this

-- EXEC('IF OBJECT_ID (''' + @TABLE_NAME + '_AUDIT'') IS NOT NULL DROP TABLE ' + @TABLE_NAME + '_AUDIT')

-- Create the audit table if it doesn't exist.  Do not create audit tables for Audit tables

IF NOT EXISTS(SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME= @TABLE_NAME + '_AUDIT') AND RIGHT(@TABLE_NAME, 6) != '_AUDIT'

BEGIN

-- PRINT @Create_Audit_Table_SQL

SELECT @Create_Audit_Table_SQL

EXEC(@Create_Audit_Table_SQL)

END

-- Create triggers for all tables except the audit tables

IF RIGHT(@TABLE_NAME, 6) != '_AUDIT'

BEGIN

-- Delete the trigger if it exist

EXEC('IF OBJECT_ID (''' + @TABLE_NAME+ '_ChangeTracking'', ''TR'') IS NOT NULL DROP TRIGGER ' + @TABLE_NAME+ '_ChangeTracking')

-- Create the trigger

SET @Create_Trigger_SQL = 'create trigger ' + @TABLE_NAME+ '_ChangeTracking on ' + @TABLE_NAME+ ' for insert, update, delete

as

declare @AuditType char(1),  @PKCols VARCHAR(MAX), @SQL VARCHAR(MAX)

--Find the Primary keys to be used in the inserted and deleted 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 = ''' + @TABLE_NAME + '''

and CONSTRAINT_TYPE = ''PRIMARY KEY''

and c.TABLE_NAME = pk.TABLE_NAME

and c.CONSTRAINT_NAME = pk.CONSTRAINT_NAME

select * into #ins from inserted

select * into #del from deleted

if exists (select * from inserted)

if exists (select * from deleted)

SET @AuditType = ''U''

else

SET @AuditType = ''I''

else

SET @AuditType = ''D''

EXEC(''INSERT INTO ' + @TABLE_NAME + '_AUDIT ('

-- Add the columns - current and old to the select

SET @Create_Trigger_SQL = @Create_Trigger_SQL + @Trigger_Select_Col_Current + @Trigger_Select_Col_Old + ' AuditType)

SELECT i.*, d.*, '''''' + @AuditType + '''''' FROM #ins i full outer join #del d '' + @PKCols )'

-- PRINT @Create_Trigger_SQL

SELECT @Create_Trigger_SQL

EXEC(@Create_Trigger_SQL)

END

-- Next table

SELECT @TABLE_NAME = MIN(TABLE_NAME) FROM INFORMATION_SCHEMA.Tables

WHERE TABLE_NAME > @TABLE_NAME

AND TABLE_TYPE= 'BASE TABLE'

AND TABLE_NAME!= 'sysdiagrams'

END

Wednesday, May 28, 2008 9:04 PM by Wayne Fontes

# re: Adding simple trigger-based auditing to your SQL Server database

When I execute the code

USE PUBS

GO

IF EXISTS(SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME= 'Audit')

DROP TABLE Audit

CREATE TABLE Audit

(

AuditID [int]IDENTITY(1,1) NOT NULL,

Type char(1),

TableName varchar(128),

PrimaryKeyField varchar(1000),

PrimaryKeyValue varchar(1000),

FieldName varchar(128),

OldValue varchar(1000),

NewValue varchar(1000),

UpdateDate datetime DEFAULT (GetDate()),

UserName varchar(128)

)

GO

DECLARE @sql varchar(8000), @TABLE_NAME sysname

SET NOCOUNT ON

SELECT @TABLE_NAME= MIN(TABLE_NAME)

FROM INFORMATION_SCHEMA.Tables

WHERE

TABLE_TYPE = 'BASE TABLE'

AND TABLE_NAME!= 'sysdiagrams'

AND TABLE_NAME!= 'Audit'

WHILE @TABLE_NAME IS NOT NULL

BEGIN

EXEC('IF OBJECT_ID (''' + @TABLE_NAME + '_ChangeTracking'', ''TR'') IS NOT NULL DROP TRIGGER ' + @TABLE_NAME+ '_ChangeTracking')

SELECT @sql = 'create trigger ' + @TABLE_NAME+ '_ChangeTracking on ' + @TABLE_NAME+ ' for insert, update, delete

as

declare @bit int ,

@field int ,

@maxfield int ,

@char int ,

@fieldname varchar(128) ,

@TableName varchar(128) ,

@PKCols varchar(1000) ,

@sql varchar(2000),

@UpdateDate varchar(21) ,

@UserName varchar(128) ,

@Type char(1) ,

@PKFieldSelect varchar(1000),

@PKValueSelect varchar(1000)

select @TableName = ''' + @TABLE_NAME+ '''

-- date and user

select @UserName = system_user ,

@UpdateDate = convert(varchar(8), getdate(), 112) + '' '' + convert(varchar(12), getdate(), 114)

-- Action

if exists (select * from inserted)

if exists (select * from deleted)

select @Type = ''U''

else

select @Type = ''I''

else

select @Type = ''D''

-- get list of columns

select * into #ins from inserted

select * into #del from deleted

-- Get primary key columns for full outer join

select@PKCols = coalesce(@PKCols + '' and'', '' on'') + '' i.'' + c.COLUMN_NAME + '' = d.'' + c.COLUMN_NAME

from INFORMATION_SCHEMA.TABLE_CONSTRAINTS pk ,

INFORMATION_SCHEMA.KEY_COLUMN_USAGE c

where pk.TABLE_NAME = @TableName

and CONSTRAINT_TYPE = ''PRIMARY KEY''

and c.TABLE_NAME = pk.TABLE_NAME

and c.CONSTRAINT_NAME = pk.CONSTRAINT_NAME

-- Get primary key fields select for insert

select @PKFieldSelect = coalesce(@PKFieldSelect+''+'','''') + '''''''' + COLUMN_NAME + ''''''''

from INFORMATION_SCHEMA.TABLE_CONSTRAINTS pk ,

INFORMATION_SCHEMA.KEY_COLUMN_USAGE c

where pk.TABLE_NAME = @TableName

and CONSTRAINT_TYPE = ''PRIMARY KEY''

and c.TABLE_NAME = pk.TABLE_NAME

and c.CONSTRAINT_NAME = pk.CONSTRAINT_NAME

select @PKValueSelect = coalesce(@PKValueSelect+''+'','''') + ''convert(varchar(100), coalesce(i.'' + COLUMN_NAME + '',d.'' + COLUMN_NAME + ''))''

from INFORMATION_SCHEMA.TABLE_CONSTRAINTS pk ,    

INFORMATION_SCHEMA.KEY_COLUMN_USAGE c  

where  pk.TABLE_NAME = @TableName  

and CONSTRAINT_TYPE = ''PRIMARY KEY''  

and c.TABLE_NAME = pk.TABLE_NAME  

and c.CONSTRAINT_NAME = pk.CONSTRAINT_NAME

if @PKCols is null

begin

raiserror(''no PK on table %s'', 16, -1, @TableName)

return

end

select @field = 0, @maxfield = max(ORDINAL_POSITION) from INFORMATION_SCHEMA.COLUMNS where TABLE_NAME = @TableName

while @field < @maxfield

begin

select @field = min(ORDINAL_POSITION) from INFORMATION_SCHEMA.COLUMNS where TABLE_NAME = @TableName and ORDINAL_POSITION > @field

select @bit = (@field - 1 )% 8 + 1

select @bit = power(2,@bit - 1)

select @char = ((@field - 1) / 8) + 1

if substring(COLUMNS_UPDATED(),@char, 1) & @bit > 0 or @Type in (''I'',''D'')

begin

select @fieldname = COLUMN_NAME from INFORMATION_SCHEMA.COLUMNS where TABLE_NAME = @TableName and ORDINAL_POSITION = @field

select @sql = ''insert Audit (Type, TableName, PrimaryKeyField, PrimaryKeyValue, FieldName, OldValue, NewValue, UpdateDate, UserName)''

select @sql = @sql + '' select '''''' + @Type + ''''''''

select @sql = @sql + '','''''' + @TableName + ''''''''

select @sql = @sql + '','' + @PKFieldSelect

select @sql = @sql + '','' + @PKValueSelect

select @sql = @sql + '','''''' + @fieldname + ''''''''

select @sql = @sql + '',convert(varchar(1000),d.'' + @fieldname + '')''

select @sql = @sql + '',convert(varchar(1000),i.'' + @fieldname + '')''

select @sql = @sql + '','''''' + @UpdateDate + ''''''''

select @sql = @sql + '','''''' + @UserName + ''''''''

select @sql = @sql + '' from #ins i full outer join #del d''

select @sql = @sql + @PKCols

select @sql = @sql + '' where i.'' + @fieldname + '' <> d.'' + @fieldname

select @sql = @sql + '' or (i.'' + @fieldname + '' is null and  d.'' + @fieldname + '' is not null)''

select @sql = @sql + '' or (i.'' + @fieldname + '' is not null and  d.'' + @fieldname + '' is null)''

exec (@sql)

end

end

'

SELECT @sql

EXEC(@sql)

SELECT @TABLE_NAME= MIN(TABLE_NAME) FROM INFORMATION_SCHEMA.Tables

WHERE TABLE_NAME> @TABLE_NAME

AND TABLE_TYPE= 'BASE TABLE'

AND TABLE_NAME!= 'sysdiagrams'

AND TABLE_NAME!= 'Audit'

END

I am getting these errors

Server: Msg 170, Level 15, State 1, Procedure authors_ChangeTracking, Line 37

Line 37: Incorrect syntax near '='.

Server: Msg 170, Level 15, State 1, Procedure discounts_ChangeTracking, Line 37

Line 37: Incorrect syntax near '='.

Server: Msg 170, Level 15, State 1, Procedure employee_ChangeTracking, Line 37

Line 37: Incorrect syntax near '='.

Server: Msg 170, Level 15, State 1, Procedure jobs_ChangeTracking, Line 37

Line 37: Incorrect syntax near '='.

Server: Msg 170, Level 15, State 1, Procedure pub_info_ChangeTracking, Line 37

Line 37: Incorrect syntax near '='.

Server: Msg 170, Level 15, State 1, Procedure publishers_ChangeTracking, Line 37

Line 37: Incorrect syntax near '='.

Server: Msg 170, Level 15, State 1, Procedure roysched_ChangeTracking, Line 37

Line 37: Incorrect syntax near '='.

Server: Msg 170, Level 15, State 1, Procedure sales_ChangeTracking, Line 37

Line 37: Incorrect syntax near '='.

Server: Msg 170, Level 15, State 1, Procedure stores_ChangeTracking, Line 37

Line 37: Incorrect syntax near '='.

Server: Msg 170, Level 15, State 1, Procedure titleauthor_ChangeTracking, Line 37

Line 37: Incorrect syntax near '='.

Server: Msg 170, Level 15, State 1, Procedure titles_ChangeTracking, Line 37

Line 37: Incorrect syntax near '='.

I could not get what is the error, please let me know where the bug is

Tuesday, July 01, 2008 11:43 PM by Syed

# re: Adding simple trigger-based auditing to your SQL Server database

Syed,

Look at (approximately) line 61 of your code:

select@PKCols = coalesce(@PKCols + '' and'', '' on'') + '' i.'' + c.COLUMN_NAME + '' = d.'' + c.COLUMN_NAME

Looks like you're missing a space between "select" and "@PKCols".

Wednesday, July 02, 2008 7:15 AM by Rich Streicher

# Audit options with NHibernate

Pingback from  Audit options with NHibernate

Friday, July 11, 2008 9:58 AM by Audit options with NHibernate

# re: Adding simple trigger-based auditing to your SQL Server database

Great Blog...Thanks Jon and others for the information. This auditing technique indeed can be used as a lightweight auditing scheme for application with light auditing requirements.

Friday, July 11, 2008 2:56 PM by Charles Barnett

# 112 &raquo; Blog Archive &raquo; re: Adding simple trigger-based auditing to your SQL Server database

Pingback from  112  &raquo; Blog Archive   &raquo; re: Adding simple trigger-based auditing to your SQL Server database

Leave a Comment

(required) 
(required) 
(optional)
(required)