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
 Need input on trigger based database auditing

Author  Topic 

egil
Starting Member

16 Posts

Posted - 2008-06-05 : 12:37:55
Hi

I am building a pretty simple intranet application where we need to be able to track changes to tables. The tracking feature do not need to be very advanced, we just need to see who changed something and what it was. Therefore I decided just to use a trigger based solution, but need some input/advice since my SQL skills is somewhat lacking.

Consider the following (mock-up) schema:


-- My content table
CREATE TABLE [Content](
[ContentGuid] [uniqueidentifier] NOT NULL PRIMARY KEY DEFAULT (newid()),
[Data] [nvarchar](4000) NOT NULL,
[ChangedBy] [nchar](10) NOT NULL,
[MaybeNull] [int] NULL
)

-- My history table
CREATE TABLE [History](
[ChangedTable] [nvarchar](50) NOT NULL,
[ReferenceGuid] [uniqueidentifier] NOT NULL,
[ChangedBy] [nchar](10) NOT NULL,
[ChangedOn] [datetime] NOT NULL DEFAULT (getutcdate()),
[IsDelete] [bit] NOT NULL DEFAULT ((0)),
[Changes] [xml] NOT NULL
) ON [PRIMARY]

-- My insert/update trigger
CREATE TRIGGER [RecordChangeOnInsertUpdate]
ON [Content]
AFTER INSERT,UPDATE
AS
BEGIN
SET NOCOUNT ON;

DECLARE @Changes xml
SET @Changes = (select * from [inserted] for xml raw ('Content'), elements xsinil)

INSERT INTO [History]
([ChangedTable]
,[ReferenceGuid]
,[ChangedBy]
,[ChangedOn]
,[IsDelete]
,[Changes])
SELECT 'Content', ContentGuid, ChangedBy, getutcdate(), 0, @Changes
FROM [inserted]
END

-- My delete trigger
CREATE TRIGGER [RecordChangeOnDelete]
ON [Content]
AFTER DELETE
AS
BEGIN
SET NOCOUNT ON;

DECLARE @Changes xml
SET @Changes = (select * from [deleted] for xml raw ('Content'), elements xsinil)

INSERT INTO [History]
([ChangedTable]
,[ReferenceGuid]
,[ChangedBy]
,[ChangedOn]
,[IsDelete]
,[Changes])
SELECT 'Content', ContentGuid, ChangedBy, getutcdate(), 1, @Changes
FROM [deleted]
END


I have decided to use the "one history table for all table changes" method. The changes for a row is stored in a xml column which obviously limits the total size of columns in a table being tracked, but that is not a problem in my application. In general I like this set up, since I will be able to change the schema continuously without having to change the triggers, and since the application will probably evolve a lot over the coming months this is pretty important to me.

EDIT: I should add that all the tables I will be tracking have a uniqueidentifier column. This makes it possible to related table rows in the different tables being tracked with their history.

A few concerns with the above SQL:

- Can the inserted/deleted table change between "SET @Changes = (select * ..." and the "INSERT INTO ..." statement, such that the data is not valid? If so, how to work around that?

- If I were to (hypothetically) perform a "UPDATE [Content] SET [Data] = 'something'", not only is my update trigger called once for each row updated, but the XML added to the inserted row in the history table ([History].[Changes]) represent all the rows updated in the batch update. How do I get around this?

Are there any other issues I should be aware of?

Regards, Egil.

Page47
Master Smack Fu Yak Hacker

2878 Posts

Posted - 2008-06-05 : 13:24:09
quote:
Can the inserted/deleted table change between...

No
quote:
If I were to (hypothetically) perform a "UPDATE [Content] SET [Data] = 'something'", not only is my update trigger called once for each row updated....

False ... you trigger will run once
quote:
...but the XML added to the inserted row in the history table ([History].[Changes]) represent all the rows updated in the batch update.

True ... you will insert one row into [History]. If you want one row in [History] for each row in INSERTED/DELETED, you'll need to either loop through INSERTED/DELETED (weak) or write you insert into [History] in a set based manner (the true path).

Jay
to here knows when
Go to Top of Page

egil
Starting Member

16 Posts

Posted - 2008-06-05 : 13:40:43
Hi Jay

Thanks for replying.

quote:
False ... you trigger will run once


Are you completely sure? I just did a quick test. Added two rows to the [Content] table and then did the mentioned updated, and two new rows was added the [History] table.

I am running SQL Server 2005 in case that makes a difference.

quote:
True ... you will insert one row into [History]. If you want one row in [History] for each row in INSERTED/DELETED, you'll need to either loop through INSERTED/DELETED (weak) or write you insert into [History] in a set based manner (the true path).


Curious, how do I do a set based insert?
Go to Top of Page

tkizer
Almighty SQL Goddess

38200 Posts

Posted - 2008-06-05 : 14:09:43
egil,

It depends how you did the INSERT. If you ran two INSERT statements to add the two rows, then you ran two batches and therefore the trigger fired twice. If instead you added the two rows in one INSERT statement, then you ran one batch and therefore the trigger fired once and needs to be written to handle multiple rows.

See this for an example of a trigger that handles multiple rows:
http://weblogs.sqlteam.com/tarad/archive/2004/09/14/2077.aspx

Tara Kizer
Microsoft MVP for Windows Server System - SQL Server
http://weblogs.sqlteam.com/tarad/

Database maintenance routines:
http://weblogs.sqlteam.com/tarad/archive/2004/07/02/1705.aspx
Go to Top of Page

Page47
Master Smack Fu Yak Hacker

2878 Posts

Posted - 2008-06-05 : 14:24:50
If you have 10 rows in [Content] and you run:
update [Content] set [Data] = 'something'


...your trigger will run 1 time, not 10 times.

Jay
to here knows when
Go to Top of Page

egil
Starting Member

16 Posts

Posted - 2008-06-05 : 14:42:22
quote:
Originally posted by Page47

If you have 10 rows in [Content] and you run:
update [Content] set [Data] = 'something'


...your trigger will run 1 time, not 10 times.



Hmm. Why do I get two new rows in my [History] table when doing the update on [Content] table (which has a total of two rows) with
update [Content] set [Data] = 'something'
?

Regards, Egil.
Go to Top of Page

Page47
Master Smack Fu Yak Hacker

2878 Posts

Posted - 2008-06-05 : 15:01:02
Well, it is not because the trigger fired twice. Promise.

Take a look at you query in the trigger. Insert into History...select from inserted. You've got two rows in inserted, right?

Jay
to here knows when
Go to Top of Page

egil
Starting Member

16 Posts

Posted - 2008-06-05 : 15:29:30
Ahh sorry Jay, you are of cause right.

I am trying to write a set-based solution that should solve that problem, if I understand you guys correct.
I think I understand what I need to do - something like this:


WITH ChangedContent (ContentGuid, Changes)
AS
(
... this is where I get stuck. I need to call
(select * from [inserted] for xml raw ('Content'), elements xsinil)
for each row in [inserted] such that my temp ChangedContent table
contains ContentGuid and associated xml representation of the row
)
INSERT INTO [History]
([ChangedTable]
,[ReferenceGuid]
,[ChangedBy]
,[ChangedOn]
,[IsDelete]
,[Changes])
SELECT 'Content', ContentGuid, ChangedBy, getutcdate(), 0, @Changes
FROM [inserted] JOIN ChangedContent ON [inserted].ContentGuid == ChangedContent.ContentGuid


Am I on the right path or?

Thanks for the help, Egil.
Go to Top of Page

tkizer
Almighty SQL Goddess

38200 Posts

Posted - 2008-06-05 : 15:43:32
Why do you need a CTE for this?

Tara Kizer
Microsoft MVP for Windows Server System - SQL Server
http://weblogs.sqlteam.com/tarad/

Database maintenance routines:
http://weblogs.sqlteam.com/tarad/archive/2004/07/02/1705.aspx
Go to Top of Page

egil
Starting Member

16 Posts

Posted - 2008-06-05 : 15:56:56
tkizer: I probably don't :)

I managed to get the below code to work as I want, but please tell me if there is a better way.

with ChangeTable (TableName, ContentGuid, ChangedBy, ChangedOn, IsDelete, Changes)
as (select 'Content', c2.ContentGuid, c2.ChangedBy, getutcdate(), 0, (select * from [inserted] c1 where c1.ContentGuid = c2.ContentGuid for xml raw ('Content'), elements xsinil) as Changes
from [inserted] c2
)
insert into [History]
([ChangedTable]
,[ReferenceGuid]
,[ChangedBy]
,[ChangedOn]
,[IsDelete]
,[Changes])
select TableName, ContentGuid, ChangedBy, ChangedOn, Isdelete, Changes
from ChangeTable


Regards, Egil.
Go to Top of Page

tkizer
Almighty SQL Goddess

38200 Posts

Posted - 2008-06-05 : 16:00:29
I would think you'd just use this without the CTE:

INSERT INTO [History]
([ChangedTable]
,[ReferenceGuid]
,[ChangedBy]
,[ChangedOn]
,[IsDelete]
,[Changes])
SELECT 'Content', ContentGuid, ChangedBy, getutcdate(), 0, @Changes
FROM [inserted]
JOIN ChangedContent
ON [inserted].ContentGuid = ChangedContent.ContentGuid


Tara Kizer
Microsoft MVP for Windows Server System - SQL Server
http://weblogs.sqlteam.com/tarad/

Database maintenance routines:
http://weblogs.sqlteam.com/tarad/archive/2004/07/02/1705.aspx
Go to Top of Page

egil
Starting Member

16 Posts

Posted - 2008-06-05 : 16:26:31
Ahh yes, that works too Tara. Thanks.

Could a query like this be written better?

SELECT
c2.ContentGuid,
(SELECT * FROM [Content] c1 WHERE c1.ContentGuid = c2.ContentGuid FOR XML RAW ('Content'), ELEMENTS XSINIL)
FROM [Content] c2;

It seems odd to me to have a nested select statement where I have.

Regards, Egil.
Go to Top of Page

tkizer
Almighty SQL Goddess

38200 Posts

Posted - 2008-06-05 : 16:32:09
I haven't a clue what that does.

Tara Kizer
Microsoft MVP for Windows Server System - SQL Server
http://weblogs.sqlteam.com/tarad/

Database maintenance routines:
http://weblogs.sqlteam.com/tarad/archive/2004/07/02/1705.aspx
Go to Top of Page

egil
Starting Member

16 Posts

Posted - 2008-06-05 : 16:40:28
Ahh sorry. The select statement builds the ChangedContent content table in your example above. The [Content] in the statement should be replaced with [inserted].
Go to Top of Page
   

- Advertisement -