Is there any way to combine an update and an insert statements in a way that they fires a trigger only once?
I have one particular table that has (and currently needs) a trigger AFTER INSERT, UPDATE, DELETE. Now I want to update one row and insert another row and have the trigger fire only once for that.
Is this at all possible?
I already tried a MERGE-Statement without success: The trigger fires once for the update- and once for the insert-part.
Well, problem solved for me. I did NOT find a way to combine the statements into one fire-event of the trigger. But the trigger behaves in an interesting way, that was good enough for me: Both calls to the trigger do already have access to the fully updated data.
Just execute the following statements and you will see what I mean.
CREATE TABLE Foo (V INT)
GO
CREATE TRIGGER tFoo ON Foo AFTER INSERT, UPDATE, DELETE
AS
SELECT 'inserted' AS Type, * FROM inserted
UNION ALL
SELECT 'deleted', * FROM deleted
UNION ALL
SELECT 'actual', * FROM Foo
GO
DELETE FROM Foo
INSERT Foo VALUES (1)
;MERGE INTO Foo
USING (SELECT 2 AS V) AS Source ON 1 = 0
WHEN NOT MATCHED BY SOURCE THEN DELETE
WHEN NOT MATCHED BY TARGET THEN INSERT (V) VALUES (Source.V);
As a result, the trigger will be called twice for the MERGE. But both times, "SELECT * FROM Foo" delivers the fully updated data already: There will be one row with the value 2. The value 1 is deleted already.
This really surprised me: The insert-trigger is called first and the deleted row is gone from the data before the call to the delete-trigger happens.
Only the values of "inserted" and "deleted" correspond to the delete- or insert-statement.
You could try something like this:
The trigger would check for the existence of #temp table.
If it doesn't exist, it creates it with dummy data. It then checks if the recent values contain the same user (SPID) that is running now and if the last time it was triggered was within 20 seconds.
If these are true then it will PRINT 'Do Nothing' and drop the table, otherwise it will do your trigger statement.
At the end of your trigger statement it inserts into the table the SPID and current datetime.
This temp table should last as long as the SPID connection, if you want it to last longer make it a ##temp or a real table.
IF OBJECT_ID('tempdb..#temp') IS NULL
begin
Create table #temp(SPID int, dt datetime)
insert into #temp values (0, '2000-01-01')
end
If ##SPID = (select top 1 SPID from #temp order by dt desc)
and Convert(datetime,Convert(varchar(19),GETDATE(),121)) between
Convert(datetime,Convert(varchar(19),(Select top 1 dt from #temp order by dt desc),121)) and
Convert(datetime,Convert(varchar(19),DateAdd(second, 20, (select top 1 dt from #temp order by dt desc)),121))
begin
PRINT 'Do Nothing'
Drop table #temp
end
else
begin
--trigger statement
Insert into #temp values (##SPID, GETDATE())
end
Related
I am working in SQL Server Management Studio v18, and I have the following trigger:
CREATE TRIGGER [dbo].[update_surface]
ON [dbo].[my_table]
FOR INSERT
AS
BEGIN
SET NOCOUNT ON;
DECLARE #surface_m2 REAL
SET #surface_m2 = (SELECT cast(round(CAST(Dimension1*Dimension2 as decimal)/ cast(1000000 as decimal),3,3) as decimal(10,3)) AS surface FROM my_table WHERE Surface_m2 IS NULL)
UPDATE dbo.my_table SET Surface_m2 = #surface_m2
END
I have two columns in my_table, which are Dimesion1 and Dimension2. I want that the trigger multiplies them, and set the result to other column in the same table, which is Surface_m2, whenever this column is null. The trigger does his function, but based on the type of insert I do:
If I insert a row in my_table by the graphic environment the trigger works as I wish. With each new row, Surface_m2 has his own result.
But if I insert by INSERT INTO my_table VALUES ().... (query) the trigger updates Surface_m2 column of all previous rows with the result of each new insert.
Why is the trigger working like that? Is there any other simple way to do what I am trying to do?
Thanks.
Insert trigger gives you actual values that are inserted in a special table called... "inserted".
So what you need to do is join this table against your main table and perform the logic needed, no variables required.
Something like this untested code
create trigger...
begin
UPDATE t
SET Surface_m2 = cast(round(CAST(t.Dimension1*t.Dimension2 as decimal)
/ cast(1000000 as decimal),3,3) as decimal(10,3))
from dbo.my_table t
inner join inserted i
ON i.ID = t.ID
WHERE t.Surface_m2 IS NULL
end
This begs the question though, why can't you just insert the Surface_m2 value yourself. Or even better, change Surface_m2 to be a computed column if it's always depends on Dimension1 and Dimension2
I am trying to accomplish the following 3 simple tasks as a transaction (i.e. I need to lock old_table and new_table until the process completes).
Create a new table (new_table)
Add a trigger to old_table, which queues updates to new_table.
Select all the data from old_table and return it.
Note that I want these handled in a single transaction. I cannot allow inserts into old_table (and therefore triggered inserts into new_table) in between the trigger creation and the select on old_table.
My current closest attempt is this, but truthfully I feel that I am very far off from accomplishing my goal with this code. I have added the code just for reference of what I am trying, but I am mostly interested in non-specific answers that layout how to accomplish the above three comands in a transaction.
DROP PROCEDURE IF EXISTS dbo.BuildAll;
CREATE PROCEDURE dbo.BuildAll
AS
BEGIN
BEGIN TRANSACTION
DECLARE #TriggerCode VARCHAR(MAX)
CREATE TABLE dbo.new_table
(
status nvarchar(5),
type char(1),
col1 nvarchar(50),
col2 smallint
)
SELECT #TriggerCode = 'CREATE TRIGGER myTrigger
ON dbo.old_table FOR INSERT
AS
DECLARE #col1_new nvarchar(50)
DECLARE #col2_new smallint
SELECT #col1_new = col1 FROM inserted
SELECT #col2_new = col2 FROM inserted
IF #col1_new IS NOT NULL
BEGIN
INSERT INTO new_table (status, type, col1, col2)
SELECt "Q", "A", #col1, #col2 FROM inserted
END'
EXEC(#TriggerCode)
SELECT * FROM old_table
COMMIT
END
Going to suggest this an a possible solution you can try. This doesn't address the correctness of your actual trigger, you have two separate questions here really.
You don't need to encapsulate this entire process in a transaction.
Create your new table.
Create your trigger on old table, but disabled.
set transaction isolation level serializable
begin tran
go
create trigger <Name> on <Table> etc
go
disable trigger <Name> on <Table>
go
commit
Now in a transaction you can lock the old table against other activity while you work
begin tran
update oldtable with(tablockx) set column=column where id=0 /* block other processes from updating table, id=0 row doesn't exist */
query your data and process as required
enable trigger <Name> on <Table>
commit
This trigger code of yours is kinda odd .... you have a trigger on all three operations - yet it appears as if you're never using the values you fetch from the deleted pseudo table, and if the value from the inserted table is NULL, you're not doing anything inside your trigger - so you can really spare yourself the DELETE case - that'll never do anything....
Also, as mentioned in my comment - you Inserted pseudo table can easily contain multiple rows - but you're selecting from it as if you only ever expect it to contain a single row.
You should really rewrite your trigger code to handle the case of multiple rows in Inserted and make the whole thing properly set-based - something like this:
CREATE TRIGGER myTrigger
ON dbo.old_table
FOR INSERT, UPDATE
AS
INSERT INTO new_table (status, type, col1, col2)
SELECT 'Q', 'A', i.col1, i.col2
FROM Inserted i
Whether you need this on the UPDATE case at all - I cannot tell, you need to decide this. But basically: just select from the Inserted table, take the Col1 and Col2 values, and add the constant values 'Q' and 'A' to your insert to handle multiple rows properly. That should do it.
I am new to sql triggers and learning as developing triggers for asp.net application.i am having case where I need to first save table and then edit the same table - this edit create new row in different table but the the problem is every time I edit the table it create new row in different table I want to create row in different table for only first edit.
Dividing my problem for readability.
I have two tables:
Table A and table B
I have written trigger on table A that add row in table B.
Problem:
Every time I edit row in table A a new row get added to table B. (So every edit create new row)
Required result:
I want my trigger to add ONLY one row in table B for the first edit in table A but not for subsequent edits.
I am using update triggers.
Any example with code would be great
Thanks you much in advance .
Create TRIGGER [dbo].[triggerName] ON [dbo].[databaseName]
For Update
As
Begin
DECLARE #i int
DECLARE #d int
DECLARE #action char(6)
DECLARE #Car VARCHAR(20)
IF (##ROWCOUNT = 0) RETURN
SELECT #i = Count(*) From Inserted
SELECT #d = Count(*) From Deleted
SELECT #action = CASE
WHEN (#i <> 0) and (#d <> 0) THEN 'UPDATE'
WHEN (#i = 0) and (#d <> 0) THEN 'DELETE'
WHEN (#i <> 0) and (#d = 0) THEN 'INSERT'
End
SELECT #Car = A From inserted
IF #action = 'UPDATE' AND #Car in ('BMW')
Begin
INSERT INTO Tableb (c,d,f)
Select c,d,f from inserted
End
Your trigger has some flaws in it.
First, You don't need to test if it was fired because of update, insert or delete. The trigger is specified for update, so inserts and deletes will not fire it anyway.
Second, SELECT #Car = A From inserted will raise an error whenever you update more then one row in the table.
Third, As you said, this will insert a record in tableB for every update, while you want it to insert a record only for the first update done (I assume one for the first update on any row, so if you update row 1 then insert, update row 2 then another insert, and update row 1 again don't insert).
I would write it like this:
Create TRIGGER [dbo].[triggerName] ON [dbo].[tableName]
For Update
As
Begin
INSERT INTO Tableb (c,d,f)
Select c,d,f
from inserted i
left join Tableb t ON(i.c = t.c and i.d = t.d and i.f = t.f)
where t.id is null -- or some other non-nullable column
and i.a = 'BMW'
End
You can modify your SQL trigger to execute only after INSERT
CREATE TRIGGER dbo.myTable_Insert
ON dbo.myTable
AFTER INSERT
AS
It is possible to create SQL trigger to run after insert, update or delete as seen in the referred tutorial
I have a trigger on a table that is something like this:
ALTER TRIGGER [shoot_sms]
ON [dbo].[MyTable]
AFTER INSERT
AS
begin
declare #number bigint
declare #body varchar(50)
declare #flag int
select #number=number,#body=body,#flag=flag from inserted
if(#flag=0)
begin
insert into temptable (number,body,status)
select #number,#body,'P'
end
end
Now I am making two entries in mytable as below:
insert into mytable(number, body, flag)
values(3018440225, 'This is test', 0)
insert into mytable(number, body, flag)
values(3018440225, 'This is test', 0)
I execute these queries at a time, but for both of the queries the trigger fires only once and performs the task for the first query only.
How can I make it work for both insert statements?
Just an idea but put a GO statement between those two insert statements and that might cause the trigger to fire twice.
You should probably rewrite your trigger to handle multiple row inserts I think.
Here is your query converted. You should get two rows now.
ALTER TRIGGER [shoot_sms]
ON [dbo].[MyTable]
AFTER INSERT
AS
begin
insert into temptable (number,body,status)
select number,body,'P'
from inserted
where flag = 0
end
Also notice your trigger is much simpler now.
Since those two statements are in one SQL batch, the trigger will (by design) only fire once.
Triggers don't fire once per row - they fire once per statement! So if you have an INSERT or UPDATE statement that affects more than one row, your trigger will have more than one row in the Inserted (and possibly Deleted) pseudo tables.
The way you wrote this trigger is really not taking into account that Inserted could contain multiple rows - what row do you select from the Inserted table if you're inserting 20 rows at once?
select #number = number, #body = body, #flag = flag from inserted
You need to change your trigger to take that into account!
Here's what I'm trying to do:
1) Insert into a temp table some values from an original table
INSERT INTO temp_table SELECT id FROM original WHERE status='t'
2) Update the original table
UPDATE original SET valid='t' WHERE status='t'
3) Select based on a join between the two tables
SELECT * FROM original WHERE temp_table.id = original.id
Is there a way to combine steps 1 and 2?
You can combine the steps by doing the update in PL/SQL and using the RETURNING clause to get the updated ids into a PL/SQL table.
EDIT:
If you still need to do the final query, you can still use this method to insert into the temp_table; although depending on what that last query is for, there may be other ways of achieving what you want. To illustrate:
DECLARE
id_table_t IS TABLE OF original.id%TYPE INDEX BY PLS_INTEGER;
id_table id_table_t;
BEGIN
UPDATE original SET valid='t' WHERE status='t'
RETURNING id INTO id_table;
FORALL i IN 1..id_table.COUNT
INSERT INTO temp_table
VALUES (id_table(i));
END;
/
SELECT * FROM original WHERE temp_table.id = original.id;
No, DML statements can not be mixed.
There's a MERGE statement, but it's only for operations on a single table.
Maybe create a TRIGGER wich fires after inserting into a temp_table and updates the original
Create a cursor holding the values from insert and then loop through the cursor updating the table. No need to create temp table in the first place.
You can combine steps 1 and 2 using a MERGE statement and DML error logging. Select twice as many rows, update half of them, and force the other half to fail and then be inserted into an error log that you can use as your temporary table.
The solution below assumes that you have a primary key constraint on ID, but there are other ways you could force a failure.
Although I think this is pretty cool, I would recommend you not use it. It looks very weird, has some strange issues (the inserts into TEMP_TABLE are auto-committed), and is probably very slow.
--Create ORIGINAL table for testing.
--Primary key will be intentionally violated later.
create table original (id number, status varchar2(10), valid varchar2(10)
,primary key (id));
--Create TEMP_TABLE as error log. There will be some extra columns generated.
begin
dbms_errlog.create_error_log(dml_table_name => 'ORIGINAL'
,err_log_table_name => 'TEMP_TABLE');
end;
/
--Test data
insert into original values(1, 't', null);
insert into original values(2, 't', null);
insert into original values(3, 's', null);
commit;
--Update rows in ORIGINAL and also insert those updated rows to TEMP_TABLE.
merge into original original1
using
(
--Duplicate the rows. Only choose rows with the relevant status.
select id, status, valid, rownumber
from original
cross join
(select 1 rownumber from dual union all select 2 rownumber from dual)
where status = 't'
) original2
on (original1.id = original2.id and original2.rownumber = 1)
--Only math half the rows, those with rownumber = 1.
when matched then update set valid = 't'
--The other half will be inserted. Inserting ID causes a PK error and will
--insert the data into the error table, TEMP_TABLE.
when not matched then insert(original1.id, original1.status, original1.valid)
values(original2.id, original2.status, original2.valid)
log errors into temp_table reject limit 999999999;
--Expected: ORIGINAL rows 1 and 2 have VALID = 't'.
--TEMP_TABLE has the two original values for ID 1 and 2.
select * from original;
select * from temp_table;