Reputation: 5379
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:
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
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
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