All of my tables have a Trigger for CRUD operations.
here is a sample:
ALTER TRIGGER [dbo].[Cities_tr] ON [dbo].[Cities] AFTER INSERT, UPDATE
AS
BEGIN
DECLARE #operation CHAR(6)
SET #operation = CASE WHEN EXISTS (SELECT * FROM inserted) AND EXISTS (SELECT * FROM deleted)
THEN 'Update'
WHEN EXISTS (SELECT * FROM inserted)
THEN 'Insert'
WHEN EXISTS(SELECT * FROM deleted)
THEN 'Delete'
ELSE NULL
END
IF #operation = 'Insert'
INSERT INTO history ([dt],[tname],[cuser] ,[id],op)
SELECT GETDATE(),'Cities', i.ldu, i.CityId,#operation
FROM inserted i
set nocount on
IF #operation = 'Update'
INSERT INTO history ([dt],[tname],[cuser] ,[id],op)
SELECT GETDATE(),'Cities', i.ldu, i.CityId,#operation
FROM deleted d, inserted i
END
If I update one row, everything works fine and trigger inserts one row in history.
For example
update top(1) cities set f=1
But if more than one row updated, updatedrow^2 rows will be inserted.
For example 9 for 3 rows 100 for 10 rows...
What is wrong with my trigger and how could I solve it?
The problem with your code is that you are cross joining inserted and deleted. On a multi-rows update, both contain many rows, which the cartesian product multiplies.
It looks like you actually want to log the "new" rows (either inserted or updated). If so, you don’t want to select from deleted. Also, the conditional logic can be moved within a single query, which allows to simplify your code as follows:
ALTER TRIGGER dbo.Cities_tr
ON dbo.Cities
AFTER INSERT, UPDATE
AS
BEGIN
INSERT INTO history (dt, tname, cuser, id, op)
SELECT
getdate(),
'Cities',
ldu,
cityId,
case when exists (select 1 from deleted) then 'Update' else 'Insert' end
FROM inserted;
END
On the other hand, if you want to log both the "old" and "new" rows (which is not what your code does, even on a single-row update), then you want to union all two queries that select from inserted and deleted.
You are cross joining inserted and deleted. Normally, they would be joined using the table's primary key, which is presumably CityId:
INSERT INTO history ([dt], [tname], [cuser] , [id], op)
SELECT GETDATE(), 'Cities', i.ldu, i.CityId, #operation
FROM deleted d JOIN
inserted i
ON d.CityId = i.CityId;
In this case, deleted is not being used, so it does not even need to be included in the query.
You could implement the entire trigger as a single query in the table using LEFT JOIN:
INSERT INTO history ([dt], [tname], [cuser] , [id], op)
SELECT GETDATE(), 'Cities', i.ldu, i.CityId,
(CASE WHEN d.CityId IS NOT NULL THEN 'Update' ELSE 'Insert' END)
FROM inserted i LEFT JOIN
deleted d
ON d.CityId = i.CityId;
Related
I'm trying to update a field called LastUpdate with a timestamp whenever either one of 2 other fields are changed to a value that's different from the original value. Those two columns are LoadDate and CompleteDate. The trigger I have works, but since I'm updating the row each time I run my process, regardless of whether the date changes, its updating the timestamp. I need to compare to see if the old LoadDate is different from the new LoadDate, or if the old CompleteDate is different from the new CompleteDate. Here's my trigger:
CREATE TRIGGER time_stamp
ON my_table
FOR UPDATE, INSERT
AS
BEGIN
SET NOCOUNT ON;
DECLARE #LoadDate datetime, #CompleteDate datetime
SELECT #LoadDate = LoadDate, #CompleteDate = CompleteDate FROM my_table
UPDATE my_table
SET LastUpdate = GETDATE()
WHERE LoadDate = #LoadDate
AND CompleteDate = #CompleteDate
END
You should just need one statement in your trigger. Something like this ought to do:
update MT
set LastUpdate = GetDate()
from My_Table as MT inner join
-- Inner join with inserted to pick up only those rows inserted or updated.
inserted as i on i.YourIdColumn = MT.YourIdColumn left outer join
-- Left outer join with deleted to pick up the "before" values if this is an UPDATE.
deleted as d on d.YourIdColumn = MT.YourIdColumn
where
-- LoadDate has changed or this is an INSERT.
( i.LoadDate <> d.LoadDate or d.LoadDate is NULL ) or
-- CompleteDate has changed or this is an INSERT.
( i.CompleteDate <> d.CompleteDate or d.CompleteDate is NULL )
Note that this is performing a set operation. An INSERT or UPDATE that affects multiple rows will cause the trigger to fire once and process all of the affected rows.
Case
I have two tables with following structures (I removed 34 other columns that were not related to question)
Table1
------
Id
LastAccessed
Data1
Data2
Table1_History
------
_Id
Action
Id
LastAccessed
Data1
Data2
Every time a user reads the record (with specific procedure) the LastAccessed timestamp will change.
I attached a trigger to Table1 which copies the record to history table if Data1 or Data2 (or in my case any other column changes); However I don't want a copy or record in case LastAccessed changes.
Here is my trigger
CREATE TRIGGER [TRIGGER!]
ON TABLE1
AFTER UPDATE, DELETE
AS
BEGIN
SET NOCOUNT ON;
IF NOT UPDATE([LastAccessed]) BEGIN
IF EXISTS(SELECT * FROM inserted) BEGIN
INSERT INTO [Table1_Hist](
Action, Id, LastAccessed, Data1, Data2
) SELECT
'EDIT', Id, LastAccessed, Data1, Data2
FROM deleted
END ELSE BEGIN
INSERT INTO [Table1_Hist](
Action, Id, LastAccessed, Data1, Data2
) SELECT
'DELETE', Id, LastAccessed, Data1, Data2
FROM deleted
END
END
END
This trigger will not copy row if LastAccessed and Data1 both change (which I want to). to solve this issue I can change the IF statement to
IF UPDATE(Id) OR UPDATE(Data1) OR UPDATE(Data2) ... BEGIN
In this case it will work as intended
Question:
As I have 34 columns in my table it is not easy to specify every column in IF statement.
Is there any easier way of doing this?
Hopefully someone has a more elegant answer than this, but worst case you can generate that ugly IF statement for all 34 columns with a simple query:
SELECT CASE WHEN column_id = 1 THEN 'IF ' ELSE 'OR ' END + 'UPDATE(' + name + ')'
FROM sys.columns
WHERE object_id = OBJECT_ID(N'[dbo].[Table1]')
AND name <> 'LastUpdated'
ORDER BY column_id
Then you can just cut-and-paste the results for your IF statement... not an elegant solution, but it's quicker than typing it all by hand.
CREATE TRIGGER [TRIGGER!]
ON TABLE1
AFTER UPDATE
AS
BEGIN
INSERT INTO Table1_Hist (
Action
Id
LastAccessed
Data1
Data2
)
SELECT 'UPDATE',d.ID,d.LastAccessed,d.Data1,d.Data2, etc., etc.
FROM DELETED d
INNER JOIN INSERTED i ON d.id = i.id
WHERE
COALESCE(d.data1,'') <> COALESCE(i.data1,'') OR
COALESCE(d.data2,'') <> COALESCE(i.data2,'') OR
<repeat for every attribute EXCEPT LastAccessed>
END
This will also work for inserts or updates of multiple rows, whereas your method would potentially insert rows where the data did not actually change.
Also, you want to have separate UPDATE and DELETE triggers because your query should just dump the full row set from DELETED in case of an actual DELETE.
I need an SQL constraint (using SQLDeveloper) to check that for a specific account_id, only ONE or NO regular_id exists, such as the data attached, cell containing '6' being what should not be allowed, even though it is a different value.
AccountID RegularID OpenID
1 5 null
1 null 10
1 null 11
1 6 <-- Forbidden
Best way is with a trigger
Create trigger trig_NoSingleRegId
On MyTable For Insert, Update
As
if Exists(Select * From MyTable t
Where AccountId In (Select AcountId From inserted)
Group By AccountId
Having Count(Distinct regularId) > 1)
Begin
RollBack Transaction
Raiserror('Cannot have more than one RegularId per AccountId', 16, 1)
End
Note: The Where clause is for performance only, to limit trigger to only those accountIds inserted or updated by the triggering update or insert.
or you can also can use join to accomplish same restriction.
Create trigger trig_NoSingleRegId
On MyTable For Insert, Update
As
if Exists(Select * From MyTable t
join inserted I
on i.AccountId = t.AccountId
Group By t.AccountId
Having Count(Distinct t.regularId) > 1)
Begin
RollBack Transaction
Raiserror('Cannot have more than one RegularId per AccountId', 16, 1)
End
I want to create a trigger to detect whether a row has been changed in SQL Server. My current approach is to loop through each field, apply COLUMNS_UPDATED() to detect whether UPDATE has been called, then finally compare the values of this field for the same row (identified by PK) in inserted vs deleted.
I want to eliminate the looping from the procedure. Probably I can dump the content of inserted and deleted into one table, group on all columns, and pick up the rows with count=2. Those rows will count as unchanged.
The end goal is to create an audit trail:
1) Track user and timestamp
2) Track insert, delete and REAL changes
Any suggestion is appreciated.
Instead of looping you can use BINARY_CHECKSUM to compare entire rows between the inserted and deleted tables, and then act accordingly.
Example
Create table SomeTable(id int, value varchar(100))
Create table SomeAudit(id int, Oldvalue varchar(100), NewValue varchar(100))
Create trigger tr_SomTrigger on SomeTable for Update
as
begin
insert into SomeAudit
(Id, OldValue, NewValue)
select i.Id, d.Value, i.Value
from
(
Select Id, Value, Binary_CheckSum(*) Version from Inserted
) i
inner join
(
Select Id, Value, Binary_CheckSum(*) Version from Deleted
) d
on i.Id = d.Id and i.Version <> d.Version
End
Insert into sometable values (1, 'this')
Update SomeTable set Value = 'That'
Select * from SomeAudit
I need to write a procedure that will allow me to select x amount of rows and at the same time update those rows so the calling application will know those records are locked and in use. I have a column in the table named "locked". The next time the procedure is called it will only pull the next x amount of records that do not have the "locked" column checked. I have read a little about the OUTPUT method for SQL server, but not sure that is what I want to do.
As you suggested, you can use the OUTPUT clause effectively:
Live demo: https://data.stackexchange.com/stackoverflow/query/8058/so3319842
UPDATE #tbl
SET locked = 1
OUTPUT INSERTED.*
WHERE id IN (
SELECT TOP 1 id
FROM #tbl
WHERE locked = 0
ORDER BY id
)
Also see this article:
http://www.sqlmag.com/article/tsql3/more-top-troubles-using-top-with-insert-update-and-delete.aspx
Vote for Cade Roux's answer, using OUTPUT:
UPDATE #tbl
SET locked = 1
OUTPUT INSERTED.*
WHERE id IN (SELECT TOP 1 id
FROM #tbl
WHERE locked = 0
ORDER BY id)
Previously:
This is one of the few times I can think of using a temp table:
ALTER PROCEDURE temp_table_test
AS
BEGIN
SELECT TOP 5000 *
INTO #temp_test
FROM your_table
WHERE locked != 1
ORDER BY ?
UPDATE your_table
SET locked = 1
WHERE id IN (SELECT id FROM #temp_test)
SELECT *
FROM #temp_test
IF EXISTS (SELECT NULL
FROM tempdb.dbo.sysobjects
WHERE ID = OBJECT_ID(N'tempdb..#temp_test'))
BEGIN
DROP TABLE #temp_test
END
END
This:
Fetches the rows you want, stuffs them into a local temp table
Uses the temp table to update the rows to be "locked"
SELECTs from the temp table to give you your resultset output
Drops the temp table because they live for the session