44

The table :

    CREATE TABLE GUESTS (
      GUEST_ID int IDENTITY(1,1) PRIMARY KEY, 
      GUEST_NAME VARCHAR(50), 
      GUEST_SURNAME VARCHAR(50), 
      ADRESS VARCHAR(100), 
      CITY VARCHAR(50), 
      CITY_CODE VARCHAR(10), 
      COUNTRY VARCHAR(50), 
      STATUS VARCHAR(20), 
      COMMENT nvarchar(max);

For the logging :

CREATE TABLE AUDIT_GUESTS (
  ID int IDENTITY(1,1) PRIMARY KEY, 
  GUEST_ID int,
  OLD_GUEST_NAME VARCHAR(50), 
  NEW_GUEST_NAME VARCHAR(50), 
  OLD_GUEST_SURNAME VARCHAR(50), 
  NEW_GUEST_SURNAME VARCHAR(50),
  OLD_ADRESS VARCHAR(100), 
  NEW_ADRESS VARCHAR(100),
  OLD_CITY VARCHAR(50), 
  NEW_CITY VARCHAR(50),
  OLD_CITY_CODE VARCHAR(10), 
  NEW_CITY_CODE VARCHAR(10), 
  OLD_COUNTRY VARCHAR(50), 
  NEW_COUNTRY VARCHAR(50), 
  OLD_STATUS VARCHAR(20), 
  NEW_STATUS VARCHAR(20), 
  OLD_COMMENT nvarchar(max), 
  NEW_COMMENT nvarchar(max), 
  AUDIT_ACTION varchar(100),
  AUDIT_TIMESTAMP datetime);

I would like to create a trigger on my GUESTS table to log all changes in my AUDIT_GUESTS table. How can I do that in SQL Server 2014 Express ?

I tried :

create TRIGGER trgAfterUpdate ON [dbo].[GUESTS] 
FOR UPDATE
AS
    declare @GUEST_ID int;
    declare @GUEST_NAME varchar(50);
    declare @GUEST_SURNAME VARCHAR(50);
    declare @ADRESS VARCHAR(100); 
    declare @CITY VARCHAR(50);
    declare @CITY_CODE VARCHAR(10); 
    declare @COUNTRY VARCHAR(50);
    declare @STATUS VARCHAR(20);
    declare @COMMENT nvarchar(max);
    declare @AUDIT_ACTION varchar(100);
    declare @AUDIT_TIMESTAMP datetime;

    select @GUEST_ID=i.GUEST_ID from inserted i;            
    select @GUEST_NAME=i.GUEST_NAME from inserted i;    
    select @GUEST_SURNAME=i.GUEST_SURNAME from inserted i;
    select @ADRESS=i.ADRESS from inserted i;
    select @CITY=i.CITY from inserted i;
    select @CITY_CODE=i.CITY_CODE from inserted i;
    select @COUNTRY=i.COUNTRY from inserted i;
    select @STATUS=i.STATUS from inserted i;
    select @COMMENT=i.COMMENT from inserted i;

        if update(GUEST_NAME)
        set @audit_action='Updated Record -- After Update Trigger.';

        if update(GUEST_SURNAME)
        set @audit_action='Updated Record -- After Update Trigger.';

        if update(ADRESS)
        set @audit_action='Updated Record -- After Update Trigger.';

        if update(CITY)
        set @audit_action='Updated Record -- After Update Trigger.';

        if update(CITY_CODE)
        set @audit_action='Updated Record -- After Update Trigger.';

        if update(COUNTRY)
        set @audit_action='Updated Record -- After Update Trigger.';

        if update(STATUS)
        set @audit_action='Updated Record -- After Update Trigger.';

        if update(COMMENT)
        set @audit_action='Updated Record -- After Update Trigger.';

        insert into AUDIT_GUESTS
           (GUEST_ID,GUEST_NAME,GUEST_SURNAME,ADRESS,CITY,CITY_CODE,COUNTRY,STATUS,COMMENT,audit_action,AUDIT_TIMESTAMP) 
    values(@GUEST_ID,@GUEST_NAME,@GUEST_SURNAME,@ADRESS,@CITY,@CITY_CODE,@COUNTRY,@STATUS,@COMMENT,@audit_action,getdate());
    GO

Works kind of ok but I would like to see old-new values.

In SQLite I had :

CREATE TRIGGER [LOG_UPDATE]
AFTER UPDATE OF [GUEST_NAME], [GUEST_SURNAME], [ADRESS], [CITY], [CITY_CODE], [COUNTRY], [STATUS], [COMMENT]
ON [GUESTS]
BEGIN
INSERT INTO GUESTS_LOG
 ( GUEST_ID,
   NAME_OLD,NAME_NEW,
   SURNAME_OLD,SURNAME_NEW,
   ADRESS_OLD,ADRESS_NEW,
   CITY_OLD,CITY_NEW,
   CITY_CODE_OLD,CITY_CODE_NEW,
   COUNTRY_OLD,COUNTRY_NEW,
   STATUS_OLD,STATUS_NEW,   
   COMMENT_OLD,COMMENT_NEW,sqlAction,DATE_TIME)   

   VALUES   

 (OLD.GUEST_ID,
  OLD.GUEST_NAME,NEW.GUEST_NAME, 
  OLD.GUEST_SURNAME,NEW.GUEST_SURNAME,
  OLD.ADRESS,NEW.ADRESS,
  OLD.CITY,NEW.CITY,
  OLD.CITY_CODE,NEW.CITY_CODE,
  OLD.COUNTRY,NEW.COUNTRY,  
  OLD.STATUS,NEW.STATUS,
  OLD.COMMENT,NEW.COMMENT,'record changed',datetime('now','localtime'));  

END

and it worked OK. Just dont know how to pass this to SQL server. Just begun learning it.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
user763539
  • 3,509
  • 6
  • 44
  • 103
  • 9
    Your first and fundamental flaw in the trigger is assuming that `Inserted` and `Deleted` only contain a single row - that is **NOT** the case! If your `UPDATE` statement affects 10 rows at once, your trigger is fired **once** and has **10 rows** in both `Inserted` and `Deleted` - you need to work with a **set-based** approach and not just select what you think is the only value from those pseudo tables! – marc_s Nov 02 '13 at 07:26
  • From SQL Server 2016 and onwards there's built in support for this, see https://learn.microsoft.com/en-us/sql/relational-databases/tables/temporal-tables?view=sql-server-ver15 – Rory Feb 03 '22 at 11:18

7 Answers7

87

Take a look at this article on Simple-talk.com by Pop Rivett. It walks you through creating a generic trigger that will log the OLDVALUE and the NEWVALUE for all updated columns. The code is very generic and you can apply it to any table you want to audit, also for any CRUD operation i.e. INSERT, UPDATE and DELETE. The only requirement is that your table to be audited should have a PRIMARY KEY (which most well designed tables should have anyway).

Here's the code relevant for your GUESTS Table.

  1. Create AUDIT Table.
    IF NOT EXISTS
          (SELECT * FROM sysobjects WHERE id = OBJECT_ID(N'[dbo].[Audit]') 
                   AND OBJECTPROPERTY(id, N'IsUserTable') = 1)
           CREATE TABLE Audit 
                   (Type CHAR(1), 
                   TableName VARCHAR(128), 
                   PK VARCHAR(1000), 
                   FieldName VARCHAR(128), 
                   OldValue VARCHAR(1000), 
                   NewValue VARCHAR(1000), 
                   UpdateDate datetime, 
                   UserName VARCHAR(128))
    GO
  1. CREATE an UPDATE Trigger on the GUESTS Table as follows.
    CREATE TRIGGER TR_GUESTS_AUDIT ON GUESTS FOR UPDATE
    AS
    
    DECLARE @bit INT ,
           @field INT ,
           @maxfield INT ,
           @char INT ,
           @fieldname VARCHAR(128) ,
           @TableName VARCHAR(128) ,
           @PKCols VARCHAR(1000) ,
           @sql VARCHAR(2000), 
           @UpdateDate VARCHAR(21) ,
           @UserName VARCHAR(128) ,
           @Type CHAR(1) ,
           @PKSelect VARCHAR(1000)
           
    
    --You will need to change @TableName to match the table to be audited. 
    -- Here we made GUESTS for your example.
    SELECT @TableName = 'GUESTS'
    
    -- date and user
    SELECT         @UserName = SYSTEM_USER ,
           @UpdateDate = CONVERT (NVARCHAR(30),GETDATE(),126)
    
    -- Action
    IF EXISTS (SELECT * FROM inserted)
           IF EXISTS (SELECT * FROM deleted)
                   SELECT @Type = 'U'
           ELSE
                   SELECT @Type = 'I'
    ELSE
           SELECT @Type = 'D'
    
    -- get list of columns
    SELECT * INTO #ins FROM inserted
    SELECT * INTO #del FROM deleted
    
    -- Get primary key columns for full outer join
    SELECT @PKCols = COALESCE(@PKCols + ' and', ' on') 
                   + ' i.' + c.COLUMN_NAME + ' = d.' + c.COLUMN_NAME
           FROM    INFORMATION_SCHEMA.TABLE_CONSTRAINTS pk ,
    
                  INFORMATION_SCHEMA.KEY_COLUMN_USAGE c
           WHERE   pk.TABLE_NAME = @TableName
           AND     CONSTRAINT_TYPE = 'PRIMARY KEY'
           AND     c.TABLE_NAME = pk.TABLE_NAME
           AND     c.CONSTRAINT_NAME = pk.CONSTRAINT_NAME
    
    -- Get primary key select for insert
    SELECT @PKSelect = COALESCE(@PKSelect+'+','') 
           + '''<' + COLUMN_NAME 
           + '=''+convert(varchar(100),
    coalesce(i.' + COLUMN_NAME +',d.' + COLUMN_NAME + '))+''>''' 
           FROM    INFORMATION_SCHEMA.TABLE_CONSTRAINTS pk ,
                   INFORMATION_SCHEMA.KEY_COLUMN_USAGE c
           WHERE   pk.TABLE_NAME = @TableName
           AND     CONSTRAINT_TYPE = 'PRIMARY KEY'
           AND     c.TABLE_NAME = pk.TABLE_NAME
           AND     c.CONSTRAINT_NAME = pk.CONSTRAINT_NAME
    
    IF @PKCols IS NULL
    BEGIN
           RAISERROR('no PK on table %s', 16, -1, @TableName)
           RETURN
    END
    
    SELECT         @field = 0, 
           @maxfield = MAX(ORDINAL_POSITION) 
           FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = @TableName
    WHILE @field < @maxfield
    BEGIN
           SELECT @field = MIN(ORDINAL_POSITION) 
                   FROM INFORMATION_SCHEMA.COLUMNS 
                   WHERE TABLE_NAME = @TableName 
                   AND ORDINAL_POSITION > @field
           SELECT @bit = (@field - 1 )% 8 + 1
           SELECT @bit = POWER(2,@bit - 1)
           SELECT @char = ((@field - 1) / 8) + 1
           IF SUBSTRING(COLUMNS_UPDATED(),@char, 1) & @bit > 0
                                           OR @Type IN ('I','D')
           BEGIN
                   SELECT @fieldname = COLUMN_NAME 
                           FROM INFORMATION_SCHEMA.COLUMNS 
                           WHERE TABLE_NAME = @TableName 
                           AND ORDINAL_POSITION = @field
                   SELECT @sql = '
    insert Audit (    Type, 
                   TableName, 
                   PK, 
                   FieldName, 
                   OldValue, 
                   NewValue, 
                   UpdateDate, 
                   UserName)
    select ''' + @Type + ''',''' 
           + @TableName + ''',' + @PKSelect
           + ',''' + @fieldname + ''''
           + ',convert(varchar(1000),d.' + @fieldname + ')'
           + ',convert(varchar(1000),i.' + @fieldname + ')'
           + ',''' + @UpdateDate + ''''
           + ',''' + @UserName + ''''
           + ' from #ins i full outer join #del d'
           + @PKCols
           + ' where i.' + @fieldname + ' <> d.' + @fieldname 
           + ' or (i.' + @fieldname + ' is null and  d.'
                                    + @fieldname
                                    + ' is not null)' 
           + ' or (i.' + @fieldname + ' is not null and  d.' 
                                    + @fieldname
                                    + ' is null)' 
                   EXEC (@sql)
           END
    END
    
    GO
Dale K
  • 25,246
  • 15
  • 42
  • 71
Shiva
  • 20,575
  • 14
  • 82
  • 112
  • This requires various combinations which tend to get too long. Nice way is to have old and new values. That's why I asked. How can I log old-new values? – user763539 Nov 02 '13 at 01:22
  • 1
    Ok, so you are looking for a way to log ANY field updated to the log table. If 3 fields were updated, you want to log 3 separate rows in audit, 1 per field changed with OLD and NEW values in that audit row? – Shiva Nov 02 '13 at 01:25
  • Yes, exactly... I just put 3 fields to make it look simple. Basically I want to log all of them. – user763539 Nov 02 '13 at 01:27
  • Got it! I updated my answer to suit your needs now. I tried it on Northwind database Contact table and it works. You can use this solution for any table like I mentioned in my answer. – Shiva Nov 02 '13 at 01:45
  • You lost me here.... dont have northwind so I dont know what fields are there. Can you refrase the example according to the fields I have provided? – user763539 Nov 02 '13 at 01:56
  • What I meant was that I created that trigger on the Northwind sample SQL database to make sure the code will work. so the code in my answer is now updated the code so it will work for your GUESTS table. Just run it inside the database. Then execute some UPDATE statements and select * from the Audit table, you should see 1 audit row per field changed. – Shiva Nov 02 '13 at 03:12
  • 3
    The real answer is what Shiva has here from pop-rivetts – Joe Johnston Dec 31 '13 at 17:50
  • Is it possible in PostgreSQL?? – fLen Jun 30 '16 at 07:30
  • @Shiva I think I found a rare case where this code doesn't work. ( it works on all of my tables except one and that one has nothing strange compare to the others) – Royi Namir Jan 13 '17 at 10:33
  • Can you post your code on github or somewhere and share it? – Shiva Jan 14 '17 at 00:19
  • 9
    You should change your code . see [this](http://chat.stackexchange.com/transcript/message/34774768#34774768) – Royi Namir Jan 14 '17 at 16:49
  • @RoyiNamir I think you should post that as an alternative answer. Your comment isn't visible when you first visit this page! – Syntax Error Mar 26 '18 at 10:13
  • @MrJF Yeah I know , I didn't had the time :) – Royi Namir Mar 26 '18 at 10:13
  • 1
    @Shiva noob question: How come the trigger is only for UPDATE events? Does it work, will it work if I delete or insert a record? We are after an DELETE trigger as well, in case people delete data, which they should not do. Thanks – Fandango68 Aug 07 '18 at 05:24
  • @RoyiNamir, I'm not sure why you are listed as an editor of this answer but you haven't updated the code with the corrections. [Another answer](https://stackoverflow.com/a/50593808/1898524) on this page from **dwilli** contains your corrections as well as another fix related to column names containing spaces. – Ben Feb 17 '19 at 04:38
  • @Shiva If I am not mistaken your code will throw an error because the updatedate in audit table is datetime while trigger operates with string value. – user763539 May 28 '19 at 20:52
18

This is the code with two bug fixes. The first bug fix was mentioned by Royi Namir in the comment on the accepted answer to this question. The bug is described on StackOverflow at Bug in Trigger Code. The second one was found by @Fandango68 and fixes columns with multiples words for their names.

ALTER TRIGGER [dbo].[TR_person_AUDIT]
ON [dbo].[person]
FOR UPDATE
AS
           DECLARE @bit            INT,
                   @field          INT,
                   @maxfield       INT,
                   @char           INT,
                   @fieldname      VARCHAR(128),
                   @TableName      VARCHAR(128),
                   @PKCols         VARCHAR(1000),
                   @sql            VARCHAR(2000),
                   @UpdateDate     VARCHAR(21),
                   @UserName       VARCHAR(128),
                   @Type           CHAR(1),
                   @PKSelect       VARCHAR(1000)


           --You will need to change @TableName to match the table to be audited.
           -- Here we made GUESTS for your example.
           SELECT @TableName = 'PERSON'

           SELECT @UserName = SYSTEM_USER,
                  @UpdateDate = CONVERT(NVARCHAR(30), GETDATE(), 126)

           -- Action
           IF EXISTS (
                  SELECT *
                  FROM   INSERTED
              )
               IF EXISTS (
                      SELECT *
                      FROM   DELETED
                  )
                   SELECT @Type = 'U'
               ELSE
                   SELECT @Type = 'I'
           ELSE
               SELECT @Type = 'D'

           -- get list of columns
           SELECT * INTO #ins
           FROM   INSERTED

           SELECT * INTO #del
           FROM   DELETED

           -- Get primary key columns for full outer join
           SELECT @PKCols = COALESCE(@PKCols + ' and', ' on') 
                  + ' i.[' + c.COLUMN_NAME + '] = d.[' + c.COLUMN_NAME + ']'
           FROM   INFORMATION_SCHEMA.TABLE_CONSTRAINTS pk,
                  INFORMATION_SCHEMA.KEY_COLUMN_USAGE c
           WHERE  pk.TABLE_NAME = @TableName
                  AND CONSTRAINT_TYPE = 'PRIMARY KEY'
                  AND c.TABLE_NAME = pk.TABLE_NAME
                  AND c.CONSTRAINT_NAME = pk.CONSTRAINT_NAME

           -- Get primary key select for insert
           SELECT @PKSelect = COALESCE(@PKSelect + '+', '') 
                  + '''<[' + COLUMN_NAME 
                  + ']=''+convert(varchar(100),
           coalesce(i.[' + COLUMN_NAME + '],d.[' + COLUMN_NAME + ']))+''>'''
           FROM   INFORMATION_SCHEMA.TABLE_CONSTRAINTS pk,
                  INFORMATION_SCHEMA.KEY_COLUMN_USAGE c
           WHERE  pk.TABLE_NAME = @TableName
                  AND CONSTRAINT_TYPE = 'PRIMARY KEY'
                  AND c.TABLE_NAME = pk.TABLE_NAME
                  AND c.CONSTRAINT_NAME = pk.CONSTRAINT_NAME

           IF @PKCols IS NULL
           BEGIN
               RAISERROR('no PK on table %s', 16, -1, @TableName)

               RETURN
           END

           SELECT @field = 0,
                  -- @maxfield = MAX(COLUMN_NAME) 
                  @maxfield = -- FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = @TableName


                  MAX(
                      COLUMNPROPERTY(
                          OBJECT_ID(TABLE_SCHEMA + '.' + @TableName),
                          COLUMN_NAME,
                          'ColumnID'
                      )
                  )
           FROM   INFORMATION_SCHEMA.COLUMNS
           WHERE  TABLE_NAME = @TableName






           WHILE @field < @maxfield
           BEGIN
               SELECT @field = MIN(
                          COLUMNPROPERTY(
                              OBJECT_ID(TABLE_SCHEMA + '.' + @TableName),
                              COLUMN_NAME,
                              'ColumnID'
                          )
                      )
               FROM   INFORMATION_SCHEMA.COLUMNS
               WHERE  TABLE_NAME = @TableName
                      AND COLUMNPROPERTY(
                              OBJECT_ID(TABLE_SCHEMA + '.' + @TableName),
                              COLUMN_NAME,
                              'ColumnID'
                          ) > @field

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

               SELECT @bit = POWER(2, @bit - 1)

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





               IF SUBSTRING(COLUMNS_UPDATED(), @char, 1) & @bit > 0
                  OR @Type IN ('I', 'D')
               BEGIN
                   SELECT @fieldname = COLUMN_NAME
                   FROM   INFORMATION_SCHEMA.COLUMNS
                   WHERE  TABLE_NAME = @TableName
                          AND COLUMNPROPERTY(
                                  OBJECT_ID(TABLE_SCHEMA + '.' + @TableName),
                                  COLUMN_NAME,
                                  'ColumnID'
                              ) = @field



                   SELECT @sql = 
                          '
           insert into Audit (    Type, 
           TableName, 
           PK, 
           FieldName, 
           OldValue, 
           NewValue, 
           UpdateDate, 
           UserName)
           select ''' + @Type + ''',''' 
                          + @TableName + ''',' + @PKSelect
                          + ',''' + @fieldname + ''''
                          + ',convert(varchar(1000),d.' + @fieldname + ')'
                          + ',convert(varchar(1000),i.' + @fieldname + ')'
                          + ',''' + @UpdateDate + ''''
                          + ',''' + @UserName + ''''
                          + ' from #ins i full outer join #del d'
                          + @PKCols
                          + ' where i.' + @fieldname + ' <> d.' + @fieldname 
                          + ' or (i.' + @fieldname + ' is null and  d.'
                          + @fieldname
                          + ' is not null)' 
                          + ' or (i.' + @fieldname + ' is not null and  d.' 
                          + @fieldname
                          + ' is null)' 



                   EXEC (@sql)
               END
           END
dwilli
  • 643
  • 3
  • 11
  • 21
  • Could you explain your answer more, as to what the "bug" was and how you'd fixed it? Slapping code with no explanation is poor. – Fandango68 Aug 07 '18 at 05:26
  • 3
    @Fandango68 Thanks for your request and there's no need for the judgemental language. The bug is noted in one of the comments on the previous answer and I will provide the link clearly in my answer. By the way, I didn't fix it, I'm just trying to be helpful by providing the fixed code close to the code with the bug. – dwilli Aug 09 '18 at 03:37
  • my apologies. It happens too often imo. There is another bug too. Tables with columns names that have spaces between words won't work. All column names have to be single words. The fix is easy. Place "[" and "]" chars around the logic. – Fandango68 Aug 09 '18 at 04:28
  • @Fandango68 You're right, and I certainly have my own pet peeves. Anyway, your point was good. It wasn't clear what I was posting. I hope it's better now. Did you intend for me to include your fix in the code I posted? – dwilli Aug 09 '18 at 04:34
  • 1
    Yeah why not? Place those characters around where ever the COLUMN_NAME is referenced. Then I think your answer should have been the official and final answer! :P – Fandango68 Aug 09 '18 at 04:53
  • I haven't tried the code from this answer but I have used Pop Rivett's original method. I noticed that the code on his site and in the most accepted answer on this page, do not contain the corrections that are in the comments with each article. *This answer* should be getting more attention. – Ben Feb 17 '19 at 04:30
  • I ran into the same problem "ORDINAL_POSITION". It took me hours and finally I found this post. Pop Rivett's original method works when no columns deleted/moved position, but you will run into problem when the position of column moved around/deleted. Using @dwilli's method has solved the problem. Thank you. – Milacay Mar 08 '19 at 19:10
  • @dwilli I think it is a bad practice to use strings for dates in audit table. – user763539 May 28 '19 at 21:11
  • @user763539 - Thanks for your input. Could you please be more specific about what you see that's bad practice? The script builds an insert statement that includes a string for the UpdateDate field, but when the script is run SqlServer will insert date DateTime data into the table if that's the data type for the UpdateDate column. Otherwise, I'm not sure what you mean. – dwilli May 28 '19 at 22:24
  • @dwilli what I mean is that if you change UpdateDate in the audit table to datetime you will get the conversion error (the conversion of varchar data type to datetime resulted in an out of range value). – user763539 May 31 '19 at 02:04
  • @user763539 I understand. When I used this trigger the Audit table already had the DateTime type on the UpdateDate column so it worked fine. Do you have a suggested correction that works better? – dwilli May 31 '19 at 02:19
  • 1. why save old and new column values? Effectively the target table row contains the new value. Surely it would save considerable space and avoid duplication by only saving the difference, i.e. the old/original column value; 2. saving the table name also seems superfluous. The table name is `OBJECT_NAME(parent_id)` of the trigger so saving the parent_id allows the table name to be derived by saving `parent_id` and joining with `INFORMATION_SCHEMA.TABLES` for consumption; and 3. for a generic trigger, `@TableName` can be derived `WHERE sys.objects.name = OBJECT_NAME(@@PROCID)` – David Clarke Aug 26 '20 at 21:44
  • i tried that trigger and is very good, but with masive changes are too slow. :-( – IcuScc Jan 03 '23 at 17:41
  • @dwilli, / all, Thank you for your help on this! In learning triggers, the research is suggesting loops or cursors in a trigger is a bad idea. Since this has a while loop in the code, is this considered bad practice now, and if not, why would this loop be okay in a trigger? Thanks again for your help! – JM1 Aug 18 '23 at 11:33
  • @JM1 You're quite welcome! I think that updating the trigger table in a loop, which a lot of people might try to do, would produce unpredictable results. This trigger loops through the system table to find data about the trigger table and uses that data to insert a new record in the Audit table, so it never updates the trigger table while it's in the loop. – dwilli Aug 19 '23 at 00:01
10

I know this is old, but maybe this will help someone else.

Do not log "new" values. Your existing table, GUESTS, has the new values. You'll have double entry of data, plus your DB size will grow way too fast that way.

I cleaned this up and minimized it for this example, but here is the tables you'd need for logging off changes:

CREATE TABLE GUESTS (
      GuestID INT IDENTITY(1,1) PRIMARY KEY, 
      GuestName VARCHAR(50), 
      ModifiedBy INT, 
      ModifiedOn DATETIME
)

CREATE TABLE GUESTS_LOG (
      GuestLogID INT IDENTITY(1,1) PRIMARY KEY, 
      GuestID INT, 
      GuestName VARCHAR(50), 
      ModifiedBy INT, 
      ModifiedOn DATETIME
)

When a value changes in the GUESTS table (ex: Guest name), simply log off that entire row of data, as-is, to your Log/Audit table using the Trigger. Your GUESTS table has current data, the Log/Audit table has the old data.

Then use a select statement to get data from both tables:

SELECT 0 AS 'GuestLogID', GuestID, GuestName, ModifiedBy, ModifiedOn FROM [GUESTS] WHERE GuestID = 1
UNION
SELECT GuestLogID, GuestID, GuestName, ModifiedBy, ModifiedOn FROM [GUESTS_LOG] WHERE GuestID = 1
ORDER BY ModifiedOn ASC

Your data will come out with what the table looked like, from Oldest to Newest, with the first row being what was created & the last row being the current data. You can see exactly what changed, who changed it, and when they changed it.

Optionally, I used to have a function that looped through the RecordSet (in Classic ASP), and only displayed what values had changed on the web page. It made for a GREAT audit trail so that users could see what had changed over time.

Bryan
  • 155
  • 2
  • 12
  • 9
    @user763539 One field? It logs off the entire current row in the table that you are editing. So if will work on ALL fields that you update. – Bryan Mar 22 '17 at 20:42
6

From SQL Server 2016 and onwards there's built-in support for this: use System-Versioned Temporal Tables

Rory
  • 40,559
  • 52
  • 175
  • 261
2

In both Shiva's and dwilli's answer, instead of defining the table name statically (i.e. SELECT @TableName = 'PERSON'), here is a way to get it dynamically:

SELECT @TableName = OBJECT_NAME(parent_object_id) FROM sys.objects
WHERE sys.objects.name = OBJECT_NAME(@@PROCID)

Clarification: This is not my code, i have found it somewhere on the internet, it was quite a while ago though, so i can't remember where. Also, since i can't comment here, i'm posting this as an answer.

FaultyOverflow
  • 153
  • 1
  • 9
2

There are some great answers here to turn the source code into a better experience for the community. If any of you are like me and are looking for the full set of code with test tables ready for copy, paste (RAW on GitHub), and execute I've assembled it below:

/*this is a journey... 
    Source links:

    https://www.red-gate.com/simple-talk/databases/sql-server/database-administration-sql-server/pop-rivetts-sql-server-faq-no-5-pop-on-the-audit-trail/ 
    https://stackoverflow.com/questions/19737723/log-record-changes-in-sql-server-in-an-audit-table
    https://chat.stackexchange.com/transcript/message/34774768#34774768 
    http://jsbin.com/lafayiluri/1/edit?html,output 
*/

-- #region | Set up the tables
/*Firstly, we create the audit table.
There will only need to be one of these in a database*/
DROP TABLE IF EXISTS dbo.audit_trigger; 
CREATE TABLE dbo.audit_trigger (
    Type CHAR(1),
    TableName VARCHAR(128),
    PK VARCHAR(1000),
    FieldName VARCHAR(128),
    OldValue VARCHAR(1000),
    NewValue VARCHAR(1000),
    UpdateDate datetime,
    UserName VARCHAR(128)
);
GO
/*now we will illustrate the use of this tool
by creating a dummy test table called TrigTest.*/
DROP TABLE IF EXISTS dbo.trigtest;
CREATE TABLE trigtest (
    i INT NOT NULL,
    j INT NOT NULL,
    s VARCHAR(10),
    t VARCHAR(10)
);
GO

/*note that for this system to work there must be a primary key
to the table but then a table without a primary key
isn’t really a table is it?*/
ALTER TABLE trigtest ADD CONSTRAINT pk PRIMARY KEY (i, j);
GO
-- #endregion 

/*and now create the trigger itself. This has to be created for every
table you want to monitor*/

-- #region | trigger with bug fix
DROP TRIGGER IF EXISTS dbo.tr_audit_trigtest;
GO
CREATE TRIGGER tr_audit_trigtest ON dbo.trigtest 
    FOR  
        /*uncomment INSERT if you want. The insert data is on the source table
            but sometimes your end users wanna see ALL the data in the audit table
            and hey, storage is cheap-ish now /shrug    
        */
        -- INSERT, 
        UPDATE, 
        DELETE 
AS
SET NOCOUNT ON;
/*declare all the variables*/
DECLARE @bit INT;
DECLARE @field INT;
DECLARE @maxfield INT;
DECLARE @char INT;
DECLARE @fieldname VARCHAR(128);
DECLARE @TableName VARCHAR(128);
DECLARE @PKCols VARCHAR(1000);
DECLARE @sql VARCHAR(2000);
DECLARE @UpdateDate VARCHAR(21);
DECLARE @UserName VARCHAR(128);
DECLARE @Type CHAR(1);
DECLARE @PKSelect VARCHAR(1000);

/*now set some of these variables*/
SET @TableName = (
    SELECT 
        OBJECT_NAME(parent_object_id) 
    FROM sys.objects
    WHERE 
        sys.objects.name = OBJECT_NAME(@@PROCID)
);
SET @UserName = SYSTEM_USER;
SET @UpdateDate = CONVERT(NVARCHAR(30), GETDATE(), 126);

/*Action*/
IF EXISTS (SELECT * FROM INSERTED)
    IF EXISTS (SELECT * FROM DELETED)
         SET @Type = 'U'
    ELSE SET @Type = 'I'
    ELSE SET @Type = 'D'
;
/*get list of columns*/
SELECT * 
INTO #ins
FROM INSERTED;

SELECT * 
INTO #del
FROM DELETED;

/*set @PKCols and @PKSelect via SELECT statement.*/
SELECT @PKCols = /*Get primary key columns for full outer join*/
        COALESCE(@PKCols + ' and', ' on') 
        + ' i.[' + c.COLUMN_NAME + '] = d.[' + c.COLUMN_NAME + ']'
    FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS pk
        INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS c ON (
            c.TABLE_NAME = pk.TABLE_NAME
            AND c.CONSTRAINT_NAME = pk.CONSTRAINT_NAME
        )
    WHERE pk.TABLE_NAME = @TableName
        AND CONSTRAINT_TYPE = 'PRIMARY KEY'
;
SELECT @PKSelect = /*Get primary key select for insert*/
        COALESCE(@PKSelect + '+', '') 
        + '''<[' + COLUMN_NAME + ']=''+convert(varchar(100),
        coalesce(i.[' + COLUMN_NAME + '],d.[' + COLUMN_NAME + ']))+''>'''
    FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS pk,
        INFORMATION_SCHEMA.KEY_COLUMN_USAGE c
    WHERE  pk.TABLE_NAME = @TableName
        AND CONSTRAINT_TYPE = 'PRIMARY KEY'
        AND c.TABLE_NAME = pk.TABLE_NAME
        AND c.CONSTRAINT_NAME = pk.CONSTRAINT_NAME
;
IF @PKCols IS NULL
BEGIN
    RAISERROR('no PK on table %s', 16, -1, @TableName);
    RETURN;
END

SET @field = 0;
SET @maxfield = (
    SELECT 
        MAX(
            COLUMNPROPERTY(
                OBJECT_ID(TABLE_SCHEMA + '.' + @TableName),
                COLUMN_NAME,
                'ColumnID'
            )
        )
    FROM INFORMATION_SCHEMA.COLUMNS
    WHERE  
        TABLE_NAME = @TableName
);

WHILE @field < @maxfield
BEGIN
    SET @field = (
        SELECT 
            MIN(
                COLUMNPROPERTY(
                    OBJECT_ID(TABLE_SCHEMA + '.' + @TableName),
                    COLUMN_NAME,
                    'ColumnID'
                )
            )
        FROM INFORMATION_SCHEMA.COLUMNS
        WHERE  
            TABLE_NAME = @TableName
            AND COLUMNPROPERTY(
                    OBJECT_ID(TABLE_SCHEMA + '.' + @TableName),
                    COLUMN_NAME,
                    'ColumnID'
                ) > @field
    );
    SET @bit = (@field - 1)% 8 + 1;
    SET @bit = POWER(2, @bit - 1);
    SET @char = ((@field - 1) / 8) + 1;

    IF (
        SUBSTRING(COLUMNS_UPDATED(), @char, 1) & @bit > 0
        OR @Type IN ('I', 'D')
    )
    BEGIN
        SET @fieldname = (
            SELECT 
                COLUMN_NAME
            FROM INFORMATION_SCHEMA.COLUMNS
            WHERE  
                TABLE_NAME = @TableName
                AND COLUMNPROPERTY(
                        OBJECT_ID(TABLE_SCHEMA + '.' + @TableName),
                        COLUMN_NAME,
                        'ColumnID'
                    ) = @field
        );
        SET @sql = ('
            INSERT INTO audit_trigger (    
                Type, 
                TableName, 
                PK, 
                FieldName, 
                OldValue, 
                NewValue, 
                UpdateDate, 
                UserName
            )
            SELECT ''' 
                + @Type + ''',''' 
                + @TableName + ''',' 
                + @PKSelect + ',''' 
                + @fieldname + ''''
                + ',convert(varchar(1000),d.' + @fieldname + ')'
                + ',convert(varchar(1000),i.' + @fieldname + ')'
                + ',''' + @UpdateDate + ''''
                + ',''' + @UserName + '''' + 
            ' FROM #ins AS i FULL OUTER JOIN #del AS d'
                    + @PKCols + 
            ' WHERE i.' + @fieldname + ' <> d.' + @fieldname 
                    + ' or (i.' + @fieldname + ' is null and  d.'
                    + @fieldname
                    + ' is not null)' 
                    + ' or (i.' + @fieldname + ' is not null and  d.' 
                    + @fieldname
                    + ' is null)' 
        );
        EXEC (@sql)
    END
END
SET NOCOUNT OFF;
GO 
-- #endregion 


-- #region | now we can test the trigger out
INSERT trigtest SELECT 1,1,'hi', 'bye';
INSERT trigtest SELECT 2,2,'hi', 'bye';
INSERT trigtest SELECT 3,3,'hi', 'bye';
SELECT * FROM dbo.audit_trigger;
SELECT * FROM trigtest;
UPDATE trigtest SET s = 'hibye' WHERE i <> 1;
UPDATE trigtest SET s = 'bye' WHERE i = 1;
UPDATE trigtest SET s = 'bye' WHERE i = 1;
UPDATE trigtest SET t = 'hi' WHERE i = 1;
SELECT * FROM dbo.audit_trigger;
SELECT * FROM dbo.trigtest;
DELETE dbo.trigtest;
SELECT * FROM dbo.audit_trigger;
SELECT * FROM dbo.trigtest;
GO

DROP TABLE dbo.audit_trigger;
GO
DROP TABLE dbo.trigtest ;
GO
-- #endregion

-1

Hey It's very simple see this

@OLD_GUEST_NAME = d.GUEST_NAME from deleted d;

this variable will store your old deleted value and then you can insert it where you want.

for example-

Create trigger testupdate on test for update, delete
  as
declare @tableid varchar(50);
declare @testid varchar(50);
declare @newdata varchar(50);
declare @olddata varchar(50);


select @tableid = count(*)+1 from audit_test
select @testid=d.tableid from inserted d;
select @olddata = d.data from deleted d;
select @newdata = i.data from inserted i;

insert into audit_test (tableid, testid, olddata, newdata) values (@tableid, @testid, @olddata, @newdata)

go
Vijay809
  • 9
  • 2