After Update Trigger puzzlement - sql

I am trying to get my head round an AFTER UPDATE trigger.
Currently in our DB there is a Trigger that contains a cursor. From my understanding cursors in triggers are generally bad performing, so I'm trying to get rid of the cursor.
Currently the trigger looks like this:
ALTER TRIGGER [dbo].[trg_TaskMovement_Zone] ON [dbo].[Tasks_Movement]
AFTER INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON
DECLARE #rowcheck int
DECLARE #MovementID INT
DECLARE #SiteFromID INT
DECLARE #SiteToID INT
DECLARE #SiteResponsibleID INT
DECLARE #FromAddress_Postcode Varchar(20)
DECLARE #ToAddress_Postcode Varchar(20)
DECLARE zcursor CURSOR FOR SELECT ID, SiteFromID, SiteToID, SiteResponsibleID
, FromAddress_Postcode, ToAddress_Postcode FROM inserted
OPEN zcursor
SELECT #rowcheck=1
WHILE #rowcheck=1
BEGIN
FETCH NEXT FROM zcursor INTO #MovementID, #SiteFromID, #SiteToID, #SiteResponsibleID, #FromAddress_Postcode, #ToAddress_Postcode
IF (##FETCH_STATUS = 0)
BEGIN
UPDATE Tasks_Movement
SET ZoneFromID = dbo.fn_GetZoneFromPostcode(#FromAddress_Postcode),
ZoneToID = dbo.fn_GetZoneFromPostcode(#ToAddress_Postcode)
WHERE Tasks_Movement.ID = #MovementID
UPDATE Tasks_Movement
SET SiteResponsibleID = [dbo].[fn_GetDefaultDepotResponsibleForSite](#SiteFromID)
WHERE Tasks_Movement.ID = #MovementID
AND (#SiteResponsibleID Is NULL OR #SiteResponsibleID=0)
AND (#SiteFromID Is NOT NULL AND #SiteFromID>0)
UPDATE Tasks_Movement
SET SiteResponsibleID = [dbo].[fn_GetDefaultDepotResponsibleForSite](#SiteToID)
WHERE Tasks_Movement.ID = #MovementID
AND (#SiteResponsibleID Is NULL OR #SiteResponsibleID=0)
AND (#SiteToID Is NOT NULL AND #SiteToID>0)
END
ELSE
SELECT #rowcheck=0
END
CLOSE zcursor
DEALLOCATE zcursor
END
From what I can tell the cursor in this is completely unnecessary(?)
Would I be right in thinking that the following would work better:
ALTER TRIGGER [dbo].[trg_TaskMovement_Zone] ON [dbo].[Tasks_Movement]
AFTER INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON
UPDATE Tasks_Movement
SET ZoneFromID = dbo.fn_GetZoneFromPostcode(inserted.FromAddress_Postcode),
ZoneToID = dbo.fn_GetZoneFromPostcode(inserted.ToAddress_Postcode)
FROM inserted
WHERE Tasks_Movement.ID IN (SELECT id FROM inserted)
UPDATE Tasks_Movement
SET SiteResponsibleID = [dbo].[fn_GetDefaultDepotResponsibleForSite](inserted.SiteFromID)
FROM inserted
WHERE Tasks_Movement.ID IN (SELECT id FROM inserted
WHERE (inserted.SiteResponsibleID Is NULL OR inserted.SiteResponsibleID=0)
AND (inserted.SiteFromID Is NOT NULL AND inserted.SiteFromID>0))
UPDATE Tasks_Movement
SET SiteResponsibleID = [dbo].[fn_GetDefaultDepotResponsibleForSite](#SiteToID)
FROM inserted
WHERE Tasks_Movement.ID IN (SELECT id FROM inserted
WHERE (inserted.SiteResponsibleID Is NULL OR inserted.SiteResponsibleID=0)
AND (inserted.SiteToID Is NOT NULL AND inserted.SiteToID>0))
END

I think your trigger should be something like this:
ALTER TRIGGER [dbo].[trg_TaskMovement_Zone] ON [dbo].[Tasks_Movement]
AFTER INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON
UPDATE tm
SET ZoneFromID = dbo.fn_GetZoneFromPostcode(i.FromAddress_Postcode),
ZoneToID = dbo.fn_GetZoneFromPostcode(i.ToAddress_Postcode)
FROM Tasks_Movement tm
INNER JOIN inserted i
ON i.ID = tm.ID;
UPDATE tm
SET SiteResponsibleID = [dbo].[fn_GetDefaultDepotResponsibleForSite](i.SiteFromID)
FROM Tasks_Movement tm
INNER JOIN inserted i
ON i.ID = tm.ID
WHERE (i.SiteResponsibleID IS NULL OR i.SiteResponsibleID = 0)
AND i.SiteFromID > 0
UPDATE tm
SET SiteResponsibleID = [dbo].[fn_GetDefaultDepotResponsibleForSite](i.SiteToID)
FROM Tasks_Movement tm
INNER JOIN inserted i
ON i.ID = tm.ID
WHERE (i.SiteResponsibleID IS NULL OR i.SiteResponsibleID = 0)
AND i.SiteToID > 0
END
I've changed it to use SQl Server's UPDATE .. FROM syntax, and also removed the redundant null check when you are checking if a site ID > 0. NULL is not greater than or less than 0, so if SiteID is null SiteID > 0 can never evaluate to true, so it is a redundant additional check.
Finally, I would also recommend removing the user defined functions, although I can't see under the hood of these, based on the name they look very much like they are simple loukup functions that could be achived much more efficiently with joins.
EDIT
Rather than using the UPDATE(column) function I would add an additional join to the update to filter for updated rows, e.g.:
UPDATE tm
SET SiteResponsibleID = [dbo].[fn_GetDefaultDepotResponsibleForSite](i.SiteToID)
FROM Tasks_Movement tm
INNER JOIN inserted i
ON i.ID = tm.ID
LEFT JOIN deleted d
ON d.ID = i.ID
WHERE (i.SiteResponsibleID IS NULL OR i.SiteResponsibleID = 0)
AND i.SiteToID > 0
AND AND ISNULL(i.SiteToID, 0) != ISNULL(d.SiteToID);
I'd do it this way because UPDATE(siteToID) will return true if any row has an updated value, so if you update 1,000,000 rows and one has a change it will perform the update on all of them, not just the ones that have changed, by joining to deleted you can limit the update to relevant rows.

Related

UDF don't give the same result

when I exececute the UDF out of the trigger the result is not the same
the UDF return always true when executing in the trigger
but out of the trigger the result is true or false
ALTER FUNCTION [dbo].[MandatExist]
(
#Numero int,
#IdBranche int,
#Exercice int
)
RETURNS bit
AS
BEGIN
DECLARE #Result bit
DECLARE #Nbr int
DECLARE #Categ int
SELECT #Categ = CategorieNumero
FROM Branche
WHERE IdBranche = #IdBranche
SELECT #Nbr=COUNT(*)
FROM Mandat AS M INNER JOIN Branche AS B ON M.IdBranche=B.IdBranche
WHERE (Numero = #Numero) AND (B.CategorieNumero = #Categ) AND (Exercice = #Exercice)
IF #Nbr = 0
SET #Result = 0
ELSE
SET #Result = 1
RETURN #Result
END
the trigger call MandatExist to get if the number exist or not
ALTER TRIGGER [dbo].[ValidInsertUpdate_Mandat]
ON [dbo].[Mandat]
FOR INSERT,UPDATE
AS
BEGIN
SET NOCOUNT ON;
DECLARE #Cloturer AS bit
DECLARE #Exercice AS int
DECLARE #IdBranche AS int
DECLARE #Numero AS int
DECLARE #Message AS nvarchar(100)
SELECT #Cloturer=Cloturer, #Exercice=Exercice, #Numero=Numero, #IdBranche=IdBranche
FROM INSERTED
IF (dbo.MandatExist(#Numero, #IdBranche, #Exercice)=1)
BEGIN
SET #Message = 'Numero de mandat existant.'
RAISERROR(#Message, 16, 1)
ROLLBACK TRAN
END
INSERTED is a table, thus may contain more than one row which means that this code
SELECT #Cloturer=Cloturer, #Exercice=Exercice, #Numero=Numero, #IdBranche=IdBranche
FROM INSERTED
is essentially incorrect.
UDFs are not the best choice for set-based programming and may cause performance degradation. Specifically this UDF makes absolutely no sense, has no reason for encapsulating this code into separate module. This is a trivial EXISTS code.
Entire function can and must be replaced by EXISTS statement, the whole code of the trigger might look like:
IF EXISTS(
SELECT 1
FROM INSERTED
INNER JOIN ...
WHERE ...
)
BEGIN
RAISERROR(...)
END
I'm not sure what's the meaning of your tables and columns, but I assume you are trying to check some uniqueness. So, assuming that you don't want another Mandat record for the same CategorieNumero, the final EXISTS might look like:
IF EXISTS(
SELECT 1
FROM INSERTED i
INNER JOIN Branch b on b.IdBranche = i.IdBranch
-- other branches with the same CategorieNumero; why isn't CategorieNumero unique?
INNER JOIN Branch b_dup on b_dup.CategorieNumero = b.CategorieNumero
-- existing Mandat rows for the same CategorieNumero with any IdBranch
INNER JOIN Mandat m_dup on m_dup = b_dup.IdBranch
-- ensure you're not comparing inserted/updated Mandat row to itself
WHERE i.ID != m_dup.ID
)
...
But your intent is unclear to me and I think that after clarification most of your needs will be easily satisfied by unique constraints.
If you don't want more that one row for each set of (Exercice, Numero, IdBranch) - just add a unique constraint to Mandat table and drop both trigger and function!
Don't overuse triggers. And UDFs.
i have used the solution of Ivan
IF EXISTS(
SELECT 1
FROM INSERTED I INNER JOIN Branche b ON b.IdBranche = i.IdBranche
INNER JOIN Branche b_dup ON b_dup.IdBranche = b.IdBranche
INNER JOIN Mandat m_dup on (m_dup.Exercice = i.Exercice) AND (m_dup.Numero = i.Numero) AND (b_dup.IdBranche=i.IdBranche)
WHERE i.IdMandat != m_dup.IdMandat
)
BEGIN
RAISERROR('error', 16, 1)
ROLLBACK TRAN
END

How to set that one record is marked 1 and all other 0

I have a table Addresses with columns ID (PK), EmpID (int), Address (nvarchar(100), IsDefault (bit).
Now I have to ensure that only one record with same EmpID have IsDefault set to 1.
I have done for now following with triggers.
First one is an insert trigger. First it's checks if there is entered value 1 in IsDefault. If yes, then it checks if there are more then one record with same EmpID and IsDefault value set on 1. If that is also true, then it sets all other IsDefault values for the same EmpID to 0. :
create trigger [dbo].[TRG_dbo_Addresses_IsDefault_OnlyOneRecord_insert]
on [dbo].[lAddressesOrganisations]
after insert
as
begin
set nocount on;
begin try
if exists (
select *
from inserted as i
where i.IsDefault = 1)
begin
if (
select count(*)
from dbo.Addresses as lao
inner join inserted as i on i.ID=lao.ID
where lao.IsDefault = 1
and lao.EmpID = i.EmpID
) > 1
begin
update lao
set lao.IsDefault = 0
from dbo.Addresses as lao
where (
select row_number () over (partition by EmpID order by ID desc) as rn
from dbo.Addresses as lao
) > 1
end
end
end try
begin catch
if ##trancount > 0
rollback tran;
end catch
end
This other one is update trigger that I don't have a clue how to write it. First it does the same as insert trigger, checking the inserted value is 1 where value was 0. If yes, if there is more then one record with same EmpID and IsDefault set to 1. If yes,
how to write that all other records are set to 0 for same EmpID, while the one that is being updated remain 1?
create trigger [dbo].[TRG_dbo_Addresses_IsDefault_OnlyOneRecord_update]
on [dbo].[Addresses]
after update
as
begin
set nocount on;
begin try
if exists (
select *
from inserted as i
inner join deleted as d on d.ID=i.ID
where i.IsDefault = 1
and d.IsDefault = 0)
begin
if (
select count(*)
from dbo.Addresses as lao
inner join inserted as i on i.ID=lao.ID
where lao.IsDefault = 1
and lao.EmpID = i.EmpID
) > 1
begin
update lao
set lao.IsDefault = 0
from dbo.Addresses as lao
inner join inserted as i on i.ID=lao.ID
where (I don't have an idea what to put here)
and lao.OrganisationID = i.OrganisationID
end
end
end try
begin catch
if ##trancount > 0
rollback tran;
end catch
end
I'm working on ms sql 2016.
I agree with Matt: if possible, avoid trigger.
Anyway, I think in your UPDATE trigger you should change to:
if (
select count(*)
from dbo.Addresses as lao
inner join inserted as i on lao.EmpID = i.EmpID
where lao.IsDefault = 1
) > 1
begin
update lao
set lao.IsDefault = 0
from dbo.Addresses as lao
inner join inserted as i on lao.EmpID = i.EmpID
where lao.IsDefault = 1
and lao.ID <> i.id
end
Morevore, you can rewrite it to:
IF EXISTS(SELECT 1 FROM
FROM dbo.Addresses as lao
INNER join inserted as i on lao.EmpID = i.EmpID
WHERE lao.IsDefault = 1 AND lao.ID <>i.ID)
BEGIN
UPDATE lao
SET lao.IsDefault = 0
FROM dbo.Addresses as lao
INNER JOIN inserted as i on lao.EmpID = i.EmpID
WHERE lao.IsDefault = 1
and lao.ID <> i.id
END
You should change your insert trigger too.
Updated: insert trigger.
As far I can see (but I can't do tests, so please do complete case tests), and if ID is always max value or if you want to preserve default of last id inserted, I think you could rewrite your insert trigger as following: (you could remove IF too, if you don't care about doing always an UPDATE for zero rows too)
as
begin
set nocount on;
begin try
IF EXISTS(SELECT 1
FROM dbo.Addresses as lao
INNER join inserted as i on lao.EmpID = i.EmpID
WHERE lao.IsDefault = 1
AND lao.ID <>i.ID
AND i.IsDefault=1)
BEGIN
UPDATE lao
SET lao.IsDefault = 0
FROM dbo.Addresses as lao
INNER JOIN inserted as i on lao.EmpID = i.EmpID
WHERE lao.IsDefault = 1
and lao.ID <> i.id
AND i.IsDefault=1
END
end try
begin catch
if ##trancount > 0
rollback tran;
end catch
This change (AND i.IsDefault=1) could be applied to UPDATE trigger too.
I might consider whether the problem that you are trying to solve here could be solved without a trigger. Triggers tend to carry significant performance penalties and can separate logic from the rest of your code, making it a headache to maintain in the future.
If you really want to go with a trigger solution on row insert/update and there is always exactly 1 row marked with the bit, don't try to calculate counts of rows with it. Simply set all rows to 0 for the relevant EmpID and then update the inserted row to 1 after that in the trigger.

Modify SQL trigger to work when inserted table contains more than one row

I've got a SQL trigger written for a table in SQL Server 2008. It works well when there is only one row in the 'inserted' table. How can I modify this trigger to work correctly when there are multiple rows? Performance is key here, so I'd like to stay away from cursors, temp tables, etc. (if possible).
Essentially the trigger checks to see if either the 'ClientID' or 'TemplateID' fields were changed. If they were, and the OriginalClientID or OriginalTemplateID fields are null, it populates them (thus setting the OriginalXXX fields once and only once so I can always see what the first values were).
CREATE TRIGGER [dbo].[trigSetOriginalValues]
ON [dbo].[Review]
FOR INSERT, UPDATE
AS
BEGIN
IF (NOT UPDATE(TemplateID) AND NOT UPDATE(ClientID)) return
DECLARE #TemplateID int
DECLARE #OriginalTemplateID int
DECLARE #ClientID int
DECLARE #OriginalClientID int
DECLARE #ReviewID int
SET #ReviewID = (SELECT ReviewID FROM inserted)
SET #ClientID = (SELECT ClientID FROM inserted)
SET #TemplateID = (SELECT TemplateID FROM inserted)
SET #OriginalTemplateID = (SELECT OriginalTemplateID FROM inserted);
SET #OriginalClientID = (SELECT OriginalClientID FROM inserted);
IF (#OriginalTemplateID IS NULL AND #TemplateID IS NOT NULL)
BEGIN
UPDATE [dbo].[Review] SET OriginalTemplateID = #TemplateID WHERE ReviewID=#ReviewID
END
IF (#OriginalClientID IS NULL AND #ClientID IS NOT NULL)
BEGIN
UPDATE [dbo].[Review] SET OriginalClientID = #ClientID WHERE ReviewID=#ReviewID
END
END
This should be your trigger:
UPDATE A
SET A.OriginalTemplateID = B.TemplateID
FROM [dbo].[Review] A
INNER JOIN INSERTED B
ON A.ReviewID = B.ReviewID
WHERE A.OriginalTemplateID IS NULL AND B.TemplateID IS NOT NULL
UPDATE A
SET A.OriginalClientID = B.ClientID
FROM [dbo].[Review] A
INNER JOIN INSERTED B
ON A.ReviewID = B.ReviewID
WHERE A.OriginalClientID IS NULL AND B.ClientID IS NOT NULL
Though you could still do this on a single UPDATE, but with a more complicated filter.

deleted value of the after delete trigger

I've got problems with my triggers (add and delete):
ALTER TRIGGER [trgAfterShoppingInserted]
ON [ShopingList] AFTER INSERT
AS BEGIN
DECLARE #cardId char(6), #points int, #userId int
SET #cardId = (SELECT cardId FROM inserted)
SET #points = (SELECT points FROM inserted)
SET #userId = (SELECT userId from [Card] WHERE id = #cardId)
IF #userId = 0
BEGIN
Update [Card]
set points = points + #points
where id =#cardId
END
ELSE
Update [Card]
set points = points + #points
where id =#cardId OR
userId = #userId
END
ALTER TRIGGER [trgAfterShoppingDeleted]
ON [ShopingList] AFTER DELETE
AS BEGIN
DECLARE #cardId char(6), #points int, #userId int
SET #cardId = (SELECT cardId FROM inserted)
SET #points = (SELECT points FROM inserted)
SET #userId = (SELECT userId from [Card] WHERE id = #cardId)
IF #userId = 0
BEGIN
Update [Card]
set points = points - #points
where id =#cardId
END
ELSE
Update [Card]
set points = points - #points
where id =#cardId OR
userId = #userId
END
The problem is on the SET #cardId..
The error is:
Subquery returned more than 1 value. This is not permitted when the subquery follows =, !=, <, <= , >, >= or when the subquery is used as an expression.
how is it possibile ?
thanks for any help
Both triggers will not work if your insert or delete statement should ever insert or delete multiple rows. You need to stop assuming that your trigger only deals with a single row - that's just not the case. You need to rewrite your triggers to handle multiple rows at once (in the Inserted and Deleted tables)
As an example - you could rewrite your trgAfterShoppingDeleted something like this:
ALTER TRIGGER [trgAfterShoppingDeleted]
ON [ShopingList] AFTER DELETE
AS BEGIN
UPDATE [Card]
SET points = points - i.points
FROM Inserted i
WHERE Card.id = i.id AND i.UserId = 0
UPDATE [Card]
SET points = points - #points
FROM Inserted i
WHERE i.UserId <> 0 AND (Card.id = i.id OR Card.userId = i.userId)
END
You need to think in sets of data - don't assume single rows in your trigger pseudo tables, and don't do RBAR (row-by-agonizing-row) processing in a trigger.
deleted table contains the records that were deleted as part of a given DELETE statement and can contain multiple rows if the DELETE criteria matched multiple records.
This is what happened in your case and when you tried to select cardId from the deleted table that contained multiple records, the select statement is returning multiple values and so the trigger is throwing that exception.

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