SQL Trigger Inconsistently firing - sql-server

I have a SQL Trigger on a table that works... most of the time. And I cannot figure out why sometimes the fields are NULL
The trigger works by Updateing the LastUpdateTime whenever something is modified in the field, and the InsertDatetime when first Created.
For some reason this only seems to work some times.
ALTER TRIGGER [dbo].[DateTriggerTheatreListHeaders]
ON [dbo].[TheatreListHeaders]
AFTER INSERT,UPDATE
AS
BEGIN
SET NOCOUNT ON;
IF NOT EXISTS(SELECT * FROM DELETED)
BEGIN
UPDATE ES
SET InsertDatetime = Getdate()
,LastUpdateDateTime = Getdate()
FROM TheatreListHeaders es
JOIN Inserted I ON es.UNIQUETHEATRELISTNUMBER = I.UNIQUETHEATRELISTNUMBER
END
IF UPDATE(LastUpdateDateTime) OR UPDATE(InsertDatetime)
RETURN;
IF EXISTS (
SELECT
*
FROM
INSERTED I
JOIN
DELETED D
-- make sure to compare inserted with (same) deleted person
ON D.UNIQUETHEATRELISTNUMBER = I.UNIQUETHEATRELISTNUMBER
)
BEGIN
UPDATE ES
SET InsertDatetime = ISNULL(es.Insertdatetime,Getdate())
,LastUpdateDateTime = Getdate()
FROM TheatreListHeaders es
JOIN Inserted I ON es.UNIQUETHEATRELISTNUMBER = I.UNIQUETHEATRELISTNUMBER
END
END

A much simpler and efficient approach to do what you are trying to do, would be something like...
ALTER TRIGGER [dbo].[DateTriggerTheatreListHeaders]
ON [dbo].[TheatreListHeaders]
AFTER INSERT,UPDATE
AS
BEGIN
SET NOCOUNT ON;
--Determine if this is an INSERT OR UPDATE Action .
DECLARE #Action as char(1);
SET #Action = (CASE WHEN EXISTS(SELECT * FROM INSERTED)
AND EXISTS(SELECT * FROM DELETED)
THEN 'U' -- Set Action to Updated.
WHEN EXISTS(SELECT * FROM INSERTED)
THEN 'I' -- Set Action to Insert.
END);
UPDATE ES
SET InsertDatetime = CASE WHEN #Action = 'U'
THEN ISNULL(es.Insertdatetime,Getdate())
ELSE Getdate()
END
,LastUpdateDateTime = Getdate()
FROM TheatreListHeaders es
JOIN Inserted I ON es.UNIQUETHEATRELISTNUMBER = I.UNIQUETHEATRELISTNUMBER;
END

"If update()" is poorly defined/implemented in sql server IMO. It does not do what is implied. The function only determines if the column was set by a value in the triggering statement. For an insert, every column is implicitly (if not explicitly) assigned a value. Therefore it is not useful in an insert trigger and difficult to use in a single trigger that supports both inserts and updates. Sometimes it is better to write separate triggers.
Are you aware of recursive triggers? An insert statement will execute your trigger which updates the same table. This causes the trigger to execute again, etc. Is the (database) recursive trigger option off (which is typical) or adjust your logic to support that?
What are your expectations for the insert/update/merge statements against this table? This goes back to your requirements. Is the trigger to ignore any attempt to set the datetime columns directly and set them within the trigger always?
And lastly, what exactly does "works sometimes" actually mean? Do you have a test case that reproduces your issue. If you don't, then you can't really "fix" the logic without a specific failure case. But the above comments should give you sufficient clues. To be honest, your logic seems to be overly complicated. I'll add that it also is logically flawed in the way that it set insertdatetime to getdate if the existing value is null during an update. IMO, it should reject any update that attempts to set the value to null because that is overwriting a fact that should never change. M.Ali has provided an example that is usable but includes the created timestamp problem. Below is an example that demonstrates a different path (assuming the recursive trigger option is off). It does not include the rejection logic - which you should consider. Notice the output of the merge execution carefully.
use tempdb;
set nocount on;
go
create table zork (id integer identity(1, 1) not null primary key,
descr varchar(20) not null default('zippy'),
created datetime null, modified datetime null);
go
create trigger zorktgr on zork for insert, update as
begin
declare #rc int = ##rowcount;
if #rc = 0 return;
set nocount on;
if update(created)
select 'created column updated', #rc as rc;
else
select 'created column NOT updated', #rc as rc;
if exists (select * from deleted) -- update :: do not rely on ##rowcount
update zork set modified = getdate()
where exists (select * from inserted as ins where ins.id = zork.id);
else
update zork set created = getdate(), modified = getdate()
where exists (select * from inserted as ins where ins.id = zork.id);
end;
go
insert zork default values;
select * from zork;
insert zork (descr) values ('bonk');
select * from zork;
update zork set created = null, descr = 'upd #1' where id = 1;
select * from zork;
update zork set descr = 'upd #2' where id = 1;
select * from zork;
waitfor delay '00:00:02';
merge zork as tgt
using (select 1 as id, 'zippity' as descr union all select 5, 'who me?') as src
on tgt.id = src.id
when matched then update set descr = src.descr
when not matched then insert (descr) values (src.descr)
;
select * from zork;
go
drop table zork;

Related

SQL Server INSERT in an AFTER UPDATE Trigger

I'm new to SQL Server, and I'm trying to build a simple update trigger that writes a row to a staging table whenever the column ceu_amount is updated from zero to any number greater than zero.
From using PRINT statements, I know that the variables are containing the correct values to execute the INSERT statement, but no rows are being inserted.
Can you help?
CREATE TRIGGER [dbo].[TRG_Product_Function_Modified] ON [dbo].[Product_Function]
AFTER UPDATE
AS
BEGIN
SET NOCOUNT ON;
--
-- Variable definitions
--
DECLARE #product_code_new as varchar(31)
DECLARE #product_code_old as varchar(31)
--
-- Check if the staging table needs to be updated.
--
SELECT #product_code_new = product_code FROM Inserted where ISNULL(ceu_amount,0) > 0;
SELECT #product_code_old = product_code FROM Deleted where ISNULL(ceu_amount,0) = 0;
IF #product_code_new IS NOT NULL
AND #product_code_old IS NOT NULL
INSERT INTO Product_Function_Staging VALUES (#product_code_new,CURRENT_TIMESTAMP);
END;
This part of code looks suspicious to me..
SELECT #product_code_new = product_code FROM Inserted where ISNULL(ceu_amount,0) > 0;
SELECT #product_code_old = product_code FROM Deleted where ISNULL(ceu_amount,0) = 0;
IF #product_code_new IS NOT NULL
AND #product_code_old IS NOT NULL
INSERT INTO Product_Function_Staging VALUES (#product_code_new,CURRENT_TIMESTAMP);
The above will work fine ,if there is only one row updated,what if there is more than one value..the product_code will default to last value
You can change the above part of code to below
Insert into Product_Function_Staging
select product_code ,CURRENT_TIMESTAMP from inserted where product_code is not null
You will get undetermined values for #product_code_new if there are more than one rows updated with ceu_amount>0; Similar for #product_code_old if more than one rows updated with ceu_amount NULL or equal 0.
Can you post some sample data?
I would not use variables like that in a trigger, since what causes the trigger could be an update to more than one row, at which point you would have multiple rows in your updated and deleted tables.
I think we can more safely and efficiently make this insert with one simple query, though I'm assuming you have a unique key to use:
CREATE TRIGGER [dbo].[TRG_Product_Function_Modified] ON [dbo].[Product_Function]
AFTER UPDATE
AS
BEGIN
SET NOCOUNT ON;
INSERT INTO Product_Function_Staging
SELECT i.product_code, CURRENT_TIMESTAMP
FROM inserted i
JOIN deleted d ON i.product_code = d.product_code -- assuming product_code is unique
WHERE i.ceu_amount > 0 -- new value > 0
AND ISNULL(d.ceu_amount, 0) = 0; -- old value null or 0
END;
I'm not sure where you need to check for nulls in your data, so I've made a best guess in the where clause.
Try using this
CREATE TRIGGER [dbo].[Customer_UPDATE]
ON [dbo].[Customers]
AFTER UPDATE
AS
BEGIN
SET NOCOUNT ON;
DECLARE #CustomerId INT
DECLARE #Action VARCHAR(50)
SELECT #CustomerId = INSERTED.CustomerId
FROM INSERTED
IF UPDATE(Name)
BEGIN
SET #Action = 'Updated Name'
END
IF UPDATE(Country)
BEGIN
SET #Action = 'Updated Country'
END
INSERT INTO CustomerLogs
VALUES(#CustomerId, #Action)
END

T-SQL Trigger - prevent duplicate, but not always

I would like to avoid INSERTS and UPDATES which could duplicate field's value, but not always.
I have a varchar field Cat_Catalog. in table Catalog.
I can have two rows with Cat_Catalog's value "123" duplicated, but I cannot have duplicated field Cat_Catalog which starts with 'KAT' word (so I cannot have 2 rows with "KAT123" Cat_Catalog's value)
The following trigger i made doesn't work fine because field that's going to be updated starts with KAT trigger always raise error (variable #IfExist always return true - it is probably because of AFTER UPDATE,INSERT syntax).
I would like to avoid using INSTEAD OF syntax because updates are generated by some API which to i have no documentation and I'm not sure what to do in case when value doesn't starts with 'KAT'.
GO
/****** Object: Trigger [dbo].[Catalog_InsertUpdateCatalog] ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
--If first three characters are 'KAT'
--then check for duplicate and raiseerror
ALTER TRIGGER [dbo].[Catalog_InsertUpdateCatalog] ON [dbo].[Catalog]
FOR UPDATE,INSERT
AS
set nocount on;
DECLARE #CatalogInsert varchar(50)
DECLARE #IfKat varchar(10) = 'FALSE'
DECLARE #IfExist varchar(10) = 'FALSE'
SELECT #CatalogInsert = Cat_Catalog
FROM inserted
--Does It starts with 'KAT' ?
IF (#CatalogInsert like 'KAT%')
BEGIN
SET #IfKat = 'TRUE'
END
--Check for Duplicate
IF EXISTS(
Select * from Test.dbo.Catalog t
where t.Cat_Catalog = #CatalogInsert
)
BEGIN
SET #IfExist = 'TRUE'
END
IF ( #IfExist = 'TRUE' and #IfKat = 'TRUE' )
BEGIN
RAISERROR ('Catalog allready exists: %s , ISKAT:%s , EXIST:%s', 16, 1, #CatalogInsert, #IfKat, #IfExist);
END
`
The problem is that I don't know how can I check the current value to be updated allready exists in Catalog table (check must be done before update).
Using CHECK constraint instead of a trigger would be a better solution, since triggers are executed much later in the transaction and eventual rollback could be expensive. You can define check constraint as follows:
CREATE FUNCTION IsDuplicate(#col varchar(50))
RETURNS BIT
AS
BEGIN
IF CHARINDEX('KAT', #col) = 1 AND (SELECT COUNT(*) FROM [Catalog] WHERE [Cat_Catalog] = #col) > 1
return 1;
return 0;
END;
GO
ALTER TABLE [Catalog]
ADD CONSTRAINT chkForDuplicates CHECK (dbo.IsDuplicate([Cat_Catalog]) = 0)
GO
Have in mind that if you already have duplicate "KATxxx" values in the table you'll have to either delete them or create the constraint with NOCHECK clause.
Please, try this new version:
ALTER TRIGGER [dbo].[Catalog_InsertUpdateCatalog] ON [dbo].[Catalog]
FOR UPDATE,INSERT
AS BEGIN
set nocount on;
--Check for Duplicate
IF EXISTS(
Select 1
From (
-- Updated
SELECT
COUNT(*) OVER (PARTITION BY Cat_Catalog) Cnt,
Cat_Catalog
FROM dbo.Catalog
WHERE
Cat_Catalog LIKE 'KAT%'
) t
Join inserted i ON t.Cat_Catalog = i.Cat_Catalog AND Cnt > 1
)
BEGIN
RAISERROR ('Catalog allready exists!', 16, 1);
ROLLBACK TRANSACTION;
END
END
This trigger check the existence of rows with the same [Cat_Catalog] field as in the inserted (or updated) rows. If duplicated rows exists and [Cat_Catalog] starts with 'KAT' trigger RAISE error + rollback transaction.
UPDATED: I change trigger. It should now work correctly (i test it). Trigger FOR UPDATE, INSERT fires after changes take place in table. So we need check duplicate rows in the table.
I do it throught COUNT(*) OVER(PARTITION BY Cat_Catalog) but you may check it in a more familiar way:
SELECT
COUNT(*) Cnt,
Cat_Catalog
FROM
dbo._Catalog
WHERE
Cat_Catalog LIKE 'KAT%'
GROUP BY
Cat_Catalog
Use Instead OF Trigger instead After Trigger.
If your problem with Instead OF Triggers, then you need to use NOT Eqals operator to avoid such wrong indication to passed. I supposed to say
DECLARE #CATALOG_PK BIGINT;
SELECT #CATALOG_PK = CATALOG_PK FROM INSERTED
--Check for Duplicate
IF EXISTS(
Select * from Test.dbo.Catalog t
where t.Cat_Catalog = #CatalogInsert
AND CATALOG_PK <>#CATELOG_PK LIKE t.CATALOG LIKE 'KAT%'
)
BEGIN
SET #IfExist = 'TRUE'
END
However this will fail in-case of multiple UPDATES or INSERTS done.
Try these two triggers:
CREATE TRIGGER [MyCatalog_InsertCatalog] ON [dbo].[MyCatalog]
FOR INSERT
AS
IF EXISTS(
SELECT I.*
FROM Inserted I
INNER JOIN MyCatalog M
ON M.Cat_Catalog = I.Cat_Catalog
WHERE I.Cat_Catalog LIKE 'KAT%' )
BEGIN
RAISERROR ('Insert Catalog already exists: ', 16, 1);
ROLLBACK TRANSACTION;
END
GO
CREATE TRIGGER [MyCatalog_UpdateCatalog] ON [dbo].[MyCatalog]
FOR UPDATE
AS
IF EXISTS(
SELECT I.*
FROM Inserted I
INNER JOIN MyCatalog M
ON M.Cat_Catalog = I.Cat_Catalog
INNER JOIN deleted d
ON d.Cat_Id = i.Cat_Id
WHERE I.Cat_Catalog LIKE 'KAT%' AND d.Cat_Catalog <> i.Cat_Catalog)
BEGIN
RAISERROR ('Update Catalog already exists: ', 16, 1);
ROLLBACK TRANSACTION;
END
EDIT
You could combine both triggers into one:
CREATE TRIGGER [MyCatalog_InsertUpdateCatalog] ON [dbo].[MyCatalog]
FOR INSERT, UPDATE
AS
IF EXISTS(
SELECT I.*
FROM Inserted I
INNER JOIN MyCatalog M
ON M.Cat_Catalog = I.Cat_Catalog
INNER JOIN deleted d
ON d.Cat_Id = i.Cat_Id
WHERE I.Cat_Catalog LIKE 'KAT%' AND d.Cat_Catalog <> i.Cat_Catalog)
BEGIN
RAISERROR ('Update Catalog already exists: ', 16, 1);
ROLLBACK TRANSACTION;
END
ELSE BEGIN
IF EXISTS(
SELECT I.*
FROM Inserted I
INNER JOIN MyCatalog M
ON M.Cat_Catalog = I.Cat_Catalog
WHERE I.Cat_Catalog LIKE 'KAT%' )
BEGIN
RAISERROR ('Insert Catalog already exists: ', 16, 1);
ROLLBACK TRANSACTION;
END
END

Trigger for three operation(Update,Delete,Insert) conversion error

I'm trying to create one trigger for insert,delete and update on customers table.
Trigger was created successfully ,however, I'm sure it contains many errors.
Here is my code of trigger
go
create trigger Onetrig
on customers
after update,insert,delete
as
declare #log varchar(100),#name varchar(20),#activity varchar(20)
begin
if exists(select * from deleted) and exists(select * from deleted)
set #activity = 'Update'
set #name = (select c_name from inserted)
set #log = 'Record Updated in database '+#name
insert into logs
values(#activity,#log,GETDATE())
if exists(select * from inserted) and not exists(select * from deleted)
set #activity = 'Insert'
set #name = (select c_name from inserted)
set #log = 'Record Inserted in database '+#name
insert into logs
values(#activity,#log,GETDATE())
if exists(select * from deleted) and not exists(select * from inserted)
set #activity = 'Insert'
set #name = (select c_name from deleted)
set #log = 'Record Deleted from database '+#name
insert into logs
values(#activity,#log,GETDATE())
end
My task is to populate the log table with the activities of these three operations. when I perform any of the three operations it throws error of some kind of conversion.
Msg 8152, Level 16, State 14, Procedure Onetrig, Line 11
String or binary data would be truncated.
The statement has been terminated.
customers table has some records.
Here is the code of my customers & log table:
create table logs
(activity varchar(20),report varchar(20),Time datetime)
create table customers
(c_id int primary key identity(1,1) NOT NULL,c_name varchar(20),c_lastname varchar(20))
What conversion am I missing?
You have declared report to be 20 characters in logs. But, you are inserting the string 'Record Updated in database '+#name into it. This has at least 27 characters.
Suggestions:
Fix the length of the varchar fields. You might as well make them much longer, even up to 8000 characters.
When doing insert, always include the list of columns explicitly.
Fix the first if statement to refer to inserted and deleted, so the logic makes sense.
Put semicolons at the end of each statement.
Gordon Linoff's answer is very good.
I would like to add that in case of trigger, following lines
set #name = (select c_name from inserted)
set #name = (select c_name from deleted)
assume that there is always only one row inserted / deleted / updated. Which is wrong because DML triggers are at statement level not at row level.
I would change (for I/U/D) thus:
if exists ...
begin
set #activity = 'U/I/D'
insert into dbo.logs (... columns ...) -- <-- You should use the schema (default schema is dbo)
select #activity, 'Record Updated in database ' + x.c_name, GETDATE()
-- ^
-- |
-- ----------------------------------------------------------
-- The maximum length of dbo.logs.c_name (!?) column should be
-- LEN('Record Updated in database') + 1 + MaxLength of dbo.customers.c_name column
from inserted as x -- or deleted
end

Create Transaction and Trigger on a field

Please am new in vb.net and sql server, I have created a two tables in database called Service and Trans.
Create Table Service(
ServiceID int,
ServiceName varchar(30),
ServiceStartValue int
);
Create Table Trans(
EntryTS datetime,
EntryCounter int,
ServedTS datetime,
ServedCounter int,
Skipped int
);
I am trying to create a 'transaction and trigger' that will check and update ServedCounter based on the values in EntryCounter upon ServiceID which the update statement must not allow the ServedCounter > EntryCounter.
I don't quite understand the full requirement, but here's how you can prevent a update (or insert) from happening with a trigger.
CREATE TRIGGER Trans_upd_trg ON Trans AFTER INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON;
--Don't allow the update
IF EXISTS(SELECT 1 FROM inserted WHERE ServedCounter > EntryCounter)
RAISERROR ('ServedCounter > EntryCounter', 16, 1 );
END
GO
Within the context of the trigger you have two logical tables, INSERTED, and DELETED.
These tables contain the old and new values. (deleted is empty for a insert operation)
Hope that helps.
Use a Instead Of Trigger
CREATE TRIGGER Trans_upd_trg
ON Trans
Instead OF INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON;
IF EXISTS (SELECT *
FROM inserted
WHERE ServedCounter > EntryCounter)
AND EXISTS (SELECT *
FROM deleted)
UPDATE A
SET EntryTS = I.EntryTS,
EntryCounter = I.EntryCounter,
ServedTS = I.ServedTS,
ServedCounter = I.ServedCounter,
Skipped = I.Skipped
FROM Trans A
JOIN inserted I
ON A.EntryTS = I.EntryTS
AND A.ServedTS = I.ServedTS
WHERE i.ServedCounter > i.EntryCounter
ELSE
INSERT INTO Trans
SELECT *
FROM inserted
WHERE ServedCounter > EntryCounter
END
GO

Insert Update trigger how to determine if insert or update

I need to write an Insert, Update Trigger on table A which will delete all rows from table B whose one column (say Desc) has values like the value inserted/updated in the table A's column (say Col1). How would I go around writing it so that I can handle both Update and Insert cases. How would I determine if the trigger is executed for an update or insert.
Triggers have special INSERTED and DELETED tables to track "before" and "after" data. So you can use something like IF EXISTS (SELECT * FROM DELETED) to detect an update. You only have rows in DELETED on update, but there are always rows in INSERTED.
Look for "inserted" in CREATE TRIGGER.
Edit, 23 Nov 2011
After comment, this answer is only for INSERTED and UPDATED triggers.
Obviously, DELETE triggers can not have "always rows in INSERTED" as I said above
CREATE TRIGGER dbo.TableName_IUD
ON dbo.TableName
AFTER INSERT, UPDATE, DELETE
AS
BEGIN
SET NOCOUNT ON;
--
-- Check if this is an INSERT, UPDATE or DELETE Action.
--
DECLARE #action as char(1);
SET #action = 'I'; -- Set Action to Insert by default.
IF EXISTS(SELECT * FROM DELETED)
BEGIN
SET #action =
CASE
WHEN EXISTS(SELECT * FROM INSERTED) THEN 'U' -- Set Action to Updated.
ELSE 'D' -- Set Action to Deleted.
END
END
ELSE
IF NOT EXISTS(SELECT * FROM INSERTED) RETURN; -- Nothing updated or inserted.
...
END
Many of these suggestions do not take into account if you run a delete statement that deletes nothing.
Say you try to delete where an ID equals some value that does not exist in the table.
Your trigger still gets called but there is nothing in the Deleted or Inserted tables.
Use this to be safe:
--Determine if this is an INSERT,UPDATE, or DELETE Action or a "failed delete".
DECLARE #Action as char(1);
SET #Action = (CASE WHEN EXISTS(SELECT * FROM INSERTED)
AND EXISTS(SELECT * FROM DELETED)
THEN 'U' -- Set Action to Updated.
WHEN EXISTS(SELECT * FROM INSERTED)
THEN 'I' -- Set Action to Insert.
WHEN EXISTS(SELECT * FROM DELETED)
THEN 'D' -- Set Action to Deleted.
ELSE NULL -- Skip. It may have been a "failed delete".
END)
Special thanks to #KenDog and #Net_Prog for their answers.
I built this from their scripts.
I'm using the following, it also correctly detect delete statements that delete nothing:
CREATE TRIGGER dbo.TR_TableName_TriggerName
ON dbo.TableName
AFTER INSERT, UPDATE, DELETE
AS
BEGIN
SET NOCOUNT ON;
IF NOT EXISTS(SELECT * FROM INSERTED)
-- DELETE
PRINT 'DELETE';
ELSE
BEGIN
IF NOT EXISTS(SELECT * FROM DELETED)
-- INSERT
PRINT 'INSERT';
ELSE
-- UPDATE
PRINT 'UPDATE';
END
END;
Declare #Type varchar(50)='';
IF EXISTS (SELECT * FROM inserted) and EXISTS (SELECT * FROM deleted)
BEGIN
SELECT #Type = 'UPDATE'
END
ELSE IF EXISTS(SELECT * FROM inserted)
BEGIN
SELECT #Type = 'INSERT'
END
ElSE IF EXISTS(SELECT * FROM deleted)
BEGIN
SELECT #Type = 'DELETE'
END
I believe nested ifs a little confusing and:
Flat is better than nested [The Zen of Python]
;)
DROP TRIGGER IF EXISTS AFTER_MYTABLE
GO
CREATE TRIGGER dbo.AFTER_MYTABLE ON dbo.MYTABLE AFTER INSERT, UPDATE, DELETE
AS BEGIN
--- FILL THE BEGIN/END SECTION FOR YOUR NEEDS.
SET NOCOUNT ON;
IF EXISTS(SELECT * FROM INSERTED) AND EXISTS(SELECT * FROM DELETED)
BEGIN PRINT 'UPDATE' END
ELSE IF EXISTS(SELECT * FROM INSERTED) AND NOT EXISTS(SELECT * FROM DELETED)
BEGIN PRINT 'INSERT' END
ELSE IF EXISTS(SELECT * FROM DELETED) AND NOT EXISTS(SELECT * FROM INSERTED)
BEGIN PRINT 'DELETED' END
ELSE BEGIN PRINT 'NOTHING CHANGED'; RETURN; END -- NOTHING
END
After a lot of searching I could not find an exact example of a single SQL Server trigger that handles all (3) three conditions of the trigger actions INSERT, UPDATE, and DELETE. I finally found a line of text that talked about the fact that when a DELETE or UPDATE occurs, the common DELETED table will contain a record for these two actions. Based upon that information, I then created a small Action routine which determines why the trigger has been activated. This type of interface is sometimes needed when there is both a common configuration and a specific action to occur on an INSERT vs. UPDATE trigger. In these cases, to create a separate trigger for the UPDATE and the INSERT would become maintenance problem. (i.e. were both triggers updated properly for the necessary common data algorithm fix?)
To that end, I would like to give the following multi-trigger event code snippet for handling INSERT, UPDATE, DELETE in one trigger for an Microsoft SQL Server.
CREATE TRIGGER [dbo].[INSUPDDEL_MyDataTable]
ON [dbo].[MyDataTable] FOR INSERT, UPDATE, DELETE
AS
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with caller queries SELECT statements.
-- If an update/insert/delete occurs on the main table, the number of records affected
-- should only be based on that table and not what records the triggers may/may not
-- select.
SET NOCOUNT ON;
--
-- Variables Needed for this Trigger
--
DECLARE #PACKLIST_ID varchar(15)
DECLARE #LINE_NO smallint
DECLARE #SHIPPED_QTY decimal(14,4)
DECLARE #CUST_ORDER_ID varchar(15)
--
-- Determine if this is an INSERT,UPDATE, or DELETE Action
--
DECLARE #Action as char(1)
DECLARE #Count as int
SET #Action = 'I' -- Set Action to 'I'nsert by default.
SELECT #Count = COUNT(*) FROM DELETED
if #Count > 0
BEGIN
SET #Action = 'D' -- Set Action to 'D'eleted.
SELECT #Count = COUNT(*) FROM INSERTED
IF #Count > 0
SET #Action = 'U' -- Set Action to 'U'pdated.
END
if #Action = 'D'
-- This is a DELETE Record Action
--
BEGIN
SELECT #PACKLIST_ID =[PACKLIST_ID]
,#LINE_NO = [LINE_NO]
FROM DELETED
DELETE [dbo].[MyDataTable]
WHERE [PACKLIST_ID]=#PACKLIST_ID AND [LINE_NO]=#LINE_NO
END
Else
BEGIN
--
-- Table INSERTED is common to both the INSERT, UPDATE trigger
--
SELECT #PACKLIST_ID =[PACKLIST_ID]
,#LINE_NO = [LINE_NO]
,#SHIPPED_QTY =[SHIPPED_QTY]
,#CUST_ORDER_ID = [CUST_ORDER_ID]
FROM INSERTED
if #Action = 'I'
-- This is an Insert Record Action
--
BEGIN
INSERT INTO [MyChildTable]
(([PACKLIST_ID]
,[LINE_NO]
,[STATUS]
VALUES
(#PACKLIST_ID
,#LINE_NO
,'New Record'
)
END
else
-- This is an Update Record Action
--
BEGIN
UPDATE [MyChildTable]
SET [PACKLIST_ID] = #PACKLIST_ID
,[LINE_NO] = #LINE_NO
,[STATUS]='Update Record'
WHERE [PACKLIST_ID]=#PACKLIST_ID AND [LINE_NO]=#LINE_NO
END
END
while i do also like the answer posted by #Alex, i offer this variation to #Graham's solution above
this exclusively uses record existence in the INSERTED and UPDATED tables, as opposed to using COLUMNS_UPDATED for the first test.
It also provides the paranoid programmer relief knowing that the final case has been considered...
declare #action varchar(4)
IF EXISTS (SELECT * FROM INSERTED)
BEGIN
IF EXISTS (SELECT * FROM DELETED)
SET #action = 'U' -- update
ELSE
SET #action = 'I' --insert
END
ELSE IF EXISTS (SELECT * FROM DELETED)
SET #action = 'D' -- delete
else
set #action = 'noop' --no records affected
--print #action
you will get NOOP with a statement like the following :
update tbl1 set col1='cat' where 1=2
DECLARE #ActionType CHAR(6);
SELECT #ActionType = COALESCE(CASE WHEN EXISTS(SELECT * FROM INSERTED)
AND EXISTS(SELECT * FROM DELETED) THEN 'UPDATE' END,
CASE WHEN EXISTS(SELECT * FROM DELETED) THEN 'DELETE' END,
CASE WHEN EXISTS(SELECT * FROM INSERTED) THEN 'INSERT' END);
PRINT #ActionType;
Try this..
ALTER TRIGGER ImportacionesGS ON dbo.Compra
AFTER INSERT, UPDATE, DELETE
AS
BEGIN
-- idCompra is PK
DECLARE #vIdCompra_Ins INT,#vIdCompra_Del INT
SELECT #vIdCompra_Ins=Inserted.idCompra FROM Inserted
SELECT #vIdCompra_Del=Deleted.idCompra FROM Deleted
IF (#vIdCompra_Ins IS NOT NULL AND #vIdCompra_Del IS NULL)
Begin
-- Todo Insert
End
IF (#vIdCompra_Ins IS NOT NULL AND #vIdCompra_Del IS NOT NULL)
Begin
-- Todo Update
End
IF (#vIdCompra_Ins IS NULL AND #vIdCompra_Del IS NOT NULL)
Begin
-- Todo Delete
End
END
This might be a faster way:
DECLARE #action char(1)
IF COLUMNS_UPDATED() > 0 -- insert or update
BEGIN
IF EXISTS (SELECT * FROM DELETED) -- update
SET #action = 'U'
ELSE
SET #action = 'I'
END
ELSE -- delete
SET #action = 'D'
I found a small error in Grahams otherwise cool solution:
It should be
IF COLUMNS_UPDATED() <> 0 -- insert or update
instead of > 0
probably because top bit gets interpreted as SIGNED integer sign bit...(?).
So in total:
DECLARE #action CHAR(8)
IF COLUMNS_UPDATED() <> 0 -- delete or update?
BEGIN
IF EXISTS (SELECT * FROM deleted) -- updated cols + old rows means action=update
SET #action = 'UPDATE'
ELSE
SET #action = 'INSERT' -- updated columns and nothing deleted means action=insert
END
ELSE -- delete
BEGIN
SET #action = 'DELETE'
END
A potential problem with the two solutions offered is that, depending on how they are written, an update query may update zero records and an insert query may insert zero records. In these cases, the Inserted and Deleted recordsets will be empty. In many cases, if both the Inserted and Deleted recordsets are empty you might just want to exit the trigger without doing anything.
declare #insCount int
declare #delCount int
declare #action char(1)
select #insCount = count(*) from INSERTED
select #delCount = count(*) from DELETED
if(#insCount > 0 or #delCount > 0)--if something was actually affected, otherwise do nothing
Begin
if(#insCount = #delCount)
set #action = 'U'--is update
else if(#insCount > 0)
set #action = 'I' --is insert
else
set #action = 'D' --is delete
--do stuff here
End
This does the trick for me:
declare #action_type int;
select #action_type = case
when i.id is not null and d.id is null then 1 -- insert
when i.id is not null and d.id is not null then 2 -- update
when i.id is null and d.id is not null then 3 -- delete
end
from inserted i
full join deleted d on d.id = i.id
Since not all columns can be updated at a time you can check whether a particular column is being updated by something like this:
IF UPDATE([column_name])
just simple way
CREATE TRIGGER [dbo].[WO_EXECUTION_TRIU_RECORD] ON [dbo].[WO_EXECUTION]
WITH EXECUTE AS CALLER
FOR INSERT, UPDATE
AS
BEGIN
select #vars = [column] from inserted
IF UPDATE([column]) BEGIN
-- do update action base on #vars
END ELSE BEGIN
-- do insert action base on #vars
END
END
I like solutions that are "computer science elegant." My solution here hits the [inserted] and [deleted] pseudotables once each to get their statuses and puts the result in a bit mapped variable. Then each possible combination of INSERT, UPDATE and DELETE can readily be tested throughout the trigger with efficient binary evaluations (except for the unlikely INSERT or DELETE combination).
It does make the assumption that it does not matter what the DML statement was if no rows were modified (which should satisfy the vast majority of cases). So while it is not as complete as Roman Pekar's solution, it is more efficient.
With this approach, we have the possibility of one "FOR INSERT, UPDATE, DELETE" trigger per table, giving us A) complete control over action order and b) one code implementation per multi-action-applicable action. (Obviously, every implementation model has its pros and cons; you will need to evaluate your systems individually for what really works best.)
Note that the "exists (select * from «inserted/deleted»)" statements are very efficient since there is no disk access (https://social.msdn.microsoft.com/Forums/en-US/01744422-23fe-42f6-9ab0-a255cdf2904a).
use tempdb
;
create table dbo.TrigAction (asdf int)
;
GO
create trigger dbo.TrigActionTrig
on dbo.TrigAction
for INSERT, UPDATE, DELETE
as
declare #Action tinyint
;
-- Create bit map in #Action using bitwise OR "|"
set #Action = (-- 1: INSERT, 2: DELETE, 3: UPDATE, 0: No Rows Modified
(select case when exists (select * from inserted) then 1 else 0 end)
| (select case when exists (select * from deleted ) then 2 else 0 end))
;
-- 21 <- Binary bit values
-- 00 -> No Rows Modified
-- 01 -> INSERT -- INSERT and UPDATE have the 1 bit set
-- 11 -> UPDATE <
-- 10 -> DELETE -- DELETE and UPDATE have the 2 bit set
raiserror(N'#Action = %d', 10, 1, #Action) with nowait
;
if (#Action = 0) raiserror(N'No Data Modified.', 10, 1) with nowait
;
-- do things for INSERT only
if (#Action = 1) raiserror(N'Only for INSERT.', 10, 1) with nowait
;
-- do things for UPDATE only
if (#Action = 3) raiserror(N'Only for UPDATE.', 10, 1) with nowait
;
-- do things for DELETE only
if (#Action = 2) raiserror(N'Only for DELETE.', 10, 1) with nowait
;
-- do things for INSERT or UPDATE
if (#Action & 1 = 1) raiserror(N'For INSERT or UPDATE.', 10, 1) with nowait
;
-- do things for UPDATE or DELETE
if (#Action & 2 = 2) raiserror(N'For UPDATE or DELETE.', 10, 1) with nowait
;
-- do things for INSERT or DELETE (unlikely)
if (#Action in (1,2)) raiserror(N'For INSERT or DELETE.', 10, 1) with nowait
-- if already "return" on #Action = 0, then use #Action < 3 for INSERT or DELETE
;
GO
set nocount on;
raiserror(N'
INSERT 0...', 10, 1) with nowait;
insert dbo.TrigAction (asdf) select top 0 object_id from sys.objects;
raiserror(N'
INSERT 3...', 10, 1) with nowait;
insert dbo.TrigAction (asdf) select top 3 object_id from sys.objects;
raiserror(N'
UPDATE 0...', 10, 1) with nowait;
update t set asdf = asdf /1 from dbo.TrigAction t where asdf <> asdf;
raiserror(N'
UPDATE 3...', 10, 1) with nowait;
update t set asdf = asdf /1 from dbo.TrigAction t;
raiserror(N'
DELETE 0...', 10, 1) with nowait;
delete t from dbo.TrigAction t where asdf < 0;
raiserror(N'
DELETE 3...', 10, 1) with nowait;
delete t from dbo.TrigAction t;
GO
drop table dbo.TrigAction
;
GO
Quick solution MySQL
By the way: I'm using MySQL PDO.
(1) In an auto increment table just get the highest value (my column name = id) from the incremented column once every script run first:
$select = "
SELECT MAX(id) AS maxid
FROM [tablename]
LIMIT 1
";
(2) Run the MySQL query as you normaly would, and cast the result to integer, e.g.:
$iMaxId = (int) $result[0]->maxid;
(3) After the "INSERT INTO ... ON DUPLICATE KEY UPDATE" query get the last inserted id your prefered way, e.g.:
$iLastInsertId = (int) $db->lastInsertId();
(4) Compare and react: If the lastInsertId is higher than the highest in the table, it's probably an INSERT, right? And vice versa.
if ($iLastInsertId > $iMaxObjektId) {
// IT'S AN INSERT
}
else {
// IT'S AN UPDATE
}
I know it's quick and maybe dirty. And it's an old post. But, hey, I was searching for a solution a for long time, and maybe somebody finds my way somewhat useful anyway. All the best!
In first scenario I supposed that your table have IDENTITY column
CREATE TRIGGER [dbo].[insupddel_yourTable] ON [yourTable]
FOR INSERT, UPDATE, DELETE
AS
IF ##ROWCOUNT = 0 return
SET NOCOUNT ON;
DECLARE #action nvarchar(10)
SELECT #action = CASE WHEN COUNT(i.Id) > COUNT(d.Id) THEN 'inserted'
WHEN COUNT(i.Id) < COUNT(d.Id) THEN 'deleted' ELSE 'updated' END
FROM inserted i FULL JOIN deleted d ON i.Id = d.Id
In second scenario don't need to use IDENTITTY column
CREATE TRIGGER [dbo].[insupddel_yourTable] ON [yourTable]
FOR INSERT, UPDATE, DELETE
AS
IF ##ROWCOUNT = 0 return
SET NOCOUNT ON;
DECLARE #action nvarchar(10),
#insCount int = (SELECT COUNT(*) FROM inserted),
#delCount int = (SELECT COUNT(*) FROM deleted)
SELECT #action = CASE WHEN #insCount > #delCount THEN 'inserted'
WHEN #insCount < #delCount THEN 'deleted' ELSE 'updated' END
DECLARE #INSERTEDCOUNT INT,
#DELETEDCOUNT INT
SELECT #INSERTEDCOUNT = COUNT([YourColumnName]) FROM inserted
SELECT #DELETEDCOUNT = COUNT([YourColumnName]) FROM deleted
IF its updation
#INSERTEDCOUNT = 1
#DELETEDCOUNT = 1
if its insertion
#INSERTEDCOUNT = 1
#DELETEDCOUNT = 0
I've used those exists (select * from inserted/deleted) queries for a long time, but it's still not enough for empty CRUD operations (when there're no records in inserted and deleted tables). So after researching this topic a little bit I've found more precise solution:
declare
#columns_count int = ?? -- number of columns in the table,
#columns_updated_count int = 0
-- this is kind of long way to get number of actually updated columns
-- from columns_updated() mask, it's better to create helper table
-- or at least function in the real system
with cte_columns as (
select #columns_count as n
union all
select n - 1 from cte_columns where n > 1
), cte_bitmasks as (
select
n,
(n - 1) / 8 + 1 as byte_number,
power(2, (n - 1) % 8) as bit_mask
from cte_columns
)
select
#columns_updated_count = count(*)
from cte_bitmasks as c
where
convert(varbinary(1), substring(#columns_updated_mask, c.byte_number, 1)) & c.bit_mask > 0
-- actual check
if exists (select * from inserted)
if exists (select * from deleted)
select #operation = 'U'
else
select #operation = 'I'
else if exists (select * from deleted)
select #operation = 'D'
else if #columns_updated_count = #columns_count
select #operation = 'I'
else if #columns_updated_count > 0
select #operation = 'U'
else
select #operation = 'D'
It's also possible to use columns_updated() & power(2, column_id - 1) > 0 to see if the column is updated, but it's not safe for tables with big number of columns. I've used a bit complex way of calculating (see helpful article below).
Also, this approach will still incorrectly classifies some updates as inserts (if every column in the table is affected by update), and probably it will classifies inserts where there only default values are inserted as deletes, but those are king of rare operations (at lease in my system they are).
Besides that, I don't know how to improve this solution at the moment.
COLUMNS_UPDATED() for audit triggers by Piotr Rodak
Dealing with very large bitmasks by Adam Machanic
declare #result as smallint
declare #delete as smallint = 2
declare #insert as smallint = 4
declare #update as smallint = 6
SELECT #result = POWER(2*(SELECT count(*) from deleted),1) + POWER(2*(SELECT
count(*) from inserted),2)
if (#result & #update = #update)
BEGIN
print 'update'
SET #result=0
END
if (#result & #delete = #delete)
print 'delete'
if (#result & #insert = #insert)
print 'insert'
i do this:
select isnull((select top 1 1 from inserted t1),0) + isnull((select top 1 2 from deleted t1),0)
1 -> insert
2 -> delete
3 -> update
set #i = isnull((select top 1 1 from inserted t1),0) + isnull((select top 1 2 from deleted t1),0)
--select #i
declare #action varchar(1) = case #i when 1 then 'I' when 2 then 'D' when 3 then 'U' end
--select #action
select #action c1,* from inserted t1 where #i in (1,3) union all
select #action c1,* from deleted t1 where #i in (2)

Resources