Luigi Saggese
Luigi Saggese

Reputation: 5379

Logging update operation in triggers

I have an UPDATE trigger that produces INSERTED and DELETED table like this:

INSERTED

Id  Name    Surname
1   Stack   Overflow
2   Luigi   Saggese

DELETED

Id  Name    Surname
1   Stacks  Overflow
2   Luigi   Sag

I want to capture this update to a log table. My Log table (that is global for all tables) is like this (then I must process my INSERTED and DELETED table):

Id_Table    Table_Key_Value   Id_Value   Old_Value  New_Value
12345               1          4556645    Stack      Stacks
12345               1           544589   Overflow   Overflows
12345               2           544589   Saggese       Sag

Id_Table is the table's system object_id where I have performed the UPDATE statement, Table_Key_Value is the value of the primary key of the UPDATEd columns, Id_Value is a custom ID I mapped to each column in each table. A column's data is logged only if the column is changed by the UPDATE.

I have thought of 2 ways to do this:

  1. Performing a SELECT on the table, once for each column:

    INSERT INTO LOG (Id_Table, Table_Key_Value, Id_Value,Old_Value, New_Value)
       SELECT 12345, Id, 4556645, D.Name, I.Name
       FROM INSERTED I 
       INNER JOIN DELETED D ON I.ID = D.ID
       WHERE D.Name <> I.Name
    
       union
    
       SELECT 12345, Id, 544589, D.Surname, I.Surname
       FROM INSERTED I 
       INNER JOIN DELETED D ON I.ID = D.ID
       WHERE D.Surname <> I.Surname
    
  2. Performing a single select against a UDF:

    SELECT CustomFunction(12345,Id, I.Name, D.Name, I.Surname, D.Surname) 
    FROM INSERTED I  
    INNER JOIN DELETED D ON I.ID = D.ID
    
    **CustomFunction** (_Id_Table,_Table_Key_Value, _Old_Value_Name, _New_Value_Name, _Old_Value_Surname, _New_Value_Surname)
    
    INSERT INTO LOG(Id_Table, Table_Key_Value, Id_Value,Old_Value, New_Value)
    VALUES(_Id_Table,_Table_Key_Value, 4556645, _Old_Value_Name, _New_Value_Name)
    
    INSERT INTO LOG(Id_Table, Table_Key_Value, Id_Value,Old_Value, New_Value)
    VALUES(_Id_Table,_Table_Key_Value, 544589, _Old_Value_Surname, _New_Value_Surname)
    

Are there other ways to do this? What is most efficient and maintainable way?

Upvotes: 2

Views: 1884

Answers (1)

ErikE
ErikE

Reputation: 50201

Before answering, let me first say that I don't think it is best to log all tables to a single table. If your database grows you could end up with serious contention on the Log table. Plus, all your data has to be changed to varchar or sql_variant in order to be put in the same column, forcing it to take up more space. I also think that logging each updated column to a separate row (skipping columns that aren't updated) is going to make it very hard for you to query. Do you know how to pull all that data together to actually get a composite and sensible view of each row's changes, when, and by whom? Having one log table per table is, in my opinion, going to be much easier. Then you won't have the problems you're experiencing trying to make it work.

Also, did you know about SQL Server 2008 Change Data Capture? Use that instead, if you are using the Enterprise or Developer editions of SQL Server!

Aside from that issue, you can do what you want with a logical UNPIVOT (performing your own version of it). You can't really use the Native SQL 2005 UNPIVOT because you have two target columns, not one. Here's an example for SQL Server 2005 and up using CROSS APPLY to perform the UNPIVOT:

INSERT INTO dbo.LOG (Id_Table, Table_Key_Value, Id_Value, Old_Value, New_Value)
SELECT 12345, I.Id, X.Id_Value, X.Old_Value, X.New_Value
FROM
   INSERTED I 
   INNER JOIN DELETED D ON I.ID = D.ID
   CROSS APPLY (
      SELECT 4556645, D.Name, I.Name
      UNION ALL SELECT 544589, D.Surname, I.Surname
    ) X (Id_Value, Old_Value, New_Value)
WHERE
   X.Old_Value <> X.New_Value

Here's a more generic method for SQL 2000 or other DBMSes (should theoretically work in Oracle, MySQL, etc. -- for Oracle add FROM DUAL to each SELECT in the derived table):

INSERT INTO dbo.LOG (Id_Table, Table_Key_Value, Id_Value, Old_Value, New_Value)
SELECT *
FROM (
   SELECT
      12345,
      I.Id,
      X.Id_Value,
      CASE X.Id_Value
         WHEN 4556645 THEN D.Name
         WHEN 544589 THEN D.Surname
      END Old_Value,
      CASE X.Id_Value
         WHEN 4556645 THEN I.Name
         WHEN 544589 THEN I.Surname
      END New_Value   
   FROM
      INSERTED I 
      INNER JOIN DELETED D ON I.ID = D.ID
      CROSS JOIN (
         SELECT 4556645
         UNION ALL SELECT 544589
      ) X (Id_Value)
) Y
WHERE
   Y.Old_Value <> Y.New_Value

SQL Server 2005 and up do have the native UNPIVOT command, though in general, even when UNPIVOT will work, I like using CROSS APPLY instead because there is more flexibility to do what I want. Specifically, the native UNPIVOT command isn't workable here because UNPIVOT can only target a single destination column, but you need two (Old_Value, New_Value). Concatenating the two columns into a single value (and separating later) is not good; creating a meaningless row correlator value to PIVOT with afterward is not good, and I can't think of another way to do it that's not a variation on those two. The CROSS APPLY solution is truly going to be the best for you to match the exact log table structure you've described.

Compared to my queries here, your method #1 will not perform as well (in a ratio of about {the number of columns}:1 worse performance). Your method #2 is a good idea but still suboptimal because calling a UDF has a large overhead, plus then you have to loop over each row (shudder).

Upvotes: 3

Related Questions