SQL Server trigger dependent on change of varbinary column to track changes - sql-server

I want to track changes on a VARBINARY(MAX) column in SQL Server.
Goal is to store the date, user and the old VARBINARY value for each update into a separate table.
My code for the trigger:
CREATE TRIGGER dbo.trg_ChangeTrackingPictures
ON dbo.tbl_Content
FOR UPDATE
AS
SET NOCOUNT ON
INSERT dbo.tbl_PictureHistory (OldPicture, ChangedBy, ChangeDate, SourceID)
SELECT
D.Picture,
CURRENT_USER,
GETDATE(),
D.ID
FROM
DELETED D
JOIN
INSERTED I ON D.ID = I.ID
WHERE
HASHBYTES ('MD5', D.Picture) <> HASHBYTES ('MD5', I.Picture)
With this I always get a server timeout error when I update the column.
It works if I leave out
WHERE
HASHBYTES ('MD5', D.Picture) <> HASHBYTES ('MD5', I.Picture)
But this would track every update of the row even if the VARBINARY itself was not touched.
I also tried the simple
WHERE
D.Picture <> I.Picture
with the same result.

Related

SQL Server trigger to Log columns updated from a stored procedure dynamically

I have a stored procedure which will update ~100 columns in a single table when it is executed.
I want to create a trigger on the impacted table that will:
Identify all (actually) updated values
(So if the new value is '1' and the old value is '1', then ignore it)
From the list of actually changed values, get the column name and data value
Take the column name and old data value and store them into a log table via an insert
(INSERT ColumnName, Value INTO LogTable)
I have a solution that will work, but I would have to create a temp table and insert a row into the temp table for each column that I know is impacted... something like having 100 of these:
INSERT INTO #TempTable (
ID,
ColumnName,
OldColumnValue
)
SELECT i.ID, 'Column1', i.Column1
FROM inserted i
INNER JOIN deleted d ON d.ID = i.ID
WHERE COALESCE(d.Column1, '') != COALESCE(i.Column1, '')
After doing that insert for column1 all the way to column100, I would just insert all the records from the #TempTable into the log table .
There has to be a more dynamic approach to solve this, but I can't think of it.
*For the sake of simplicity, lets assume that the values will always be [TEXT] as will the value in the LogTable.
Without the definition of your table, I can't answer this in full, however, what you need to do here is unpivot all your data, and then when the value doesn't equal the other, insert it into your log table. This means that your query will need to look something like this:
INSERT INTO dbo.LogTable (ID,
ColumnName,
OldColumnValue)
SELECT d.ID,
V.ColumnName,
V.OldColumnValue
FROM deleted d
JOIN inserted i ON d.ID = i.ID
CROSS APPLY (VALUES(N'Column1',i.Column1, d.Column1),
(N'Column2',i.Column2, d.Column2),
(N'Column3',i.Column3, d.Column3),
(N'Column4',i.Column4, d.Column4))V(ColumnName, NewColumnValue, OldColumnValue)
WHERE V.NewColumnValue != V.OldColumnValue
OR (V.NewColumnValue IS NULL AND V.OldColumnValue IS NOT NULL)
OR (V.NewColumnValue IS NOT NULL AND V.OldColumnValue IS NULL);
Note, however, that a column must be made up of the same data type, and I doubt all of your columns are the same data type. As such you are going to need to explicitly CONVERT all of your columns to an (n)varchar. For columns that are a date and time value, I would strongly suggest you use a style code in the explicit CONVERT too; like 112 for date, and perhaps 126 for other date and time data types.

Trigger AFTER INSERT, UPDATE, DELETE to call stored procedure with table name and primary key

For a sync process, my SQL Server database should record a list items that have changed - table name and primary key.
The DB already has a table and stored procedure to do this:
EXEC #ErrCode = dbo.SyncQueueItem "tableName", 1234;
I'd like to add triggers to a table to call this stored procedure on INSERT, UPDATE, DELETE. How do I get the key? What's the simplest thing that could possibly work?
CREATE TABLE new_employees
(
id_num INT IDENTITY(1,1),
fname VARCHAR(20),
minit CHAR(1),
lname VARCHAR(30)
);
GO
IF OBJECT_ID ('dbo.sync_new_employees','TR') IS NOT NULL
DROP TRIGGER sync_new_employees;
GO
CREATE TRIGGER sync_new_employees
ON new_employees
AFTER INSERT, UPDATE, DELETE
AS
DECLARE #Key Int;
DECLARE #ErrCode Int;
-- How to get the key???
SELECT #Key = 12345;
EXEC #ErrCode = dbo.SyncQueueItem "new_employees", #key;
GO
The way to access the records changed by the operation is by using the Inserted and Deleted pseudo-tables that are provided to you by SQL Server.
Inserted contains any inserted records, or any updated records with their new values.
Deleted contains any deleted records, or any updated records with their old values.
More Info
When writing a trigger, to be safe, one should always code for the case when multiple records are acted upon. Unfortunately if you need to call a SP that means a loop - which isn't ideal.
The following code shows how this could be done for your example, and includes a method of detecting whether the operation is an Insert/Update/Delete.
declare #Key int, #ErrCode int, #Action varchar(6);
declare #Keys table (id int, [Action] varchar(6));
insert into #Keys (id, [Action])
select coalesce(I.id, D.id_num)
, case when I.id is not null and D.id is not null then 'Update' when I.id is not null then 'Insert' else 'Delete' end
from Inserted I
full join Deleted D on I.id_num = D.id_num;
while exists (select 1 from #Keys) begin
select top 1 #Key = id, #Action = [Action] from #Keys;
exec #ErrCode = dbo.SyncQueueItem 'new_employees', #key;
delete from #Keys where id = #Key;
end
Further: In addition to solving your specified problem its worth noting a couple of points regarding the bigger picture.
As #Damien_The_Unbeliever points out there are built in mechanisms to accomplish change tracking which will perform much better.
If you still wish to handle your own change tracking, it would perform better if you could arrange it such that you handle the entire recordset in one go as opposed to carrying out a row-by-row operation. There are 2 ways to accomplish this a) Move your change tracking code inside the trigger and don't use a SP. b) Use a "User Defined Table Type" to pass the record-set of changes to the SP.
You should use the Magic Table to get the data.
Usually, inserted and deleted tables are called Magic Tables in the context of a trigger. There are Inserted and Deleted magic tables in SQL Server. These tables are automatically created and managed by SQL Server internally to hold recently inserted, deleted and updated values during DML operations (Insert, Update and Delete) on a database table.
Inserted magic table
The Inserted table holds the recently inserted values, in other words, new data values. Hence recently added records are inserted into the Inserted table.
Deleted magic table
The Deleted table holds the recently deleted or updated values, in other words, old data values. Hence the old updated and deleted records are inserted into the Deleted table.
**You can use the inserted and deleted magic table to get the value of id_num **
SELECT top 1 #Key = id_num from inserted
Note: This code sample will only work for a single record for insert scenario. For Bulk insert/update scenarios you need to fetch records from inserted and deleted table stored in the temp table or variable and then loop through it to pass to your procedure or you can pass a table variable to your procedure and handle the multiple records there.
A DML trigger should operate set data else only one row will be processed. It can be something like this. And of course use magic tables inserted and deleted.
CREATE TRIGGER dbo.tr_employees
ON dbo.employees --the table from Northwind database
AFTER INSERT,DELETE,UPDATE
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
declare #tbl table (id int identity(1,1),delId int,insId int)
--Use "magic tables" inserted and deleted
insert #tbl(delId, insId)
select d.EmployeeID, i.EmployeeID
from inserted i --empty when "delete"
full join deleted d --empty when "insert"
on i.EmployeeID=d.EmployeeID
declare #id int,#key int,#action char
select top 1 #id=id, #key=isnull(delId, insId),
#action=case
when delId is null then 'I'
when insId is null then 'D'
else 'U' end --just in case you need the operation executed
from #tbl
--do something for each row
while #id is not null --instead of cursor
begin
--do the main action
--exec dbo.sync 'employees', #key, #action
--remove processed row
delete #tbl where id=#id
--refill #variables
select top 1 #id=id, #key=isnull(delId, insId),
#action=case
when delId is null then 'I'
when insId is null then 'D'
else 'U' end --just in case you need the operation executed
from #tbl
end
END
Not the best solution, but just a direct answer on the question:
SELECT #Key = COALESCE(deleted.id_num,inserted.id_num);
Also not the best way (if not the worst) (do not try this at home), but at least it will help with multiple values:
DECLARE #Key INT;
DECLARE triggerCursor CURSOR LOCAL FAST_FORWARD READ_ONLY
FOR SELECT COALESCE(i.id_num,d.id_num) AS [id_num]
FROM inserted i
FULL JOIN deleted d ON d.id_num = i.id_num
WHERE (
COALESCE(i.fname,'')<>COALESCE(d.fname,'')
OR COALESCE(i.minit,'')<>COALESCE(d.minit,'')
OR COALESCE(i.lname,'')<>COALESCE(d.lname,'')
)
;
OPEN triggerCursor;
FETCH NEXT FROM triggerCursor INTO #Key;
WHILE ##FETCH_STATUS = 0
BEGIN
EXEC #ErrCode = dbo.SyncQueueItem 'new_employees', #key;
FETCH NEXT FROM triggerCursor INTO #Key;
END
CLOSE triggerCursor;
DEALLOCATE triggerCursor;
Better way to use trigger based "value-change-tracker":
INSERT INTO [YourTableHistoryName] (id_num, fname, minit, lname, WhenHappened)
SELECT COALESCE(i.id_num,d.id_num) AS [id_num]
,i.fname,i.minit,i.lname,CURRENT_TIMESTAMP AS [WhenHeppened]
FROM inserted i
FULL JOIN deleted d ON d.id_num = i.id_num
WHERE ( COALESCE(i.fname,'')<>COALESCE(d.fname,'')
OR COALESCE(i.minit,'')<>COALESCE(d.minit,'')
OR COALESCE(i.lname,'')<>COALESCE(d.lname,'')
)
;
The best (in my opinion) way to track changes is to use Temporal tables (SQL Server 2016+)
inserted/deleted in triggers will generate as many rows as touched and calling a stored proc per key would require a cursor or similar approach per row.
You should check timestamp/rowversion in SQL Server. You could add that to the all tables in question (not null, auto increment, unique within database for each table/row etc).
You could add a unique index on that column to all tables you added the column.
##DBTS is the current timestamp, you can store today's ##DBTS and tomorrow you will scan all tables from that to current ##DBTS. timestamp/rowversion will be incremented for all updates and inserts but for deletes it won't track, for deletes you can have a delete only trigger and insert keys into a different table.
Change data capture or change tracking could do this easier, but if there is heavy volumes on the server or large number of data loads, partition switches scanning the transaction log becomes a bottleneck and in some cases you will have to remove change data capture to save the transaction log from growing indefinetely.

Fire Trigger if deleted.Column <> inserted.Column

This is similar to Compare deleted and inserted table in SQL Server 2008 but the results were not what I was looking for.
I do want the trigger to fire if a specific column changes, however the program we have does a delete of all information and an reinsert of all new information every time it is saved. there is no simple update.
I want to write to an audit table, but ONLY if that specific column has changed once a save has occurred.
ALTER TRIGGER [dbo].[deleted]
ON [dbo].[DispTech]
FOR DELETE, INSERT
AS
SET NOCOUNT ON;
IF (SELECT serviceman FROM deleted) <> (SELECT ServiceMan FROM inserted)
BEGIN
INSERT INTO misc.dbo.DeletedTest ("Status", dispatch, serviceman)
SELECT
'Deleted', d.Dispatch, d.ServiceMan
FROM
deleted d
INSERT INTO misc.dbo.DeletedTest ("Status", dispatch, serviceman)
SELECT
'Inserted', i.Dispatch, i.ServiceMan
FROM
inserted i
END
This does NOT work as it results back NULL for everything. I know I could sort it all out in the audit table if I dump everything in there each time, but I really want a cleaner set of data and want to use it for other processing.

SQL Server trigger multiple update rows

I am using a trigger on my table for update.
When I was updating 1 row, it works. But when I update 20 rows at once, the log only show 1 history update.
I want to show all updated rows. How to make it?
This my simple sql:
create trigger Triger_Update_Product
on product
for update
as begin
declare #first char(10)
declare #after char(10)
select #first = name from deleted
select #after = name from inserted
insert into historyproduk
select #first, #after, getdate()
end
You need to write the trigger in a set-based fashion and be aware that inserted and deleted will contain multiple rows - so these lines of code are really bad:
select #first = name from deleted
select #after = name from inserted
These select one arbitrary row from the update set - and they ignore all other rows. Don't do this!!
Try this instead:
create trigger Trigger_Update_Product
on product
for update
as begin
insert into historyproduk
select d.name, i.name, getdate()
from deleted d
inner join inserted i on d.PrimaryKey = i.PrimaryKey
end
You need to join the Inserted and Deleted pseudo tables on the primary key and then grab the Name from the old (deleted) and new (Inserted) and insert those - along with the date/time stamp - into the history table in a single INSERT ... SELECT .... statement to handle multiple rows being updated

I really need to compare images in SQL

I know this thread: Comparing Image Data Types In SQL but it isn't helpful
I'm trying to write a trigger in T-SQL (SQL Server 2008) which would check if an image of an article was changed and report it in some special table. The database uses Image datatype and I'm not in power to change it.
I tried:
ALTER TRIGGER PhotoUPDATE
ON ARTICLE
FOR UPDATE
AS
DECLARE #ID numeric (18,0),#PHOTO_NEW image,#PHOTO_OLD image
SET #ID = (SELECT ID FROM inserted)
SET #PHOTO_NEW = (SELECT PHOTO FROM inserted)
SET #PHOTO_OLD = (SELECT PHOTO FROM deleted)
IF (#PHOTO_NEW<>#PHOTO_OLD)
BEGIN
INSERT PhotoCHANGED (ID,DATE)
VALUES(#ID,GETDATE())
END
GO
I get error:
The text, ntext, and image data types are invalid for local variables.
When I tried without variables:
IF ((SELECT PHOTO FROM inserted)<>(SELECT PHOTO FROM deleted))
I got:
Cannot use text, ntext, or image columns in the 'inserted' and 'deleted' tables.
What else could I try?
You say
The database uses Image datatype and I'm not in power to change it.
Well in that case you can't do this then. image is deprecated. It is not permitted to access image columns in the inserted/deleted tables. Attempting to do this will cause the error
Cannot use text, ntext, or image columns in the 'inserted' and
'deleted' tables.
It is possible to get the post update column value by querying the base table but there is no way to get the DELETED.PHOTO value to compare it with.
You will need to get the person responsible to change the column datatype to varbinary(max).
ALTER TABLE ARTICLE ALTER COLUMN PHOTO VARBINARY(MAX) NOT NULL
Then you can access it inside the trigger and compare for equality.
Your trigger is broken as well. Updates can affect multiple (or zero) rows. Not always exactly one.
Also you should check IF UPDATE(PHOTO) to skip doing it if the column wasn't touched.
ALTER TRIGGER PhotoUPDATE
ON ARTICLE
FOR UPDATE
AS
SET NOCOUNT ON;
IF UPDATE(PHOTO)
BEGIN
INSERT PhotoCHANGED
(ID,
DATE)
SELECT I.ID,
GETDATE()
FROM inserted I
JOIN DELETED D
ON I.ID = D.ID
AND EXISTS (SELECT I.PHOTO
EXCEPT
SELECT D.PHOTO)
END

Resources