How to identify the operation type(insert,update,delete) in SQL Server trigger - sql

We are using the following trigger in SQL Server to maintain the history now I need to identify the operations just like insert,update or delete. I found some information HERE but it doesn't works with the SQL Server.
CREATE TRIGGER audit_guest_details ON [PMS].[GSDTLTBL]
FOR INSERT,UPDATE,DELETE
AS
DECLARE #SRLNUB1 INT;
DECLARE #UPDFLG1 DECIMAL(3,0);
SELECT #SRLNUB1 = I.SRLNUB FROM inserted I;
SELECT #UPDFLG1 = I.UPDFLG FROM inserted I;
BEGIN
/* Here I need to identify the operation and insert the operation type in the GUEST_ADT 3rd field */
insert into dbo.GUEST_ADT values(#SRLNUB1,#UPDFLG1,?);
PRINT 'BEFORE INSERT trigger fired.'
END;
GO
But here I need to identify the operation and want to insert operation type accordingly.
Here I don't want to create three trigger for every operations

For Inserted : Rows are in inserted only.
For Updated: Rows are in inserted and deleted.
For Deleted: Rows are in deleted only.
DECLARE #event_type varchar(42)
IF EXISTS(SELECT * FROM inserted)
IF EXISTS(SELECT * FROM deleted)
SELECT #event_type = 'update'
ELSE
SELECT #event_type = 'insert'
ELSE
IF EXISTS(SELECT * FROM deleted)
SELECT #event_type = 'delete'
ELSE
--no rows affected - cannot determine event
SELECT #event_type = 'unknown'

This is a simplified version of Mikhail's answer that uses a searched CASE expression.
DECLARE #Operation varchar(7) =
CASE WHEN EXISTS(SELECT * FROM inserted) AND EXISTS(SELECT * FROM deleted)
THEN 'Update'
WHEN EXISTS(SELECT * FROM inserted)
THEN 'Insert'
WHEN EXISTS(SELECT * FROM deleted)
THEN 'Delete'
ELSE
NULL --Unknown
END;

Since you can get multiple rows at once we do it as follows.
INSERT INTO Log_Table
(
LogDate
,LogAction
-- your field list here
,Field0
-- Example : Tracking new and old value for a specific field
-- Make sure that the [Field1_Old] is nullable or has a default value
,Field1,Field1_Old
)
SELECT
LogDate=GETDATE()
,LogAction = CASE WHEN d.[PK_Field] IS NULL THEN 'I' ELSE 'U' END
,i.Field0
,i.Field1, d.Field1
FROM inserted i
LEFT JOIN deleted d on i.[PK_Field]=d.[PK_Field]
WHERE i.[PK_Field] IS NOT NULL
INSERT INTO Log_Table
(
LogDate
,LogAction
-- your field list here
,Field0
-- Example : Tracking new and old value for a specific field
-- Make sure that the [Field1_Old] is nullable or has a default value
,Field1,Field1_Old
)
SELECT
LogDate=GETDATE()
,LogAction = 'D'
,d.Field0
,d.Field1, NULL
FROM deleted d
LEFT JOIN inserted i on i.[PK_Field]=d.[PK_Field]
WHERE i.[PK_Field] IS NULL

create trigger my_trigger on my_table
after update , delete , insert
as
declare #inserting bit
declare #deleting bit
declare #updating bit = 0
select #inserting = coalesce (max(1),0) where exists (select 1 from inserted)
select #deleting = coalesce (max(1),0) where exists (select 1 from deleted )
select #inserting = 0
, #deleting = 0
, #updating = 1
where #inserting = 1 and #deleting = 1
print 'Inserting = ' + ltrim (#inserting)
+ ', Deleting = ' + ltrim (#deleting)
+ ', Updating = ' + ltrim (#updating)
If all three are zero, there are no rows affected and I think there is no way to tell whether it is an update/delete/insert.

Related

Update on a table using inner join in a trigger

I have two tables in the database, they have a one-to-many relationship (Example: The table dbo.Tree [tree_id, tree_code] has many dbo.Fruits[id, name, father_tree_id, father_tree_code]) .
I'm trying to create a trigger for when the column codigo_arvore_pai of dbo.Frutos is updated or inserted into it, the column father_tree_code of dbo.Frutos is updated with the value corresponding to the tree_id of the dbo.Tree table. The condition for this is code_tree of dbo.Tree to be equal code_tree_father of dbo.Fruits.
CREATE TRIGGER [dbo].[tr_updateFruit] on [dbo].[Fruits]
AFTER INSERT, UPDATE
AS
IF (UPDATE(father_tree_code))
BEGIN
UPDATE dbo.Fruits
SET id_arvore_pai = A.id_arvore
FROM dbo.Fruits as obj
INNER JOIN dbo.Tree A ON obj.father_tree_code = A.tree_code
WHERE obj.Id IN (SELECT DISTINCT obj.Id FROM dbo.Fruits)
END;
What's wrong?
When the command to update the SQL server is executed, in fact, first the record is deleted and then the new record is inserted again with new changes, giving the illusion to the user that the editing has been done on the desired fields. But in fact, the update command is a two-step command that consists of a deletion and an insertion. First, you must specify the type of action or command executed. You can use the following code inside of the trigger for this reason:
DECLARE #WhatActionHappened as char(1);
SET #WhatActionHappened = (CASE WHEN EXISTS(SELECT * FROM INSERTED)
AND EXISTS(SELECT * FROM DELETED)
THEN 'U' -- Set to Updated.
WHEN EXISTS(SELECT * FROM INSERTED)
THEN 'I' -- Set to Insert.
WHEN EXISTS(SELECT * FROM DELETED)
THEN 'D' -- Set to Deleted.
ELSE NULL -- Skip. It may have been a "failed delete".
END)
After that, you can use the trigger as follows to insert and edit.
CREATE TRIGGER [dbo].[tr_updateFruit] on [dbo].[Fruits]
AFTER INSERT, UPDATE
AS
BEGIN
DECLARE #WhatActionHappened as char(1);
SET #WhatActionHappened = (CASE WHEN EXISTS(SELECT * FROM INSERTED)
AND EXISTS(SELECT * FROM DELETED)
THEN 'U' -- Set to Updated.
WHEN EXISTS(SELECT * FROM INSERTED)
THEN 'I' -- Set to Insert.
--WHEN EXISTS(SELECT * FROM DELETED)
--THEN 'D' -- Set to Deleted.
ELSE NULL -- Skip. It may have been a "failed delete".
END)
IF(#WhatActionHappened = 'U' OR #WhatActionHappened = 'I')
BEGIN
UPDATE dbo.Fruits
SET id_arvore_pai = A.id_arvore
FROM dbo.Fruits as obj
INNER JOIN dbo.Tree A ON obj.father_tree_code = A.tree_code
WHERE obj.Id IN (SELECT DISTINCT i.Id FROM Inserted i)
END
END

Automation Anywhere SQL results

I am trying to capture if my SQL Query have 0 rows or multiple rows. If it has 0 rows then I will insert, if 1 will perform an update, if > 1 will perform additional analysis.
Is there a way I can see if my query resulted in x results or no results in automation anywhere?
Any assistance will be appreciated.
You can make use of if exists and if not exists and check if rows exists or not, or even if there are multiple before doing the insert.
Here is a simple example using if not exists where if the row doesn't exist on dbo.Table it will insert a row. If it already exists then the ID will be logged to an Error table.
declare #InsertID int = 5, #Name nvarchar(max) = 'some name'
if ((select count(1) from dbo.Table where ID = #InsertID) > 1) -- detect error; more than one record for an id
begin
insert into dbo.Error (ErrorID, ErrorDate)
select #InsertID, getdate()
end
else if not exists (select 1 from dbo.Table where ID = #InsertID) -- no record exists for ID, insert it
begin
insert into dbo.Table (ID, Name)
select #InsertID, #Name
else if exists (select 1 from dbo.Table where ID = #InsertID) -- update the single record
begin
update dbo.Table set Name = #Name where ID = #InsertID
end
A2019 returns the results of a SQL Query as a table...
You could have an if statement right after your query which checks to see if the row count of the returned table is > 0 then take action accordingly.

Multiple Rows with in SQL INSTEAD of Trigger

looking a bit of direction or guidance as to why I’m getting multiple rows using my trigger. Basically I have a web app that controls Asset Types (i.e Laptops, Phones etc), what I’m trying to do with this trigger is when the Asset Type Name (at_typedesc) changes that I log to an audit table (in this case sql_log) what the old name was and what the new name is.
This is working, but for some reason I get multiple lines written at the INSERT TO SQL_LOG statement. It does write the old name & new name, but then I’ll get 3 additional rows which has the old name showing the new name...
This is currently on a 2008 SQL Server.
-- create the trigger
go
create trigger trg_InsteadOfUpdate on [dbo].[lkp_asset_types]
instead of update
as
begin
DECLARE #triggerAction varchar(1)
-- determine the TRIGGER action
-- this allows us to tell if its an INSERT or an UPDATE
SELECT #triggerAction = CASE
WHEN EXISTS(SELECT 1 FROM INSERTED)
AND EXISTS(SELECT 1 FROM deleted) THEN 'U'
WHEN EXISTS(SELECT 1 FROM inserted) THEN 'I'
ELSE 'D' END;
-- get the orginally asset name from the DELETED table
-- this contains the rows as they were BEFORE the UPDATE Statement
DECLARE #orgAssetTypeName varchar(255)
SET #orgAssetTypeName = (SELECT top 1 at_typedesc from lkp_asset_types WHERE at_id = (select at_id from deleted))
-- UPDATE to the new asset name based on the NEW value in the INSERTED Table
update lkp_asset_types
set at_typedesc = (select at_typedesc from inserted)
where at_id = (select at_id from inserted)
-- get the new asset name from the INSERTED table
-- this contains the rows as they were AFTER the UPDATE Statement
DECLARE #newAssetTypeName varchar(255)
SET #newAssetTypeName = (SELECT top 1 at_typedesc from lkp_asset_types WHERE at_id = (select at_id from inserted))
insert into sql_log
(sql_log)
values ('SQL PRE Changed from : ' + #orgAssetTypeName + ' to: ' + #newAssetTypeName + '. Action = ' + #triggerAction)
end
go
Logic like this in a trigger in SQL Server is just broken:
where at_id = (select at_id from inserted)
I really wish the SQL Server parser issued a warning when encountering such constructs.
There is no guarantee that inserted has only one value (nor deleted).
That is how SQL Server defines triggers: as set operations. If multiple rows are inserted at the same, then the inserted and deleted "tables" have multiple rows.
That part is simple. You will need to rewrite the trigger to take this into account.
On checking my web code the update button was performing additional updates, this caused the trigger to fire more than once thus causing duplicate rows.
create table dbo.lkp_asset_types_test
(
at_id int identity,
at_typedesc varchar(100)
)
go
create trigger trg_InsteadOfUpdate_test on [dbo].[lkp_asset_types_test]
instead of update
as
begin
select 'trigger fired!!!!'
if not exists(select * from inserted)
and not exists(select * from deleted)
begin
return;
end
--update (maybe only the diffs?)
update t
set at_typedesc = i.at_typedesc
from dbo.lkp_asset_types_test as t
join inserted as i on t.at_id = i.at_id;
--where t.at_typedesc <> i.at_typedesc & nulls??
--insert into sql_log(sql_log)
select
'SQL PRE Changed from : ' + isnull(d.at_typedesc, '*null*') + ' to: ' + isnull(i.at_typedesc, '*null*') + '. Action = U'
from inserted as i
join deleted as d on i.at_id = d.at_id
--where i.at_typedesc <> d.at_typedesc & nulls ??
end
go
insert into dbo.lkp_asset_types_test(at_typedesc) values ('A'), ('B'), ('C'), ('D'), (NULL);
go
update dbo.lkp_asset_types_test
set at_typedesc = case when at_id%2=0 then isnull(at_typedesc, 'X') else isnull(at_typedesc, '') + 'xyz' end
go
select *
from dbo.lkp_asset_types_test;
go
update dbo.lkp_asset_types_test
set at_typedesc = case when at_id%2=0 then at_typedesc else at_typedesc + 'xyz' end
where 1=2
go
--
drop table lkp_asset_types_test;

Update Trigger For Multiple Rows

I am trying to Insert data in a table named "Candidate_Post_Info_Table_ChangeLogs" whenever a record is updated in another table named "Candidate_Personal_Info_Table". my code works fine whenever a single record is updated but when i try to updated multiple rows it gives error:
"Sub query returned more then 1 value".
Following is my code :
ALTER TRIGGER [dbo].[Candidate_PostInfo_UPDATE]
ON [dbo].[Candidate_Post_Info_Table]
AFTER UPDATE
AS
BEGIN
IF ##ROWCOUNT = 0
RETURN
DECLARE #Candidate_Post_ID int
DECLARE #Candidate_ID varchar(50)
DECLARE #Action VARCHAR(50)
DECLARE #OldValue VARCHAR(MAX)
DECLARE #NewValue VARCHAR(MAX)
DECLARE #Admin_id int
IF UPDATE(Verified)
BEGIN
SET #Action = 'Changed Verification Status'
SET #Candidate_Post_ID = (Select ID From inserted)
SET #Candidate_ID = (Select Identity_Number from inserted)
SET #NewValue = (Select Verified From inserted)
SET #OldValue = (Select Verified From deleted)
IF(#NewValue != #OldValue)
BEGIN
INSERT INTO Candidate_Post_Info_Table_ChangeLogs(Candidate_Post_ID, Candidate_ID, Change_DateTime, action, NewValue, OldValue, Admin_ID)
VALUES(#Candidate_Post_ID, #Candidate_ID, GETDATE(), #Action, #NewValue, #OldValue, '1')
END
END
END
i have searched stack overflow for this issue but couldn't get any related answer specific to this scenario.
When you insert/update multiple rows into a table, the Inserted temporary table used by the system holds all of the values from all of the rows that were inserted or updated.
Therefore, if you do an update to 6 rows, the Inserted table will also have 6 rows, and doing something like this:
SET #Candidate_Post_ID = (Select ID From inserted)
Will return an error, just the same as doing this:
SET #Candidate_Post_ID = (SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6)
From the looks of things, you tried to do this with an iterative approach. Set-based is better. Maybe consider doing it like this in the body of your TRIGGER (without all of the parameters...):
IF UPDATE(Verified)
BEGIN
INSERT INTO Candidate_Post_Info_Table_ChangeLogs
(
Candidate_Post_ID
,Candidate_ID
,Change_DateTime
,action
,NewValue
,OldValue
,Admin_ID
)
SELECT
I.ID
,I.Identity_Number
,GETDATE()
,'Changed Verification Status'
,I.Verified
,O.Verified
,'1'
FROM Inserted I
INNER JOIN Deleted O
ON I.ID = O.ID -- Check this condition to make sure it's a unique join per row
WHERE I.Verified <> O.Verified
END
A similar case was solved in the following thread using cursors.... please check it
SQL Server A trigger to work on multiple row inserts
Also the below thread gives the solution based on set based approach
SQL Server - Rewrite trigger to avoid cursor based approach
*Both the above threads are from stack overflow...

SQL: Query timeout expired

I have a simple query for update table (30 columns and about 150 000 rows).
For example:
UPDATE tblSomeTable set F3 = #F3 where F1 = #F1
This query will affected about 2500 rows.
The tblSomeTable has a trigger:
ALTER TRIGGER [dbo].[trg_tblSomeTable]
ON [dbo].[tblSomeTable]
AFTER INSERT,DELETE,UPDATE
AS
BEGIN
declare #operationType nvarchar(1)
declare #createDate datetime
declare #UpdatedColumnsMask varbinary(500) = COLUMNS_UPDATED()
-- detect operation type
if not exists(select top 1 * from inserted)
begin
-- delete
SET #operationType = 'D'
SELECT #createDate = dbo.uf_DateWithCompTimeZone(CompanyId) FROM deleted
end
else if not exists(select top 1 * from deleted)
begin
-- insert
SET #operationType = 'I'
SELECT #createDate = dbo..uf_DateWithCompTimeZone(CompanyId) FROM inserted
end
else
begin
-- update
SET #operationType = 'U'
SELECT #createDate = dbo..uf_DateWithCompTimeZone(CompanyId) FROM inserted
end
-- log data to tmp table
INSERT INTO tbl1
SELECT
#createDate,
#operationType,
#status,
#updatedColumnsMask,
d.F1,
i.F1,
d.F2,
i.F2,
d.F3,
i.F3,
d.F4,
i.F4,
d.F5,
i.F5,
...
FROM (Select 1 as temp) t
LEFT JOIN inserted i on 1=1
LEFT JOIN deleted d on 1=1
END
And if I execute the update query I have a timeout.
How can I optimize a logic to avoid timeout?
Thank you.
This query:
SELECT *
FROM (
SELECT 1 AS temp
) t
LEFT JOIN
INSERTED i
ON 1 = 1
LEFT JOIN
DELETED d
ON 1 = 1
will yield 2500 ^ 2 = 6250000 records from a cartesian product of INSERTED and DELETED (that is all possible combinations of all records in both tables), which will be inserted into tbl1.
Is that what you wanted to do?
Most probably, you want to join the tables on their PRIMARY KEY:
INSERT
INTO tbl1
SELECT #createDate,
#operationType,
#status,
#updatedColumnsMask,
d.F1,
i.F1,
d.F2,
i.F2,
d.F3,
i.F3,
d.F4,
i.F4,
d.F5,
i.F5,
...
FROM INSERTED i
FULL JOIN
DELETED d
ON i.id = d.id
This will treat update to the PK as deleting a record and inserting another, with a new PK.
Thanks Quassnoi, It's a good idea with "FULL JOIN". It is helped me.
Also I try to update table in portions (1000 items in one time) to make my code works faster because for some companyId I need to update more than 160 000 rows.
Instead of old code:
UPDATE tblSomeTable set someVal = #someVal where companyId = #companyId
I use below one:
declare #rc integer = 0
declare #parts integer = 0
declare #index integer = 0
declare #portionSize int = 1000
-- select Ids for update
declare #tempIds table (id int)
insert into #tempIds
select id from tblSomeTable where companyId = #companyId
-- calculate amount of iterations
set #rc=##rowcount
set #parts = #rc / #portionSize + 1
-- update table in portions
WHILE (#parts > #index)
begin
UPDATE TOP (#portionSize) t
SET someVal = #someVal
FROM tblSomeTable t
JOIN #tempIds t1 on t1.id = t.id
WHERE companyId = #companyId
delete top (#portionSize) from #tempIds
set #index += 1
end
What do you think about this? Does it make sense? If yes, how to choose correct portion size?
Or simple update also good solution? I just want to avoid locks in the future.
Thanks