2

I am new to SQL Server 2008 and I need advice from all of you. I want to find out the changed value in inserted and deleted tables of the SQL Server 2008 for a table because I am currently doing the audit trail to keep the old and new value. How can I loop all the column to find out which field's value change from the deleted and inserted table? I had tried the if else statement to compare

For example:

create trigger trg_XXX on dbo.table
after update
as
begin
    declare 
       @oldID varchar(6),
       @newID varchar(6)

    select @oldID = ID from deleted
    select @newID = ID from inserted

    if(@oldID != @newID)
       insert into table (a, b) values (@oldID, @newID)
    else
       print('do nothing')

Is there a way to do using cursor to loop through the deleted and inserted table or any alternative way? Can give me some example?

Darkzaelus
  • 2,059
  • 1
  • 15
  • 31
user3387045
  • 31
  • 1
  • 1
  • 3
  • What do you mean by inserted and deleted table? Is this code part of a trigger to create an audit? – Mithrandir Apr 25 '14 at 07:27
  • inserted and deleted table is the temp table in the sql server 2008, you can get the old value from deleted table and new value from inserted table. – user3387045 Apr 25 '14 at 07:29
  • 3
    Those are **pseudo** ("virtual") tables that exist only in the **context of a trigger** - but ***please*** do ***NOT*** use cursors inside a trigger! That's the worst possible way of killing all your performance..... – marc_s Apr 25 '14 at 07:31

4 Answers4

5

I'm not quite sure, what your goal ist, i think it might be something like this. Let's say we have a table like this:

CREATE TABLE Product
(
     ID      INT                   PRIMARY KEY,
     Name    NVARCHAR(100)         NOT NULL,
     Price   DECIMAL(10,2)         NOT NULL
);

and some audit table like this:

CREATE TABLE ProductAudit
(
     AuditID      INT                   IDENTITY PRIMARY KEY, 
     ProductID    INT                   NOT NULL
     OldName      NVARCHAR(100)         NULL,
     OldPrice     DECIMAL(10,2)         NULL,
     NewName      NVARCHAR(100)         NULL,
     NewPrice     DECIMAL(10,2)         NULL
);

Then you create a trigger:

CREATE TRIGGER TR_AUDIT
ON Product
FOR INSERT, UPDATE, DELETE
AS
BEGIN
       INSERT INTO ProductAudit (ProductID, OldName, OldPrice, NewName, NewPrice)
       SELECT 
           COALESCE(I.ID, D.ID),
           D.Name,
           D.Price,
           I.Name,
           I.Price
       FROM 
           INSERTED I FULL OUTER JOIN DELETED D ON I.ID = D.ID;
END
GO

There you have it.

Mithrandir
  • 24,869
  • 6
  • 50
  • 66
  • 1
    +1 very nice answer. In my opinion, you should have included a timestamp for audit, but that is the easy part – t-clausen.dk Apr 25 '14 at 07:59
  • @t-clausen.dk you are quite right of course, besides a time stamp i would have also included some data about the user and process, but i think this is beside the point here. i'm not even sure the op is really clear about what he wants to do. – Mithrandir Apr 25 '14 at 08:09
  • of course i had system user,audit date for the audit table. i have 80 columns in the tables and i cannot use if else statement to do for all the 80 columns. Is there any example like UPDATE(),COLUMN_UPDATED() to detect the changes in the deleted and inserted table and insert it into audit table by loop through the column? – user3387045 Apr 25 '14 at 08:28
2

I think you are looking for what unofficially called magic tables in SQL server.

The tables "INSERTED" and "DELETED" are called magic tables of the SQL Server. We can not see these tables in the data base. But we can access these tables from the "TRIGGER"

When we insert the record into the table, the magic table "INSERTED" will be created In that table the current inserted row will be available. We can access this record in the "TRIGGER".

When we update a record on the table where trigger is created, the magic tables "INSERTED" and "DELETED" both will be created, the Old data of the updating record will be available in "DELETED" table and, the new data will be available in "INSERTED" table, while accessing them inside the trigger.

When we delete the record from the table, the magic table "DELETED" will be created In that table the current deleted row will be available. We can access this record in the "TRIGGER".

Example:

Following code Explains the magic table "INSERTED":

CREATE TRIGGER LogMessage
ON EMP
FOR INSERT
AS
   DECLARE @EMPNAME varchar(50)
   SELECT @EMPNAME= (SELECT EMPNAME FROM INSERTED)
   INSERT INTO LOGTABLE(UserId,Message) values (@EMPNAME,'Record Added')
GO

Following code Explain the magic table "DELETED"

CREATE TRIGGER LogMessage
ON EMP
FOR DELETE
AS
   DECLARE @EMPNAME varchar(50)
   SELECT @EMPNAME= (SELECT EMPNAME FROM DELETED)
   INSERT INTO LOGTABLE(UserId,Message) values (@EMPNAME,'Record Removed')
GO

Sources(All credit to these articles & authors):

http://www.codeproject.com/Questions/285423/what-is-magic-table-different-types-of-magic-table http://www.dotnetspider.com/resources/29332-Magic-tables-SQL-Server.aspx

. .

Alternatively

, you can try: Obtaining Changes by Using the Change Tracking Functions, its MSDN link to explain how to track changes by using in-built functions.

CHANGETABLE(CHANGES …) function

This rowset function is used to query for change information. The function queries the data stored in the internal change tracking tables. The function returns a results set that contains the primary keys of rows that have changed together with other change information such as the operation, columns updated and version for the row.

CHANGE_TRACKING_CURRENT_VERSION() function

Is used to obtain the current version that will be used the next time when querying changes. This version represents the version of the last committed transaction.

CHANGE_TRACKING_MIN_VALID_VERSION()function

Is used to obtain the minimum valid version that a client can have and still obtain valid results from CHANGETABLE(). The client should check the last synchronization version against the value thatis returned by this function. If the last synchronization version is less than the version returned by this function, the client will be unable to obtain valid results from CHANGETABLE() and will have to reinitialize.

Refer syntax & usage at http://technet.microsoft.com/en-us/library/cc280358%28v=sql.105%29.aspx

Hope it helps.

Pranav Singh
  • 17,079
  • 30
  • 77
  • 104
  • 8
    This statement `SELECT @EMPNAME= (SELECT EMPNAME FROM INSERTED)` is **extremely dangerous** - triggers will be called **once per statement** and `Inserted` can (and **will!**) contain **multiple rows** - so which one are you selecting here? And you're ignoring all the others, too, at the same time ..... triggers must be written in a **set-based** way, taking into account that `Inserted` and `Deleted` **will contain multiple rows!** – marc_s Apr 25 '14 at 07:32
  • i had tried this method but i have around 60-80 column to compare from the deleted and inserted table. what i mean is--if i do not want to use if else statement to compare the value, is it anyway to loop through all the column to find out which value had change.? for example cursor? But i am confuse about cursor – user3387045 Apr 25 '14 at 07:34
  • @user3387045, updated answer to add msdn link to somewhat you are trying to do. – Pranav Singh Apr 25 '14 at 07:46
  • @PranavSingh ok, i will have a look for the link u posted and thank for your reply. – user3387045 Apr 25 '14 at 07:54
2

First: do NOT use a cursor inside a trigger - EVER!

Second: to identify what fields were included in the update statement you can use: UPDATE() or COLUMNS_UPDATED()

Note: This does NOT list the fields that have had their value changed, just the list of columns included in the SET portion of the UPDATE statement.

Third: There are a variety of methods that you can use to audit changes to your tables (the accepted answer on Most efficient method to detect column change in MS SQL Server has a good list and guidelines on how to use, if you are using SQL Server Enterprise you could also look at using Change Data Capture

Some example audit code I would use (where I want to record a column by column audit table):

INSERT INTO AuditTable (ColumnChanged, OldValue, NewValue) /* I assume there are default columns logging who/when the change was done by? */
    SELECT 'ColumnA' as ColumnChanged, d.ColumnA, i.ColumnA
    FROM inserted i join deleted d ON d.PKID = i.PKID
    WHERE 
        /* both aren't null and the value has changed */
        (d.ColumnA IS NOT NULL AND i.ColumnA IS NOT NULL AND d.ColumnA != i.ColumnA) 
        /* it was null and now it isn't */
        OR (d.ColumnA IS NULL AND i.ColumnA IS NOT NULL) 
        /* it wasn't null and now it is */
        OR (d.ColumnA IS NOT NULL AND i.ColumnA IS NULL)
UNION 
    SELECT 'ColumnB' as ColumnChanged, d.ColumnB, i.ColumnB
    FROM inserted i join deleted d ON d.PKID = i.PKID
    WHERE 
        /* both aren't null and the value has changed */
        (d.ColumnB IS NOT NULL AND i.ColumnB IS NOT NULL AND d.ColumnB != i.ColumnB) 
        /* it was null and now it isn't */
        OR (d.ColumnB IS NULL AND i.ColumnB IS NOT NULL) 
        /* it wasn't null and now it is */
        OR (d.ColumnB IS NOT NULL AND i.ColumnB IS NULL)
....  /* continuing for each column */

It would be simpler (from a sql perspective and faster [due to less writes] to audit by row) ie:

INSERT INTO AuditTable (OldValueA, NewValueA, OldValueB, NewValueB) 
SELECT d.ColumnA, i.ColumnA, d.ColumnB, i.ColumnB
FROM inserted i join deleted d ON d.PKID = i.PKID
WHERE 
/* same check for columnA */
    /* both aren't null and the value has changed */
    (d.ColumnA IS NOT NULL AND i.ColumnA IS NOT NULL AND d.ColumnA != i.ColumnA) 
    /* it was null and now it isn't */
    OR (d.ColumnA IS NULL AND i.ColumnA IS NOT NULL) 
    /* it wasn't null and now it is */
    OR (d.ColumnA IS NOT NULL AND i.ColumnA IS NULL)
/* now check columnB */
    (d.ColumnB IS NOT NULL AND i.ColumnB IS NOT NULL AND d.ColumnB != i.ColumnB) 
    OR (d.ColumnB IS NULL AND i.ColumnB IS NOT NULL) 
    OR (d.ColumnB IS NOT NULL AND i.ColumnB IS NULL)
....  /* continuing for each column */
Community
  • 1
  • 1
Andrew Bickerton
  • 468
  • 4
  • 14
  • @AndrewBickerthon ~ if i have 80 columns, can i use COLUMNS_UPDATED() to loop through deleted and inserted table for the value change and insert into audit table? – user3387045 Apr 25 '14 at 08:02
  • @user3387045 from a software developers perspective it's antiquated to write out nearly identical code for all 80 columns in an audit trigger, but it is better to do that work once (when creating the trigger) than to have sql server do that work every time a row is updated. I would work out a pattern you want to use (see the answers linked) and generate the sql you want to drop into the trigger. – Andrew Bickerton Apr 25 '14 at 08:39
  • @user3387045 within a trigger/stored procedures loops are allowed (they are valid sql), but I would strongly recommend you avoid them unless absolutely necessary, they have a high cost associated with them and while the code is larger and looks more daunting it performs significantly better if you put the full audit sql in – Andrew Bickerton Apr 25 '14 at 08:42
  • @AndrewBickerthon ~ i use if else statement to compare all the inserted and deleted table before and my work been rejected. I will try your method. By the way, is this a good example to make the audit trigger?The link below, because i do not want to change the trigger anoymore if there is new column added to the table. http://p2p.wrox.com/sql-server-2000/73710-trigger-based-auditing-your-sql-server-database.html – user3387045 Apr 25 '14 at 09:13
  • @user3387045 if I was your DBA I would 1) reject that trigger due to performance and security risks of that approach. 2) recommend you use a version of that code to generate the trigger and ask you resubmit the real trigger. To re-iterate "you should not dynamically create sql to do your auditing on live (particularly as the structure of you table should not often change), instead have a method/process/tool that allows you to quickly recreate the trigger code when you add a new column. – Andrew Bickerton Apr 25 '14 at 09:47
2

You could make a dynamic "comparer" by converting the inserted and deleted tables into 2 xml, iterating through the properties and comparing the xml value.

Example:

CREATE TRIGGER MY_COMPARER
   ON TABLE_NAME
   AFTER UPDATE
AS
BEGIN
    DECLARE @columnIndex INT = 1;
    DECLARE @maxColumns INT = (select count(*) from INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'TABLE_NAME');
    DECLARE @insertedXML XML = (select top 1 * from INSERTED FOR XML PATH('ROOT'), ELEMENTS XSINIL);
    DECLARE @deletedXML XML = (select top 1 * from DELETED FOR XML PATH('ROOT'), ELEMENTS XSINIL);

    WHILE @columnIndex <= @maxColumns BEGIN 
        DECLARE @insertedXMLValue XML = (select @insertedXML.query('/ROOT/*[sql:variable("@columnIndex")]'));
        DECLARE @deletedXMLValue XML = (select @deletedXML.query('/ROOT/*[sql:variable("@columnIndex")]'));

        DECLARE @insertedValue NVARCHAR(MAX) = CONVERT(NVARCHAR(MAX), @insertedXMLProp);
        DECLARE @deletedValue NVARCHAR(MAX) = CONVERT(NVARCHAR(MAX), @deletedXMLProp);

        IF (@insertedValue != @deletedValue)
            print('Column: ' + CONVERT(NVARCHAR(MAX), @columnIndex) + ' has changed')

        SET @columnIndex = @columnIndex + 1;
    END
END
Henke
  • 302
  • 3
  • 8