We have a table that contains a status column, and associated to that is a column to track when the status changed values.
As an example, we have a status of 'Off' and a status of 'Found Off' along with associated columns, DateOff and DateFoundOff. I'm trying to create a trigger to update these date columns when the status changes.
It seems rather straightforward to me, but what is occurring is when the Status changes the associated date column updates correctly, but the other date column becomes null. So if I change Status = 'Off' DateOff has the correct date but DateFoundOff becomes null and visa versa.
I created two triggers - first one is:
ALTER TRIGGER [GIS].[UPDATE_FOUNDOFF]
ON [GIS].[METEROUTAGEPOINTS]
AFTER UPDATE
AS
IF (UPDATE (OutageStatus))
BEGIN
SET NOCOUNT ON;
UPDATE [gis].[METEROUTAGEPOINTS]
SET DateFoundOff = CURRENT_TIMESTAMP
FROM gis.METEROUTAGEPOINTS mop
INNER JOIN inserted AS i ON i.ConObject = mop.ConObject
WHERE i.OutageStatus = 'Found Off'
END
And the second
ALTER TRIGGER [GIS].[UPDATE_DATES]
ON [GIS].[METEROUTAGEPOINTS]
AFTER UPDATE
AS
IF (UPDATE (OutageStatus))
BEGIN
SET NOCOUNT ON;
UPDATE [gis].[METEROUTAGEPOINTS]
SET DateOff = CURRENT_TIMESTAMP
FROM gis.METEROUTAGEPOINTS mop
INNER JOIN inserted AS i ON i.ConObject = mop.ConObject
WHERE i.OutageStatus = 'Off'
END
I simply do not understand how one trigger is changing the value of the Date column to null that is not associated to the current status value.
Thanks.
Edit: The issue was found to not be with trigger but instead be with how the tool being used to edit the data was holding onto something. Not sure I understand why, but by changing the edit workflow the problem was resolved. Marked answer as correct based on it giving a far better way to write the trigger
Neither of your update statements change DateFoundOff or DateOff to null. Something else must be going on.
However I would improve your trigger as follows:
Only use a single trigger, every trigger has overhead, having one trigger with one update statement will run faster than 2.
You aren't actually checking that the status has changed, all you are checking is that the update included that column. Add a check against the deleted table actually checks whether the value changed.
ALTER TRIGGER [GIS].[UPDATE_FOUNDOFF]
ON [GIS].[METEROUTAGEPOINTS]
AFTER UPDATE
AS
BEGIN
-- Avoid doing any processing if no rows are updated
IF NOT EXISTS (SELECT 1 FROM Inserted) RETURN;
SET NOCOUNT ON;
IF UPDATE(OutageStatus) BEGIN
UPDATE [gis].[METEROUTAGEPOINTS] SET
DateFoundOff = CASE WHEN i.OutageStatus = 'Found Off' AND d.OutageStatus <> 'Found Off' THEN CURRENT_TIMESTAMP ELSE DateFoundOff END
, DateOff = CASE WHEN i.OutageStatus = 'Off' AND d.OutageStatus <> 'Off' THEN CURRENT_TIMESTAMP ELSE DateOff END
FROM gis.METEROUTAGEPOINTS mop
INNER JOIN inserted AS i ON i.ConObject = mop.ConObject
INNER JOIN deleted AS d ON d.ConObject = mop.ConObject;
END;
END;
Related
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.
I have the following trigger, and I need that its only executed when one column value changes, is that possible?
ALTER TRIGGER [dbo].[TR_HISTORICO]
ON [dbo].[Tbl_Contactos]
AFTER UPDATE
AS
BEGIN
IF UPDATE (primerNombre) -- sólo si actualiza PRIMER NOMBRE
BEGIN
INSERT INTO [dbo].[Tbl_Historico] ([fecha],[idUsuario],[valorNuevo], [idContacto],[tipoHistorico] )
SELECT getdate(), 1, [dbo].[Encrypt]([dbo].[Decrypt](primerNombre)), [idContacto], 1
FROM INSERTED
END
END
The problem is the code is executed always even if another column changes
The problem is probably the way you are doing updates in your code. It may be updating every field and not only the one that changed.
In this case you need to check to see if there is a difference between the values in the inserted and deleted pseudo tables. Or fix your code so that it only updates what needs to be updated.
Comparing the value of primerNombre from the inserted and deleted tables
ALTER TRIGGER [dbo].[TR_HISTORICO] ON [dbo].[Tbl_Contactos]
AFTER UPDATE AS
BEGIN
INSERT INTO [dbo].[Tbl_Historico] ([fecha],[idUsuario],[valorNuevo], [idContacto],[tipoHistorico] )
SELECT getdate(), 1, [dbo].[Encrypt]([dbo].[Decrypt](i.primerNombre)), i.[idContacto], 1
FROM INSERTED i
inner join deleted d
on i.idContacto = d.idContacto
where i.primerNombre <> d.primerNombre
END
If primerNombre is nullable, the where will need to handle null comparisons as well.
I have a small performance issue with one of my database triggers in my MS-SQL Server 2014 database.
CREATE TRIGGER [dbo].[TRG_T_TPM_Vehicle_Update] ON [dbo].[T_TPM_Vehicle]
AFTER UPDATE
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
UPDATE T_TPM_Vehicle SET LastUpdated = getdate()
WHERE Vehicle_Number IN (SELECT Vehicle_Number FROM inserted)
UPDATE T_TPM_Vehicle SET [DisturbedSince] = getdate()
WHERE Vehicle_Number IN (SELECT Vehicle_Number FROM inserted WHERE inserted.Emergency_Stop = 1)
AND Vehicle_Number IN (SELECT Vehicle_Number FROM deleted WHERE deleted.Emergency_Stop = 0)
INSERT INTO T_TPM_Vehicle_HistoricalData
([Vehicle_Ref]
,[Vehicle_Number]
,[Vehicle_Type]
,[Pos_X]
,[Pos_Y]
,[Alpha]
,[LastAutoPos_X]
,[LastAutoPos_Y]
,[LastAutoAlpha]
,[Automatic]
,[Manual]
,[Blocked]
,[Loaded]
,[Stoped]
,[Emergency_Stop]
,[User_Required]
,[BatteryAlmostEmpty]
,[BatteryEmpty]
,[BatteryLevel]
,[ChargingRelaisEnable]
,[NavOK]
,[PowerOn]
,[Available]
,[OperatingMinutes]
,[UpdateOperatingMinutes]
,[DataChangedByVIS]
,[Blockingsreleased]
,[Cancelled]
,[ProductID]
,[HUIdent1]
,[HUIdent2]
,[HUType]
,[DisturbedSince])
SELECT inserted.[Vehicle_Ref]
,inserted.[Vehicle_Number]
,inserted.[Vehicle_Type]
,inserted.[Pos_X]
,inserted.[Pos_Y]
,inserted.[Alpha]
,inserted.[LastAutoPos_X]
,inserted.[LastAutoPos_Y]
,inserted.[LastAutoAlpha]
,inserted.[Automatic]
,inserted.[Manual]
,inserted.[Blocked]
,inserted.[Loaded]
,inserted.[Stoped]
,inserted.[Emergency_Stop]
,inserted.[User_Required]
,inserted.[BatteryAlmostEmpty]
,inserted.[BatteryEmpty]
,inserted.[BatteryLevel]
,inserted.[ChargingRelaisEnable]
,inserted.[NavOK]
,inserted.[PowerOn]
,inserted.[Available]
,inserted.[OperatingMinutes]
,inserted.[UpdateOperatingMinutes]
,inserted.[DataChangedByVIS]
,inserted.[Blockingsreleased]
,inserted.[Cancelled]
,inserted.[ProductID]
,inserted.[HUIdent1]
,inserted.[HUIdent2]
,inserted.[HUType]
,inserted.[DisturbedSince]
FROM inserted
END
What it basically does is it sets the LastUpdated column for all rows in inserted and the DisturbedSince column for a subset of the inserted rows.
Finally the inserted rows get copied to a history table. (Every change on any row must be saved for two days). Older data gets deleted by a maintenance job.
As we have up to ~ 300 rows updated per second (Updates to rows can be batched together) We create a big amount of data and recursive updates.
I've now found the INSTEAD OF UPDATE triggers which seem to solve the recursive UPDATE problem caused by my trigger but I would have to process every row of the inserted table one by one with an update statement in the trigger.
I'm not sure if this is really faster. Does anyone of you have a recommendation?
What I really need is to tweak / extend the data rows before they are send to the table. Is there an approach for this?
e.g.: Something like:
CREATE TRIGGER ... INSTEAD OF UPDATE
AS
BEGIN
UPDATE inserted SET LastUpdated = getdate()
UPDATE inserted SET DisturbedSince
WHERE Vehicle_Number IN (SELECT Vehicle_Number FROM inserted WHERE inserted.Emergency_Stop = 1)
AND Vehicle_Number IN (SELECT Vehicle_Number FROM deleted WHERE deleted.Emergency_Stop = 0)
"SAVE INSERTED"
END
and an AFTER UPDATE TRIGGER with the storage of the changed data to the history table.
Thank you for any suggestions.
Thomas
You're right to think that using an INSTEAD OF trigger is the right way to go rather than an AFTER trigger, when you're wanting to change data within the same table as well.
It would be something like:
CREATE TRIGGER [dbo].[TRG_T_TPM_Vehicle_Update] ON [dbo].[T_TPM_Vehicle]
INSTEAD OF UPDATE
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
UPDATE tgt
SET
Vehicle_Ref = i.Vehicle_Ref,
Vehicle_Type = i.Vehicle_Type,
...
LastUpdated = getdate(),
DisturbedSince = CASE WHEN i.Emergency_Stop=1 and d.Emergency_Stop=0
THEN getdate() ELSE d.DisturbedSince END
OUTPUT
inserted.[Vehicle_Ref]
,inserted.[Vehicle_Number]
,inserted.[Vehicle_Type]
...
,inserted.[HUIdent2]
,inserted.[HUType]
,inserted.[DisturbedSince]
INTO T_TPM_Vehicle_HistoricalData
([Vehicle_Ref]
,[Vehicle_Number]
,[Vehicle_Type]
...
,[HUIdent2]
,[HUType]
,[DisturbedSince])
FROM
T_TPM_Vehcile tgt
inner join
inserted i
on
tgt.Vehicle_Number = i.Vehicle_Number
inner join
deleted d
on
tgt.Vehicle_Number = d.Vehicle_Number
You'll note that I've combined both the UPDATEs and the INSERT into the history table into a single compound statement.
You'll also note that it's slightly confusing because there are two inserteds in play here - the inserted as part of the trigger (aliased as i to sidestep some of the confusion) and the inserted as part of the OUTPUT clause.
I am new to triggers and I am in a position where I have to use them. I have an azure db with two triggers on a table, one on insert, one on update.
Insert: fires when a record is inserted to a table. Copies one column to another:
CREATE TRIGGER [dbo].[tr_Set_Adjusted_StartDateTime]
ON [dbo].[Work]
AFTER INSERT
AS
BEGIN
UPDATE dbo.Work
SET [ActualStartDateTime] = [work].[StartDateTime]
FROM inserted
WHERE dbo.Work.WorkUID = inserted.WorkUID;
END
Update trigger (fires when the record gets updated):
CREATE TRIGGER [dbo].[tr_Set_Actual_EndDateTime]
ON [dbo].[Work]
AFTER UPDATE
AS
IF((Select [ActualEndDateTime] from Deleted) is null)
BEGIN
UPDATE dbo.Work
SET [ActualEndDateTime] = GETUTCDATE()
FROM deleted
WHERE dbo.Work.WorkUID = deleted.WorkUID;
END
The second trigger should only execute once: the first time the record is updated. Because the stored procedure that inserts the record doesn't populate all the columns.
The second trigger didn't originally have the IF statement. But there is an admin site that can manipulate the db and set off the update trigger.
The IF statement is now automatically firing the update trigger right away.
Is there a way to disable the update trigger if it is executed BY another trigger? Or only enable the update trigger after the record has been created?
If you have an After Update trigger on a table, it will fire each time there is an update on that table. You cannot tell trigger to Only Fire once.
But there is a way around to it. You can add a field in that table , a BIT field and SET its value to 1 in your trigger, Never manipulate this field in directly. And inside your UPDATE trigger
UPDATE w
SET [ActualEndDateTime] = GETUTCDATE()
,[Tr_Update] = 1
FROM deleted d INNER JOIN dbo.Work w ON w.WorkUID = d.WorkUID
WHERE [Tr_Update] = 0
AND [ActualEndDateTime] IS NULL
On a side note you are checking if the user hasn't put any date you want to add Current Datetime to [ActualEndDateTime] column. and since this is an after update trigger if you just execute the above statement with WHERE clause [ActualEndDateTime] IS NULL,
It would update [ActualEndDateTime] to current datetime when a row is updated for the first time and next time because [ActualEndDateTime] field would not be null it would simply filter it out anyway.
UPDATE w
SET [ActualEndDateTime] = GETUTCDATE()
FROM deleted d INNER JOIN dbo.Work w ON w.WorkUID = d.WorkUID
WHERE [ActualEndDateTime] IS NULL
I have the following trigger
First trigger:
ALTER TRIGGER [dbo].[DIENSTLEISTUNG_Update]
ON [dbo].[DIENSTLEISTUNG]
INSTEAD OF UPDATE
AS
BEGIN
SET NOCOUNT ON;
DECLARE #intNewID int
INSERT INTO [DIENSTLEISTUNG]
(DESCRIPTION, QUANTITY,
PRICE, AZ_MO, AZ_DI,AZ_MI,AZ_DO,AZ_FR,
AZ_SA,AZ_SO,DIENSTLEISTUNGSART_ID,
UPDATE_USER, UPDATE_DATE,
PERMISSIONS, KONTRAKTPOSITION,ITEMNUMBER,
PRIORITY, VALID)
SELECT i.DESCRIPTION, i.QUANTITY, i.PRICE, i.AZ_MO,
i.AZ_DI,i.AZ_MI,i.AZ_DO,i.AZ_FR,
i.AZ_SA,i.AZ_SO,i.SERVICETYPE_ID, i.UPDATE_USER,GETDATE(),
i.PERMISSIONS, i.KONTRAKTPOSITION,i.ITEMNUMBER, i.PRIORITY, 'Y'
FROM INSERTED i
JOIN deleted d ON i.ID=d.ID
WHERE i.PRICE<>d.PRICE
or i.DESCRIPTION<>d.DESCRIPTION
IF ( UPDATE (PRICE) OR UPDATE (DESCRIPTION) )
UPDATE S
SET s.VALID = 'N'
FROM SERVICE s
JOIN INSERTED i ON I.ID = S.ID
IF UPDATE(PRIORITY)
UPDATE s
SET s.PRIORITY= i.PRIORITY
FROM SERVICE s
JOIN INSERTED i ON i.ID = s.ID
SET NOCOUNT OFF;
END
The first Trigger copies an entire row with a new ID if a change in the original row happens, also the trigger set a flag. The old row gets the flag VALID = 'N' and the new row gets the flag VALID = 'Y'. The trigger only creates a new row if PRICE or DESCRIPTION are updated. So far so good.
My problem is that if I want to update the PRIORITY in the new row the trigger fires again and sets the flag to VALID = 'N'. That should not happen. I want only to update the priority without creating a new row or update a another column.
Thanks for help
You cannot prevent a trigger from firing - if it's present and not disabled, it will fire. That's how triggers work.
What you can do is check inside your trigger which columns have been updated. So you could do something like this in your one single trigger:
CREATE TRIGGER [dbo].[DIENSTLEISTUNG_Update]
ON [dbo].[DIENSTLEISTUNG]
FOR UPDATE
AS
IF UPDATE(PRICE)
... (do what you need to do if PRICE is updated)...
IF UPDATE(DESCRIPTION)
... (do what you need to do if DESCRIPTION is updated)...
IF UPDATE(PRIORITY)
... (do what you need to do if PRIORITY is updated)...
Use the UPDATE() function to check whether a given column has been updated - and if so, act on it. See the MSDN docs on how to use the UPDATE() function.
You can make triggers fire only on certain columns or one colomn.
like this.
CREATE TRIGGER tr_something ON myTable
FOR INSERT, UPDATE
AS
IF UPDATE(myColumn)
BEGIN
-- do what you want
END
post below gives more details, seems I'm to slow :)
What you can do is set the context info of the session which you are in like this:
SET Context_Info 0x55555
And then in your trigger check for the context info to decide what to do:
DECLARE #Cinfo VARBINARY(128)
SELECT #Cinfo = Context_Info()
IF #Cinfo = 0x55555
RETURN