I am new to databases and I have trouble understanding triggers.
Any help would be appreciated. Thank you in advance!
My task is to create a trigger that sets producerc# = null in the table movie, when cert# from table movieexec is deleted.
I have two solutions for this task.
create trigger t
on movieexec
instead of delete
as
begin
update movie
set producerc# = null
where producerc# in (select cert# from deleted);
delete from movieexec
where cert# in (select cert# from deleted);
end;
go
This solution is not mine, it is from a book.
I have some questions about it:
Is the table deleted created before the trigger is executed,
because I do not understand how we have table deleted even though we
have not deleted anything from the table(we use instead of delete, not after delete trigger)
Also, what does exactly begin do? I read that it is used to group some statements of code. Would it be correct if I skip begin?
In the book is written that here it would not be correct to define after delete trigger, because producerc# is a foreign key. Can you explain to me, is that true and why is that.
When I test my code, both of the solutions work fine.
This is my solution:
create trigger t
on movieexec
after delete
as
update movie
set producerc# = null
where producerc# not in (select cert# from movieexec);
Do these solutions do the same thing, are both of them correct?
Can you give me some example where any of them does not work as expected.
Related
I'm confused how a trigger in SQL Server knows what event fired the trigger and what to do.
For example I have a trigger that update on table A and it is performing AFTER INSERT, DELETE, UPDATE. Now my question is how I make the body to perform each task when it triggered, do I need to make separate triggers for each task or is it a way to specify what it needs to do for each task on a single body and a single trigger. And if it is okay can anybody give some explanation and example for it
Thanks
If you really must do a single trigger - this is the logic needed to keep the three operation - INSERT, UPDATE and DELETE - apart:
CREATE TRIGGER dbo.YourTriggerName
ON dbo.YourTable
AFTER DELETE, INSERT, UPDATE
AS
BEGIN
-- check if both the Inserted as well as the Deleted pseudo tables exist
IF EXISTS (SELECT * FROM Inserted) AND EXISTS (SELECT * FROM Deleted)
BEGIN
-- trigger operation : UPDATE
-- "Inserted" contains the new values for the rows after the update
-- "Deleted" contains the old values for the rows before the update
END
-- check if only the Inserted pseudo tables exists
IF EXISTS (SELECT * FROM Inserted) AND NOT EXISTS (SELECT * FROM Deleted)
BEGIN
-- trigger operation: INSERT
-- "Inserted" contains the values for the rows inserted
END
-- check if only the Deleted pseudo tables exists
IF NOT EXISTS (SELECT * FROM Inserted) AND EXISTS (SELECT * FROM Deleted)
BEGIN
-- trigger operation: DELETE
-- "Deleted" contains the values for the rows having been deleted
END
END;
BE AWARE: the trigger is called once per statement - not once per row - so the Inserted and Deleted pseudo tables will potentially contain multiple rows - handle them in a set-based manner, as tables - don't do anything like
SELECT #ID = i.ID FROM Inserted i
This will NOT WORK for multiple rows being inserted at once in a single SQL statement!
But as I said - this is a bit messy, makes for a really large trigger, and makes it hard to maintain this code. I'd personally would much rather have three separate, focused triggers - one for each of the operations you need to handle.
I know at least three ways to insert a record if it doesn't already exist in a table:
The first one is using if not exist:
IF NOT EXISTS(select 1 from table where <condition>)
INSERT...VALUES
The second one is using merge:
MERGE table AS target
USING (SELECT values) AS source
ON (condition)
WHEN NOT MATCHED THEN
INSERT ... VALUES ...
The third one is using insert...select:
INSERT INTO table (<values list>)
SELECT <values list>
WHERE NOT EXISTS(select 1 from table where <condition>)
But which one is the best?
The first option seems to be not thread-safe, as the record might be inserted between the select statement in the if and the insert statement that follows, if two or more users try to insert the same record.
As for the second option, merge seems to be an overkill for this, as the documentation states:
Performance Tip: The conditional behavior described for the MERGE statement works best when the two tables have a complex mixture of matching characteristics. For example, inserting a row if it does not exist, or updating the row if it does match. When simply updating one table based on the rows of another table, improved performance and scalability can be achieved with basic INSERT, UPDATE, and DELETE statements.
So I think the third option is the best for this scenario (only insert the record if it doesn't already exist, no need to update if it does), but I would like to know what SQL Server experts think.
Please note that after the insert, I'm not interested to know whether the record was already there or whether it's a brand new record, I just need it to be there so that I can carry on with the rest of the stored procedure.
When you need to guarantee the uniqueness of records on a condition that can not to be expressed by a UNIQUE or PRIMARY KEY constraint, you indeed need to make sure that the check for existence and insert are being done in one transaction. You can achieve this by either:
Using one SQL statement performing the check and the insert (your third option)
Using a transaction with the appropriate isolation level
There is a fourth way though that will help you better structure your code and also make it work in situations where you need to process a batch of records at once. You can create a TABLE variable or a temporary table, insert all of the records that need to be inserted in there and then write the INSERT, UPDATE and DELETE statements based on this variable.
Below is (pseudo)code demonstrating this approach:
-- Logic to create the data to be inserted if necessary
DECLARE #toInsert TABLE (idCol INT PRIMARY KEY,dataCol VARCHAR(MAX))
INSERT INTO #toInsert (idCol,dataCol) VALUES (1,'row 1'),(2,'row 2'),(3,'row 3')
-- Logic to insert the data
INSERT INTO realTable (idCol,dataCol)
SELECT TI.*
FROM #toInsert TI
WHERE NOT EXISTS (SELECT 1 FROM realTable RT WHERE RT.dataCol=TI.dataCol)
In many situations I use this approach as it makes the TSQL code easier to read, possible to refactor and apply unit tests to.
Following Vladimir Baranov's comment, reading Dan Guzman's blog posts about Conditional INSERT/UPDATE Race Condition and “UPSERT” Race Condition With MERGE, seems like all three options suffers from the same drawbacks in a multi-user environment.
Eliminating the merge option as an overkill, we are left with options 1 and 3.
Dan's proposed solution is to use an explicit transaction and add lock hints to the select to avoid race condition.
This way, option 1 becomes:
BEGIN TRANSACTION
IF NOT EXISTS(select 1 from table WITH (UPDLOCK, HOLDLOCK) where <condition>)
BEGIN
INSERT...VALUES
END
COMMIT TRANSACTION
and option 2 becomes:
BEGIN TRANSACTION
INSERT INTO table (<values list>)
SELECT <values list>
WHERE NOT EXISTS(select 1 from table WITH (UPDLOCK, HOLDLOCK)where <condition>)
COMMIT TRANSACTION
Of course, in both options there need to be some error handling - every transaction should use a try...catch so that we can rollback the transaction in case of an error.
That being said, I think the 3rd option is probably my personal favorite, but I don't think there should be a difference.
Update
Following a conversation I've had with Aaron Bertrand in the comments of some other question - I'm not entirely convinced that using ISOLATION LEVEL is a better solution than individual query hints, but at least that's another option to consider:
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION;
INSERT INTO table (<values list>)
SELECT <values list>
WHERE NOT EXISTS(select 1 from table where <condition>);
COMMIT TRANSACTION;
Consider this SQL script:
BEGIN TRAN
CREATE TABLE foo
(
Id INT NOT NULL PRIMARY KEY,
Name NVARCHAR(20) NULL
);
GO
CREATE TRIGGER [foo_insert]
ON foo
AFTER UPDATE
AS
BEGIN
SET NOCOUNT ON;
SELECT COUNT(*) from inserted;
END
GO
INSERT INTO foo VALUES(1, 'Jonh');
INSERT INTO foo VALUES(2, 'Mary');
INSERT INTO foo VALUES(3, 'Peter');
INSERT INTO foo VALUES(4, 'Helen');
SELECT * FROM foo;
CREATE TABLE #bar
(
Id INT NOT NULL
);
UPDATE
foo
SET
foo.Name = NULL
FROM
#bar JOIN foo ON #bar.Id = foo.Id;
SELECT * FROM foo;
DROP TABLE foo;
ROLLBACK TRAN
Running it from Management Studio or similar app, you'll get output like this:
Here AFTER UPDATE trigger fires when server executes UPDATE ... FROM statement. Since #bar is empty, there's nothing to update, but trigger still fires (second result in "Results" pane).
Is there any reason for SQL Server to behave this way?
In other word, I mean, that AFTER UPDATE trigger must be, well, after update - that is, after some records were updated? No?
It's documented behavior. Why does it work that way? You'd have to ask the guy who decided to write it that way. My guess would be performance. Triggers are probably quicker if they don't have to check the operation that triggered them to see if it did anything.
You did an UPDATE, fire the UPDATE TRIGGER. That's fast.
You did an UPDATE, let me check to see if any rows were changed. No? Don't fire at all. That's slower.
Also what happens to the poor coder who wants to have an UPDATE TRIGGER perform a certain action even if no rows were updated? What about him/her, huh?
This is too long for a comment.
Obviously, there is no critical reason for executing the trigger if no rows are updated. That is the behavior of most databases.
On the other hand, there is little harm in doing so. SQL Server uses the inserted and deleted views to provide the list of rows that are modified. As you observe, these could be empty. Most triggers use the contents of inserted and deleted and do nothing when these are empty.
I could imagine situations where this would be useful, particularly for security and auditing purposes. This allows update triggers to keep track of all updates and attempts at updates. That is something that can't be done in many other databases.
Your update is not updating any records.
UPDATE
foo
SET
foo.Name = NULL
FROM
#bar JOIN foo ON #bar.Id = foo.Id;
You'll get 0 and also you are asking to return all records inserted when there hasn't been any insertions in the current transaction.
If you want to do a count, you will either need a transaction with a loop and some kind of counter or change this SELECT COUNT(*) from inserted; to count an specific event.
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.
I want to create a single trigger in MS SQL for INSERT, UPDATE and DELETE but I want to be able to simple extract the data without 3 IF statements
I want to do something like this:
DECLARE #PK int
SELECT #PK = COALESCE(i.PK, d.PK)
FROM inserted i, deleted d
This did not work but would like to know if I can do it in one query.
What are some alternatives?
You can do the switch logic found here: SQL Server Trigger switching Insert,Delete,Update
Or you can create 3 different triggers.
Those are you options.
I ended up using a FULL OUTER JOIN
DECLARE #DataIWanted as varchar(255)
SELECT #DataIWanted = COALESCE(i.TheData,d.TheData)
FROM inserted i
FULL OUTER JOIN deleted d on d.PK=i.PK
The query above will be from the deleted table or the inserted/updated table. With the assumption that TheData is defined as NON NULL in the DB.