trigger updates entire table even on single-row update - sql-server

The following triggers are meant to automate updates when a row is updated in a locations table. Changes can occur one row at a time, or 1-n many rows at a time. However, when updating a single row, while the "locations_geteditdate" is enabled, a new time stamp is written to all 28K rows in the locations table. I know I'm missing something obvious here, thanks for the help.
ALTER TRIGGER [dbo].[locations_geteditdate]
ON [dbo].[TBL_LOCATIONS]
instead of update
AS
begin
declare #recs INT
select #recs = COUNT(*)
from dbo.TBL_LOCATIONS a
join inserted i on i.Location_ID = a.Location_ID
if #recs > 0
update dbo.TBL_LOCATIONS
SET EditDate = GETDATE()
end
GO
alter TRIGGER [dbo].[locations_move_topo]
ON [dbo].[TBL_LOCATIONS]
for update
AS
BEGIN
update dbo.TBL_LOCATIONS
set topo_name = dbo.TLU_TOPO_BOUNDS.name
FROM dbo.TBL_LOCATIONS
inner join dbo.TLU_TOPO_BOUNDS
on dbo.TBL_LOCATIONS.Location_ID = dbo.TBL_LOCATIONS.Location_ID
where (TLU_TOPO_BOUNDS.Shape.STContains(TBL_LOCATIONS.SHAPE) = 1) ;
END
Accepted answer:
alter TRIGGER [dbo].[locations_geteditdate]
ON [dbo].[TBL_LOCATIONS]
for update
as
begin
update dbo.TBL_LOCATIONS
SET EditDate = GETDATE()
from dbo.TBL_LOCATIONS locn
inner join inserted i on i.location_id = locn.Location_ID
end
GO

In your if condition (in locations_geteditdate) you have no where clause; therefore it is including all records:
if #recs > 0
update dbo.TBL_LOCATIONS
SET EditDate = GETDATE()
WHERE ???
end
You correctly used the inserted table to see what had been updated but only to identify a record count
So reading the code you've put in the trigger, it looks like what you're trying to do is just apply a timestamp to the table to show that when it has been updated.
You have at least these options for this:
1. If you don't actually need a recognisable datetime in there you can use a timestamp field instead of a datetime and get it automatically updated.
2. If you can control where updates are performed to the table you can just set EditDate there (i.e. in stored procedures)
However, assuming that you want a recognisable datetime and you can't control where updates to the table are happening which is why you're implementing a trigger rather than just have a proc set EditDate, you need to go forward with one of the two types of trigger:
A) So if you persist with an "instead of" trigger you need to understand that it replaces the update that would have happened. So its incumbent upon you to then do the work that it was going to. You check column by column what has changed:
e.g.
IF UPDATE (price)
BEGIN
UPDATE t
SET price = i.price
FROM TBL_LOCATIONS t join inserted i
ON i.locn_id = t.locn_id
END
... repeat for each column (you can merge the updates if it makes sense)
B) Alternatively you can change to an "after" trigger, allow the update to happen (so you don't have to code column by column to check what's been updated) BUT YOU MUST then have a check on the EditDate column and NOT perform an update if its the EditDate column that has changed. If you don't do this you'll be in an infinite loop - your proc calls the trigger which calls the trigger etc
i.e. something like:
IF NOT UPDATE(EditDate)
BEGIN
UPDATE dbo.TBL_LOCATIONS
SET EditDate = GETDATE()
FROM dbo.TBL_LOCATIONS locn
INNER JOIN inserted i on i.locn_id = locn.locn_id
END

Related

SQL Server trigger to update row and save old version

I setup the below trigger to save the old version of a modified row into an "Archive" table. It works fine as long as the commented part stays commented. Once I uncomment the UPDATE part, the trigger saves the row as it is AFTER the update, instead of BEFORE the update. What should I change ?
ALTER trigger [dbo].[TRG_AFTER_UPDATE_Test]
ON [dbo].[Test]
AFTER UPDATE
AS
BEGIN
INSERT INTO dbo.TestArchive(
[CustId]
,[CustName]
,[SomeValue]
,[Modified]
,[ModifiedBy]
)
SELECT s.[CustId]
,s.[CustName]
,s.[SomeValue]
,s.[Modified]
,s.[ModifiedBy]
FROM dbo.Test s
INNER JOIN deleted d ON s.CustId = d.CustId
--here is the part that makes the INSERT behave badly
/*
UPDATE dbo.Test
SET ModifiedBy = USER_NAME(),
Modified = GETDATE()
FROM dbo.Test s
INNER JOIN Inserted i ON s.CustId = i.CustId
*/
END
It looks like you have an infinite loop going on here with the update...
Your trigger is set to the update event of dbo.Test, and within that trigger you are updating the dbo.Test table on which the trigger is set, which in turn fires the update trigger ad infinitum....
I am not sure if it would work by disabling the trigger within itself before performing the update and then re-enabling it afterwards.
https://msdn.microsoft.com/en-us/library/ms189748.aspx
Try inserting the data from the deleted table and not the actual table. Change the columns in your select from s.[column name] to d.[column name].

How do I add a "last updated" column (SQL Server)?

I have a database called a3lf. On my table players, I'd like to add a column called LastUpdated. When some part of the row is modified, I'd like that the table LastUpdated updates with the time and date info, so I can do some cleaning in a future time.
Is it possible? I was searching for that and I found some code that stills confusing to my knowledge:
ALTER TRIGGER dbo.SetLastUpdatedBusiness
ON dbo.Businesses
AFTER UPDATE -- not insert!
AS
BEGIN
IF NOT UPDATE(LastUpdated)
BEGIN
UPDATE t
SET t.LastUpdated = CURRENT_TIMESTAMP -- not dbo.LastUpdated!
FROM dbo.Businesses AS t -- not b!
INNER JOIN inserted AS i
ON t.ID = i.ID;
END
END
GO
Thank you!
This will be the code for your table.
CREATE TRIGGER dbo.trg_players_Update_LastUpdatedColumn
ON <Your_Schema_name>.players
AFTER UPDATE
AS
UPDATE <Your_Schema_name>.players
SET LastUpdated = GETDATE()
WHERE <ID> IN (SELECT DISTINCT <ID> FROM Inserted).
Generally <ID> is the column, based on which you have updated values (Column used in where filter of an update statement).
Hope this helps..!!

Finding out which rows are inserted , updated or deleted using triggers

I have a table named indication in Database and it has three columns Name, Age, and Enable.
I want to create a trigger that inserts an alarm into my alarm table whenever the Age is under 18 and Enable is true, I want to check the record on the indication table at the exact moment that it has been inserted, that way I can check whether it should be inserted in alarm or not.
I found COLUMNS_UPDATED (Transact-SQL) on MSDN and it works for updated columns, is there the same thing for ROWS_UPDATED?
You can always set your trigger to respond to only an INSERT action, with
CREATE TRIGGER TR_Whatever_I ON dbo.YourTable FOR INSERT
AS
... (body of trigger)
Be aware FOR INSERT is the same as AFTER INSERT. You also have the option of INSTEAD OF, but with that you have to perform the data modification yourself. There is no BEFORE trigger in SQL Server.
In some cases it is very convenient to handle more than one action at once because the script for the different actions is similar--why write three triggers when you can write just one? So in the case where your trigger looks more like this:
CREATE TRIGGER TR_Whatever_IUD ON dbo.YourTable FOR INSERT, UPDATE, DELETE
AS
... (body of trigger)
Then you don't automatically know it was an insert in the body. In this case, you can detect whether it's an insert similar to this:
IF EXISTS (SELECT * FROM Inserted)
AND NOT EXISTS (SELECT * FROM Deleted) BEGIN
--It's an INSERT.
END
Or, if you want to determine which of the three DML operations it is:
DECLARE #DataOperation char(1);
SET #DataOperation =
CASE
WHEN NOT EXISTS (SELECT * FROM Inserted) THEN 'D'
WHEN NOT EXISTS (SELECT * FROM Deleted) THEN 'I'
ELSE 'U'
END
;
Triggers still run if a DML operation affects no rows (for example, UPDATE dbo.YourTable SET Column = '' WHERE 1 = 0). In this case, you can't tell whether it was an update, delete, or insert--but since no modification occurred, it doesn't matter.
A Special Note
It's worth mentioning that in SQL Server, triggers fire once per operation, NOT once per row. This means that the Inserted and Deleted meta-tables will have as many rows in them during trigger execution as there are rows affected by the operation. Be careful and don't write triggers that assume there will only be one row.
Firstly I think you have to increase your knowledge on the way triggers work, and what the different type of triggers are.
You can create a trigger like this
CREATE TRIGGER trg_Indication_Ins
ON Indication
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
Insert Alarms (column1, column2) Select value1, value2 from inserted where Age < 18 and Enable = 1
END
This should basically do what you are looking for, and from what I understand from your quesion.
UPDATE:
Basically triggers can fire on INSERT, UPDATE or DELETE or any combination of the three, You can also set it to fire 'FOR/AFTER' the event (of which both actually means AFTER), or INSTEAD OF the event. A trigger will always have "internal" or meta-tables on the event.
These tables are inserted and deleted
The inserted table is basically all the new records that is applied to the table, and the deleted table have all the records that will be removed. In the case of the UPDATE event, the inserted table will have all the new values and deleted will have all the old values.
The inserted table will be empty on a DELETE trigger, and the deleted table will be empty on an INSERT trigger
Triggers can affect performance drastically if not used properly, so use it wisely.

How to prevent updates to a table, with an exception for one situation

I have a table that contains records that can become part of a bill. I can tell which ones are already part of a bill because the table has a BillId column that gets updated by the application code when that happens. I want to prevent updates to any record that has a non-null BillId. I'm thinking that the following should take care of that:
CREATE TRIGGER [Item_Update_AnyBilled]
ON [dbo].[Item]
FOR UPDATE
AS
BEGIN
SET NOCOUNT ON;
DECLARE #AnyBilled BIT;
SELECT TOP(1) #AnyBilled = 1
FROM inserted i
JOIN deleted d ON i.ItemId = d.ItemId
WHERE d.BillId IS NOT NULL;
IF COALESCE(#AnyBilled, 0) = 1 BEGIN
RAISERROR(2870486, 16, 1); -- Cannot update a record that is part of a bill.
ROLLBACK TRANSACTION;
END;
END;
However, there is one more wrinkle. The Item table also has a DATETIME Modified column, and a trigger that updates it.
CREATE TRIGGER [dbo].Item_Update_Modified
ON [dbo].[Item]
AFTER UPDATE
AS
BEGIN
SET NOCOUNT ON;
UPDATE a
SET Modified = getdate()
FROM Item a JOIN inserted i ON i.ItemId = a.ItemId
END
With these triggers in place, adding an Item to a Bill always causes the RAISERROR to fire. Presumably because when the BillId is populated, Item_Update_AnyBilled lets it through because the deleted.BillId is NULL, but the Item_Update_Modified then gets executed, and that secondary change causes Item_Update_AnyBilled to get executed again, and this time deleted.BillId is no longer NULL.
How can I prevent updates to the Item table except in the case where the BillId is being populated or when the only change is to the Modified column?
I'd prefer a solution that didn't require me to compare the inserted and deleted values of every column (or use COLUMNS_UPDATED()) as that would create a maintenance issue (someone would have to remember to update the trigger any time a new column is added to or deleted from the table). I am using SQL Server 2005.
Why not use an INSTEAD OF trigger? It requires a bit more work (namely a repeated UPDATE statement) but any time you can prevent work, instead of letting it happen and then rolling it back, you're going to be better off.
CREATE TRIGGER [dbo].[Item_BeforeUpdate_AnyBilled]
ON [dbo].[Item]
INSTEAD OF UPDATE
AS
BEGIN
SET NOCOUNT ON;
IF EXISTS
(
SELECT 1 FROM inserted i
JOIN deleted AS d ON i.ItemId = d.ItemId
WHERE d.BillId IS NULL -- it was NULL before, may not be NULL now
)
BEGIN
UPDATE src
SET col1 = i.col1 --, ... other columns
ModifiedDate = CURRENT_TIMESTAMP -- this eliminates need for other trigger
FROM dbo.Item AS src
INNER JOIN inserted AS i
ON i.ItemId = src.ItemId
AND (criteria to determine if at least one column has changed);
END
ELSE
BEGIN
RAISERROR(...);
END
END
GO
This doesn't fit perfectly. The criteria I've left out is left out for a reason: it can be complex to determine if a column value has changed, as it depends on the datatype, whether the column can be NULL, etc. AFAIK the built-in trigger functions can only tell if a certain column was specified, not whether the value actually changed from before.
EDIT considering that you're only concerned about the other columns that are updated due to the after trigger, I think the following INSTEAD OF trigger can replace both of your existing triggers and also deal with multiple rows updated at once (some without meeting your criteria):
CREATE TRIGGER [dbo].[Item_BeforeUpdate_AnyBilled]
ON [dbo].[Item]
INSTEAD OF UPDATE
AS
BEGIN
SET NOCOUNT ON;
UPDATE src SET col1 = i.col1 --, ... other columns,
ModifiedDate = CURRENT_TIMESTAMP
FROM dbo.Item AS src
INNER JOIN inserted AS i
ON src.ItemID = i.ItemID
INNER JOIN deleted AS d
ON i.ItemID = d.ItemID
WHERE d.BillID IS NULL;
IF ##ROWCOUNT = 0
BEGIN
RAISERROR(...);
END
END
GO

Trigger that updates just the inserted row

I'm trying to create a simple trigger using TSQL (or SQL Server 2008). The problem is: my current trigger is updating the entire table. This was fine for a while, but now the table has more than 20k rows. So I want a trigger that only updates the rows that are being inserted.
Here's my current simple trigger:
CREATE TRIGGER trig_MyPplUpdate
ON [Persons]
FOR INSERT
AS
Begin
Update Persons
set MyFile = NULL
where Len(MyFile) < 60
End
I think I'll have to use either the "inserted" table or the row_number function ordered by the primary key. Any ideas?
If it is necessary to use a trigger here at all I would use an INSTEAD OF trigger to adjust the values pre-insert and avoid the need to JOIN back onto the base table and Update them afterwards.
CREATE TRIGGER trig_MyPplUpdate
ON [Persons]
INSTEAD OF INSERT
AS
BEGIN
INSERT INTO Persons
SELECT foo,
bar,
CASE
WHEN Len(MyFile) >= 60 THEN MyFile
END
FROM Inserted
END
You need to join the Inserted pseudo table in your UPDATE statement. Always be aware that SQL Server fires the trigger once per statement and that statement can very well modify/insert multiple rows at once - so your Inserted table quite likely will contain more than one row - just need to keep that in mind when you write your trigger.
Try something like this:
CREATE TRIGGER trig_MyPplUpdate
ON [Persons]
FOR INSERT
AS
UPDATE dbo.Persons
SET MyFile = NULL
WHERE Len(MyFile) < 60
AND PersonID IN (SELECT DISTINCT PersonID FROM Inserted)
or use whatever unique column (your primary key) you have to get exactly those rows that have been inserted.

Resources