How to create history using trigger, column name, before and after value, who modified, date and time? - sql

I have a two tables one is main and other one is for history, having the same schema with different records apart from the unique one.
I want to create a query which can tell me which column was updated and what the before and after values along with who has updated and what time.
Please see below. Can anyone help me to get this done using SQL?
UniqueID Field Modified Before Value After Value updatedby: Change Date
111 Company Name Exxon Mobile ExxonMobileTest Dev 1/13/2014
122 Account Category Focused Pursuit Jeff 1/13/2014

Make and audit table that mirrors the one you want to log, then every new row you insert into the audit signifies a change in data, use this trigger:
CREATE TRIGGER [dbo].[mytrigger]
ON [dbo].[mytable]
AFTER INSERT,DELETE,UPDATE
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
declare #ERR_TXT char(255),#ER int,#RC int
if (select COUNT(*) from inserted) <> 0 -- validate the new data
begin
-- do stuff to check new data if needed
end
if (select count(*) from inserted) = (select count(*) from deleted) -- update
begin
INSERT INTO [DB].[dbo].[myAudit]
... (all columns)
SELECT ... (all columns)
from inserted
end
if (select count(*) from inserted) = 0 and (select count(*) from deleted) <> 0 -- delete
begin
INSERT INTO [DB].[dbo].[myAudit]
... (all columns)
SELECT ... (all columns)
from deleted
end
if (select count(*) from inserted) <> 0 and (select count(*) from deleted) = 0 -- insert
begin
INSERT INTO [DB].[dbo].[myAudit]
... (all columns)
SELECT ... (all columns)
from inserted
end
END
This works well for me. You can use some of the server functions to see who did the transactions, like SUSER_ID() & GETDATE().

Related

Modify two tables (insert or update) based on existance of a row in the first table

I have a simple thing to do but somehow can't figure out how to do it.
I have to modify two tables (insert or update) based on existance of a row in the first table.
There is a possibility that some other process will insert the row with id = 1
between getting the flag value and "if" statement that examines its value.
The catch is - I have to change TWO tables based on the flag value.
Question: How can I ensure the atomicity of this operation?
I could lock both tables by "select with TABLOCKX", modify them and release the lock by committing the transaction but ... won't it be overkill?
declare #flag int = 0
begin tran
select #flag = id from table1 where id = 1
if #flag = 0
begin
insert table1(id, ...) values(1, ...)
insert table2(id, ...) values(1, ...)
end
else
begin
update table1 set colX = ... where id = 1
update table2 set colX = ... where id = 1
end
commit tran
To sumarize our conversation and generalize to other's case :
If your column [id] is either PRIMARY KEY or UNIQUE you can put a Lock on that row. No other process will be able to change the value of [id]
If not, in my opinion you won't have other choice than Lock the table with a TABLOCKX. It will prevent any other process to UPDATE,DELETE or INSERT a row.
With that lock, it could possibly allow an other process to SELECT over the table depending on your isolation level.
If your database is in read_committed_snapshot, the other process would read the "old" value of the same [id].
To check your isolation level you can run
SELECT name, is_read_committed_snapshot_on FROM sys.databases

Prevent duplicate data in using After Insert Trigger

I can detect duplicate records, but when I'm inserting new data it will detect it as a duplicate record even if doesn't already exist.
Here is my code:
ALTER TRIGGER [dbo].[SDPRawInventory_Dup_Trigger]
ON [dbo].[SDPRawInventory]
AFTER INSERT
AS
DECLARE #Year float,
#Month float,
#SDPGroup nvarchar(255);
SELECT
#Year = i.InvYear, #Month = i.InvMonth, #SDPGroup = i.SDPGroup
FROM inserted i;
IF (SELECT COUNT(*) FROM SDPRawInventory A
WHERE A.InvYear = #Year
AND A.InvMonth = #Month
AND A.SDPGroup = #SDPGroup) >= 1
BEGIN
RAISERROR ('Duplicate data', 16, 1)
ROLLBACK;
END
ELSE
BEGIN
INSERT INTO SDPRawInventory
SELECT * FROM inserted;
END
This is the table
And to clarify there is no primary key nor unique identifier.
If you are unable to put a constraint in place, then you need to handle the fact that Inserted may have multiple records. And because its an after insert trigger, you don't need to do anything if no duplicates are found because the records are already inserted.
ALTER TRIGGER [dbo].[SDPRawInventory_Dup_Trigger]
ON [dbo].[SDPRawInventory]
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
IF EXISTS (
SELECT 1
FROM dbo.SDPRawInventory S
INNER JOIN Inserted I ON
-- Test for a duplicate
S.InvYear = I.InvYear
AND S.InvMonth = I.InvMonth
AND S.SDPGroup = I.SDPGroup
-- But ensure the duplicate is a *different* record - assumes a unique ID
AND S.ID <> I.ID
)
BEGIN
THROW 51000, 'Duplicate data.', 1;
END;
END;
Note the simplified and modern error handling.
EDIT: And if you have no unique key, and no permission to add one, then you need an instead of trigger to only insert non-duplicates e.g.
ALTER TRIGGER [dbo].[SDPRawInventory_Dup_Trigger]
ON [dbo].[SDPRawInventory]
INSTEAD OF INSERT
AS
BEGIN
SET NOCOUNT ON;
-- Reject the entire insert if a single duplicate exists
-- Note if multiple records are inserted, some of which are duplicates and some of which aren't, they all get rejected
IF EXISTS (
SELECT 1
FROM dbo.SDPRawInventory S
INNER JOIN Inserted I ON
-- Test for a duplicate
A.InvYear = I.InvYear
AND A.InvMonth = I.InvMonth
AND A.SDPGroup = I.#SDPGroup
)
-- Test that Inserted itself doesn't contain duplicates
OR EXISTS (SELECT 1 FROM Inserted GROUP BY InvYear, InvMonth, SDPGroup HAVING COUNT(*) > 1)
BEGIN
THROW 51000, 'Duplicate data.', 1;
END;
INSERT INTO dbo.SDPRawInventory (SDP_SKU_DESC, WholeQty, InvYear, InvMonth, SDPGroup, invUOM, LooseQty)
SELECT SDP_SKU_DESC, WholeQty, InvYear, InvMonth, SDPGroup, invUOM, LooseQty
FROM Inserted I
WHERE NOT EXISTS (
SELECT 1
FROM dbo.SDPRawInventory S
-- Test for a duplicate
WHERE S.InvYear = I.InvYear
AND S.InvMonth = I.InvMonth
AND S.SDPGroup = I.SDPGroup
);
END;
Note: This doesn't do anything to handle existing duplicates.
This trigger is executed after the new records were inserted, so it will at least find the original records in the SELECT COUNT statement. Changing >= 1 into >= 2 can only partially fix this when inserting is guaranteed to occur one record as a time. Moreover, it will still fail when there were already multiple duplicated of the newly inserted record in the database before the insert.
You need to exclude the latest inserted records from the COUNT. But a better idea would probably be to add a UNIQUE constraint for preventing duplicates, so no trigger would be necessary.
If adding a constraint is not possible yet, you should initiate a clean-up process to eliminate the existing duplicates beforehand. Everything else is looks pretty flawed to me, since it is unlikely the current approach will ever bring the table into a state with no duplicates.
You are creating the infinite loop. You just have to remove the insertion part from your trigger.
ELSE
BEGIN
INSERT INTO SDPRawInventory
SELECT * FROM inserted;
END
This should not be in the trigger as trigger is called as part of insertion. you should not write actual insertion in to table in trigger.

Using IF UPDATE on SQL Trigger when handling multiple inserted/updated records

I use this SQL Server trigger to look for insert/update of multiple records from a specific table and put it into another queue table (for processing later).
ALTER TRIGGER [dbo].[IC_ProductUpdate] ON [dbo].[StockItem]
AFTER INSERT, UPDATE
AS
BEGIN
SELECT RowNum = ROW_NUMBER() OVER(ORDER BY ItemID) , ItemID
INTO #ProductUpdates
FROM INSERTED;
DECLARE #MaxRownum INT;
SET #MaxRownum = (SELECT MAX(RowNum) FROM #ProductUpdates);
DECLARE #Iter INT;
SET #Iter = (SELECT MIN(RowNum) FROM #ProductUpdates);
WHILE #Iter <= #MaxRownum
BEGIN
-- Get Product Id
DECLARE #StockItemID INT = (SELECT ItemID FROM #ProductUpdates WHERE RowNum = #Iter);
-- Proceed If This Product Is Sync-able
IF (dbo.IC_CanSyncProduct(#StockItemID) = 1)
BEGIN
-- Check If There Is A [ProductUpdate] Queue Entry Already Exist For This Product
IF ((SELECT COUNT(*) FROM IC_ProductUpdateQueue WHERE StockItemID = #StockItemID) > 0)
BEGIN
-- Reset [ProductUpdate] Queue Entry
UPDATE IC_ProductUpdateQueue
SET Synced = 0
WHERE StockItemID = #StockItemID
END
ELSE
BEGIN
-- Insert [ProductUpdate] Queue Entry
INSERT INTO IC_ProductUpdateQueue (StockItemID, Synced)
VALUES (#StockItemID, 0)
END
END
SET #Iter = #Iter + 1;
END
DROP TABLE #ProductUpdates;
END
This works fine, however I only want the above trigger to react if certain columns were updated.
The columns I am interested in are:
Name
Description
I know I can use the following T-SQL syntax to check if a column really updated (during update event) like this:
IF (UPDATE(Name) OR UPDATE(Description))
BEGIN
// do something...
END
But, I am not sure how to incorporate this into the above trigger, since my trigger handles multiple rows being updated at same time also.
Any ideas? At which point in the trigger could i use IF (UPDATE(colX))?
First, I would suggest to have one separate trigger for each operation - one for INSERT, and another for UPDATE. Keeps the code cleaner (less messy IF statements and so forth).
The INSERT trigger is pretty simple, since there's nothing to check for updating - and there's absolutely no need for a temporary table and a slow WHILE loop - just two simple, set-based statements and you're done:
CREATE TRIGGER [dbo].[IC_ProductInsert] ON [dbo].[StockItem]
AFTER INSERT
AS
BEGIN
-- update the queue for those entries that already exist
-- those rows that *DO NOT* exist yet are not being touched
UPDATE puq
SET Synced = 0
FROM dbo.IC_ProductUpdateQueue puq
INNER JOIN Inserted i ON puq.StockItemID = i.StockItemID
-- for those rows that don't exist yet - insert the values
INSERT INTO dbo.IC_ProductUpdateQueue (StockItemID, Synced)
SELECT
i.StockItemID, 0
FROM
Inserted i
WHERE
NOT EXISTS (SELECT * FROM dbo.IC_ProductUpdateQueue puq
WHERE puq.StockItemID = i.StockItemID)
END
The UPDATE trigger needs one extra check - to see whether or not one of the two columns of interest has changed. This can be handled quite easily by combining the Inserted pseudo table with the new values (after the UPDATE), and the Deleted pseudo table with the "old" values (before the UPDATE):
ALTER TRIGGER [dbo].[IC_ProductUpdate] ON [dbo].[StockItem]
AFTER UPDATE
AS
BEGIN
-- update the queue for those entries that already exist
-- those rows that *DO NOT* exist yet are not being touched
UPDATE puq
SET Synced = 0
FROM dbo.IC_ProductUpdateQueue puq
INNER JOIN Inserted i ON puq.StockItemID = i.StockItemID
INNER JOIN Deleted d ON d.StockItemID = i.StockItemID
WHERE
i.Name <> d.Name OR i.Description <> d.Description
-- for those rows that don't exist yet - insert the values
INSERT INTO dbo.IC_ProductUpdateQueue (StockItemID, Synced)
SELECT
i.StockItemID, 0
FROM
Inserted i
INNER JOIN
Deleted d ON d.StockItemID = i.StockItemID
WHERE
i.Name <> d.Name OR i.Description <> d.Description
AND NOT EXISTS (SELECT * FROM dbo.IC_ProductUpdateQueue puq
WHERE puq.StockItemID = i.StockItemID)
END
You can join to deleted and use where I.Name <> D.Name...
https://www.mssqltips.com/sqlservertip/2342/understanding-sql-server-inserted-and-deleted-tables-for-dml-triggers/

After insert not working

all id columns has auto_increment
In my trigger:
ALTER trigger [dbo].[mytrig]
on [dbo].[requests]
after INSERT, UPDATE
as
begin
declare #MyId1 int
set #MyId1 = (select Id from inserted)
declare #MyId2 int
declare #MyId3 int
if (select column1 from inserted) = 1
begin
insert into [dbo].[contracts] select column1,column2,column3 .... from inserted
set #MyId2 = SCOPE_IDENTITY()
insert into [dbo].[History] select column1,column2,column3 .... from inserted
set #MyId3 = SCOPE_IDENTITY()
insert into [dbo].[contracts_depts](Id_Contract ,column5) select #MyId2,column6 from request_depts where Id_request=#MyId1
insert into [dbo].[History_depts] (Id_InHistory,column5) select #MyId3,column6 from request_depts where Id_request=#MyId1
end
end
#MyId1 returns value only after update but not after insert. Do I have to use scope_identity() or something ?
Your main issue is: you're assuming the triggers is called once per row - that is NOT the case!
The trigger is called once per statement, and if your statement affects multiple rows, the Inserted pseudo table will contain multiple rows - so your statement here
set #MyId1 = (select Id from inserted)
really isn't going to work - it will select one arbitrary row (out of however many there are).
You'll need to rewrite your trigger to take this fact into account! Assume that Inserted contains 100 rows - how do you want to deal with that? What are you trying to achieve? Triggers don't return values - they will record into an audit table, or update other rows, or something like that ....

IF UPDATE() in SQL server trigger

If there's:
IF UPDATE (col1)
...in the SQL server trigger on a table, does it return true only if col1 has been changed or been updated?
I have a regular update query like
UPDATE table-name
SET col1 = 'x',
col2 = 'y'
WHERE id = 999
Now what my concern is if the "col1" was 'x' previously then again we updated it to 'x'
would IF UPDATE ("col1") trigger return True or not?
I am facing this problem as my save query is generic for all columns, but when I add this condition it returns True even if it's not changed...So I am concerned what to do in this case if I want to add condition like that?
It returns true if a column was updated. An update means that the query has SET the value of the column. Whether the previous value was the same as the new value is largely irelevant.
UPDATE table SET col = col
it's an update.
UPDATE table SET col = 99
when the col already had value 99 also it's an update.
Within the trigger, you have access to two internal tables that may help. The 'inserted' table includes the new version of each affected row, The 'deleted' table includes the original version of each row. You can compare the values in these tables to see if your field value was actually changed.
Here's a quick way to scan the rows to see if ANY column changed before deciding to run the contents of a trigger. This can be useful for example when you want to write a history record, but you don't want to do it if nothing really changed.
We use this all the time in ETL importing processes where we may re-import data but if nothing really changed in the source file we don't want to create a new history record.
CREATE TRIGGER [dbo].[TR_my_table_create_history]
ON [dbo].[my_table] FOR UPDATE AS
BEGIN
--
-- Insert the old data row if any column data changed
--
INSERT INTO [my_table_history]
SELECT d.*
FROM deleted d
INNER JOIN inserted i ON i.[id] = d.[id]
--
-- Use INTERSECT to see if anything REALLY changed
--
WHERE NOT EXISTS( SELECT i.* INTERSECT SELECT d.* )
END
Note that this particular trigger assumes that your source table (the one triggering the trigger) and the history table have identical column layouts.
What you do is check for different values in the inserted and deleted tables rather than use updated() (Don't forget to account for nulls). Or you could stop doing unneeded updates.
Trigger:
CREATE TRIGGER boo ON status2 FOR UPDATE AS
IF UPDATE (id)
BEGIN
SELECT 'DETECT';
END;
Usage:
UPDATE status2 SET name = 'K' WHERE name= 'T' --no action
UPDATE status2 SET name = 'T' ,id= 8 WHERE name= 'K' --detect
To shortcut the "No actual update" case, you need also check at the beginning whether your query affected any rows at all:
set nocount on; -- this must be the first statement!
if not exists (select 1 from inserted) and not exists (select 1 from deleted)
return;
SET NOCOUNT ON;
declare #countTemp int
select #countTemp = Count (*) from (
select City,PostCode,Street,CountryId,Address1 from Deleted
union
select City,PostCode,Street,CountryId,Address1 from Inserted
) tempTable
IF ( #countTemp > 1 )
Begin
-- Your Code goes Here
End
-- if any of these "City,PostCode,Street,CountryId,Address1" got updated then trigger
-- will work in " IF ( #countTemp > 1 ) " Code)
This worked for me
DECLARE #LongDescDirty bit = 0
Declare #old varchar(4000) = (SELECT LongDescription from deleted)
Declare #new varchar(4000) = (SELECT LongDescription from inserted)
if (#old <> #new)
BEGIN
SET #LongDescDirty = 1
END
Update table
Set LongDescUpdated = #LongDescUpdated
.....