Multiple Row Update SQL Trigger from Single Update SQL Statement - sql

Ok. I am quite new to SQL triggers and have had some issues with them. Insert trigger works just fine, and the Delete trigger also. At first, doing a delete on multiple rows would only delete one, but I managed to figure that one out for myself :)
However, even after extensive searching (here and on Google) I am unable to find a satisfactory answer to the UPDATE trigger that I have. If I do an update like
UPDATE Customers Set CustomerUser = 0 Where CustomerStatus = 3
Then unfortunately, only the one record would be updated, and the other would remain as they were. Obviously, this is no good.
The trigger I am using is:
ALTER TRIGGER [dbo].[TRG_TriggerName] ON [dbo].[USER_Customers]
FOR UPDATE
AS
declare #customerid int;
declare #customervenue int;
declare #customeruser int;
declare #customerarea int;
declare #customerevent int;
declare #customerproject int;
declare #customerstatus int;
select #customerid=i.CustomerID from inserted i;
select #customervenue=i.CustomerVenue from inserted i;
select #customerarea=i.CustomerArea from inserted i;
select #customerevent=i.CustomerEvent from inserted i;
select #customerproject=i.CustomerProject from inserted i;
select #customeruser=i.CustomerUser from inserted i;
select #customerstatus=i.CustomerStatus from inserted i;
Update USER_Instances Set InstanceArea = #customerarea, InstanceVenue = #customervenue, InstanceUser = #customeruser, InstanceStatus = #customerstatus, InstanceEvent = #customerevent, InstanceLastUpdate = GetDate() Where InstanceObject = 17 AND InstanceIdentity = #customerid
GO
As you will immediately realize, this trigger is great - if you want to update just one record. Otherwise, it fails. Now - the main question here would be - How do I catch all the records that need updating, and update them all in one trigger action.
The examples I have seen here on Stack Overflow confuse me somewhat, or seem ineffective - for instance, it seems most of them deal with updating just ONE value in a second/other table, and not a whole bunch like I am trying to do. The ones that appear to work on multiple values, I can not understand :(
So after about 2 hours of searches, I give up, and hope that you can help me :) I realize this is a trigger-newbie issue, and though I know my MS-SQL, triggers are something I have never used, until now. So any help is greatly welcome :)
W

It seems that you need something like this
ALTER TRIGGER [dbo].[TRG_TriggerName] ON [dbo].[USER_Customers]
FOR UPDATE
AS
UPDATE USER_Instances
SET InstanceArea = i.CustomerArea,
InstanceVenue = i.CustomerVenue,
InstanceUser = i.CustomerUser,
InstanceStatus = i.CustomerStatus,
InstanceEvent = i.CustomerEvent,
InstanceLastUpdate = GetDate()
FROM USER_Instances JOIN inserted i
ON InstanceIdentity = i.CustomerID AND InstanceObject = 17
Since inserted virtual table can contain multiple rows you need to JOIN it to correctly do your UPDATE.

Related

Trigger running every time despite conditional statement

I am trying to write an update trigger on a table which would cause it to run an additional update statement only if a certain column has been changed, so far the trigger runs the update no matter what, hoping maybe someone can see what I am doing wrong here.
Here is the trigger.
ALTER TRIGGER [dbo].[StatusChangedUpdateTrigger]
ON [dbo].[Trans_Order]
AFTER UPDATE
AS
DECLARE #OldOrderStatusId INT, #NewStatusOrderId INT, #ERRNUM INT;
BEGIN
SET #OldOrderStatusId = (SELECT OrderStatusId FROM deleted);
SET #NewStatusOrderId = (SELECT OrderStatusId FROM inserted);
IF (#OldOrderStatusId != #NewStatusOrderId)
SET NOCOUNT ON;
UPDATE Trans_Order
SET StatusChanged = 1
WHERE Id = (SELECT ID FROM inserted)
END
For some reason this is running no matter what, I can never set StatusChanged to 0 as it will automatically flip it back to 1 even if the OrderStatusId hasn't changed. So my update statement is running no matter what, so I am guessing I am doing something wrong in the if statement.
Hmmmm . . . Your logic seems strange. I would expect:
UPDATE t
SET StatusChanged = 1
FROM Trans_Order t JOIN
Inserted i
ON t.id = i.id JOIN
Deleted d
ON t.id = d.id
WHERE i.OrderStatusId <> d.OrderStatusId;
You might need to take NULL values into account -- although your code does not.
Note that your code is just a bug waiting to happen, because it assumes that inserted and deleted have only one row.
The specific problem with your code is that it is really:
IF (#OldOrderStatusId != #NewStatusOrderId)
BEGIN
SET NOCOUNT ON;
END;
UPDATE Trans_Order
SET StatusChanged = 1
WHERE Id = (SELECT ID FROM inserted);
Your indentation has confused the logic. However, you should still use the set-based version so the trigger does not fail.
The correct way to approach your trigger is as follows:
create or alter trigger [dbo].[StatusChangedUpdateTrigger] on [dbo].[Trans_Order]
after update
as
set nocount on
if ##RowCount=0 return
if Update(OrderStatusId)
begin
update t
set statusChanged=1
from inserted i join deleted d on d.id=i.id and d.OrderStatusId != i.OrderStatusId
join Trans_Order t on t.id=i.id
end
Always test ##rowcount and return if no rows updated.
Always put set options before DML
As you are only looking to update if a specific column is updated you can test specifically for that and if the update statement that's run doesn't touch that column the trigger will not run.
This will correctly account for multiple rows being updated and only update those where the new value is different to the old value.

T-SQL String Replace is Not Locking - Last Update Wins

I have the following stored procedure, which is intended to iterate through a list of strings, which contains several substrings of the form prefix.bucketName. I want to iterate through each string and each bucket name, and replace the old prefix with a new prefix, but keep the same bucket name.
To give an example, consider this original string:
"(OldPrefix.BucketA)(OldPrefix.BucketB)"
So for example I would like to get:
"(NewPrefix.BucketA)(NewPrefix.BucketB)"
What I actually get is this:
"(OldPrefix.BucketA)(NewPrefix.BucketB)"
So, in general, only one of the prefixes get updated, and it is not predictable which one. Based on some investigation I have done, it appears that both replacements actually work, but only the last one is actually saved. It seems like SQL should be locking this column but instead, both are read at the same time, the replace is applied, and then both are written, leaving the last write as what shows in the column.
Here is the query - All variable names have been changed for privacy - Some error handling and data validation code was left out for brevity:
DECLARE #PrefixID INT = 1478,
DECLARE #PrefixName_OLD NVARCHAR(50) = 'OldPrefix',
DECLARE #PrefixName_NEW NVARCHAR(50) = 'NewPrefix'
BEGIN TRAN
-- Code to rename the section itself here not shown for brevity
UPDATE
dbo.Component
SET
AString= REPLACE(AString,'('+#Prefix_OLD+'.'+b.BucketName+')', '('+#PrefixName_NEW+'.'+b.BucketName+')'),
FROM
dbo.Component sc
JOIN
dbo.ComponentBucketFilterInString fis
ON
sc.ComponentID = fis.ComponentID
JOIN
dbo.Buckets b
ON
fis.BucketID = b.BucketID
WHERE
b.PrefixID = #PrefixID
COMMIT
RETURN 1
When I write the same query using a while loop, it performs as expected:
DECLARE #BucketsToUpdate TABLE
(
BucketID INT,
BucketName VARCHAR(256)
)
INSERT INTO #BucketsToUpdate
SELECT BucketID, BucketName
FROM Buckets WHERE PrefixID = #PrefixID
WHILE EXISTS(SELECT 1 FROM #BucketsToUpdate)
BEGIN
DECLARE #currentBucketID INT,
#currentBucketName VARCHAR(256)
SELECT TOP 1 #currentBucketID = bucketID, #currentBucketName = bucketName FROM #BucketsToUpdate
UPDATE
dbo.Component
SET
AString = REPLACE(AString,'('+#PrefixName_OLD+'.'+#currentBucketName+')', '('+#PrefixName_NEW+'.'+#currentBucketName+')')
FROM
dbo.Component sc
JOIN
dbo.ComponentBucketFilterInString fis
ON
sc.ComponentID = fis.ComponentID
WHERE fis.BucketID = #currentBucketID
DELETE FROM #BucketsToUpdate WHERE BucketID = #currentBucketID
END
Why does the first version fail? How can I fix it?
The problem you are experiencing is "undefined" behavior when there is more than single match possible for UPDATE FROM JOIN.
In order to make your update possible you should run it multiple times updating one pair of values at a time as you proposed in your second code demo.
Related: How is this script updating table when using LEFT JOINs? and Let’s deprecate UPDATE FROM!:
SQL Server will happily update the same row over and over again if it matches more than one row in the joined table, >>with only the result of the last of those updates sticking<<.
Not sure why you are making the whole process so complex. May be I am not clearly understanding the requirement. As per my understanding, you are looking to update only Prefix part for column 'AString' in the table dbo.Component. Current value for example is-
(OldPrefix.BucketA)(OldPrefix.BucketB)
You wants to update the value as-
(NewPrefix.BucketA)(NewPrefix.BucketB)
Am I right? If yes, you can update all records with a simple Update script as below-
DECLARE #PrefixID INT = 1478
DECLARE #PrefixName_OLD NVARCHAR(50) = 'OldPrefix'
DECLARE #PrefixName_NEW NVARCHAR(50) = 'NewPrefix'
UPDATE Component
SET AString= REPLACE(AString,#PrefixName_OLD,#PrefixName_NEW)

Prevent column change without the other

No-one should be allowed to update the customer address column unless the postcode column is also updated. If an attempt is made to update one without the other, then a trigger will fire and the user will be shown an error message.
Any help on how I can do this in Microsoft SQL Server 2012 will be appreciated!
You can use below logic
CREATE TRIGGER [dbo].AU_MyTrigger ON [dbo].MyTable
FOR UPDATE
AS
BEGIN
declare #bu_addr varchar(100)
declare #bu_zip varchar(100)
declare #au_addr varchar(100)
declare #au_zip varchar(100)
select #bu_addr = addr, #bu_zip = zip from DELETED
select #au_addr = addr, #ay_zip = zip from INSERTED
if (#bu_addr <> #au_addr) and (#bu_zip = #au_zip)
BEGIN
-- update table with old values
-- raise error
END
END
Note that if this update can happen in batch, you need to loop through each record and update their value to old and only return error at the end of trigger (outside of loop). In that case, for iterating on updated rows, you need to use CURSOR
You case might not be as easy as I explained, but this is the approach that works.

how to copy row from one table to another before update on specific column in sybase trigger?

I want to copy a row from one table to other table before an update happens on specific column. this column named StartDateTime_ in my table.
can anyone correct this trigger if it is not correct or need optimisation.
CREATE TRIGGER PeriodicHistory_TR_U ON order_db..Periodic
FOR UPDATE
AS IF update(StartDateTime_)
begin
declare #Identity_;
declare #Version_;
DECLARE #Revision_;
declare #Identifier_;
declare #CreationTime_;
declare #CreationUserId_;
declare #StartDateTime_;
SELECT #Identity_= i.Identity_ from inserted i;
SELECT #Version_= i.Version_ from inserted i;
SELECT #Identifier_= i.Identifier_ from inserted i;
SELECT #CreationTime_= i.CreationTime_ from inserted i;
SELECT #CreationUserId_= i.CreationUserId_ from inserted i;
SELECT #StartDateTime_= i.StartDateTime_ from inserted i;
set #Revision_ = #Version_ +1;
insert into order_db..PeriodicHistory(Identity_,Version_,Revision_,Identifier_,CreationTime_,CreationUserId_,StartDateTime_)
values(#Identity_,#Version_,#Identifier_,#CreationTime_,#CreationUserId_,#StartDateTime_);
end
How about this:
CREATE TRIGGER PeriodicHistory_TR_U ON Periodic
FOR UPDATE
AS IF update(StartDateTime_)
BEGIN
INSERT INTO PeriodicHistory(Identity_,Version_,Revision_,Identifier_,CreationTime_,CreationUserId_,StartDateTime_)
SELECT d.Identity_,d.Version_,d.Revision_ + 1,d.Identifier_,d.CreationTime_,d.CreationUserId_,d.StartDateTime_
FROM deleted d
JOIN inserted i
ON d.Identity_ = i.Identity_
END
This eliminates the need for temporary variables.
Think about what happens if the update on Periodic affects more than one row. The original trigger won't be able to store multiple values...
Also, my revised trigger updates the version in the history table in the same way as your original trigger. You probably want to update the version in the Periodic table and keep the old value in the history. As it is after ten updates you could end up with: Periodic.Version_ = 1
PeriodicHistory.Version_ = 2

SQL Server slow stored procedure that deletes

I have written a stored procedure.
Now I see, that this is very poor performance.
I think this is because of the while loop.
ALTER PROCEDURE [dbo].[DeleteEmptyCatalogNodes]
#CatalogId UNIQUEIDENTIFIER,
#CatalogNodeType int = null
AS
BEGIN
SET NOCOUNT ON;
DECLARE #CID UNIQUEIDENTIFIER
DECLARE #CNT int
SET #CID = #CatalogId
SET #CNT = #CatalogNodeType
DELETE cn FROM CatalogNodes cn
LEFT JOIN CatalogNodes as cnj on cn.CatalogNodeId = cnj.ParentId
LEFT JOIN CatalogArticles as ca on cn.CatalogNodeId = ca.CatalogNodeId
WHERE cn.CatalogId = #CID
AND cnj.CatalogNodeId IS NULL
AND ca.ArticleId IS NULL
AND (cn.CatalogNodeType = #CNT OR #CNT IS NULL)
WHILE (##ROWCOUNT > 0)
BEGIN
DELETE cn FROM CatalogNodes cn
LEFT JOIN CatalogNodes as cnj on cn.CatalogNodeId = cnj.ParentId
LEFT JOIN CatalogArticles as ca on cn.CatalogNodeId = ca.CatalogNodeId
WHERE cn.CatalogId = #CID
AND cnj.CatalogNodeId IS NULL
AND ca.ArticleId IS NULL
AND (cn.CatalogNodeType = #CNT OR #CNT IS NULL)
END
END
Do anyone of you can give me a hint on how to do it more 'set' like?
Thanks a lot!
EDIT for comments and answers:
The tables are build like this:
CatalogNodes:
CatalogNodeId|ParentId|Name
1|NULL|Root
2|1|Node1
3|1|Node2
4|2|Node1.1
CatalogArticles:
CatalogNodeId|Name
3|Article1
3|Article2
3|Article3
After my SP was called, Node1 and Node1.1 have to be deleted.
In the first delete statement, Node1.1 will be deleted.
In the While loop, Node1 will be deleted.
I hope my problem is now easier to understand, it is a tree structure.
You just do not need WHILE part as all matched rows will get deleted from the first DELETE statement
your loop doesn't do anything ... the first delete statement will delete a number of records if there are any that comply to your where condition ... so ##rowcount will be greater than 0 but there won't be any records left to be deleted in your second delete statement inside the loop. or did I miss something?
anyway I don't think this executing delete two times in a row has a big influence on the performance ... you should see it if you look at the query plan ...
One way to do this in my point is to create a table variable and put all elements that you have to delete there and use i with join to make delete in one single statement.
CatalogNodes is what you want to delete. Create a select that pulls out all the CatalogNodes you want to get rid of. If there are things tied by foreign key constraints go and delete them first and finally once they are all gotten rid of Delete the CatalogNodes. Temporary tables could be of benefit here as they are held in memory.