If exists UPDATE else INSERT for each row of a table - sql-server

I have a table named tableFrom want to insert into a table named tableTo. The insert worked well, but if I insert the same values again, I have a duplicate key error. So I want to update only the rows already existing. I know the command ON DUPLICATE with MySQL, unfortunately missing in SQL Server.
If I want to only check for only one precise row, it is easy:
IF EXISTS PK = #PK
But I am trying to do it for a whole table, and I don't know if it is possible. I thought of cheking each row with cursor, I am new to SQL.
Here is what I came up with:
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRANSACTION
IF EXISTS (
SELECT
1
FROM
tableFrom F,
tableTo T
WHERE
T.product = F._product
)
BEGIN
UPDATE
tableTo
SET
T.something = F.something
FROM
tableTo T
INNER JOIN
tableFrom F
ON
T.product = F._product
END
ELSE
BEGIN
INSERT INTO tableTo
(product,
something)
SELECT
F._product,
F.something
FROM
tableFrom F
END
COMMIT TRANSACTION
The UPDATE part is working fine, but no INSERT done.
EDIT1:
Tried this code:
MERGE tableTo AS T
USING tableFrom AS S
ON (T.product= S._product)
WHEN NOT MATCHED BY TARGET
THEN INSERT(product, something) VALUES(S._product, S.something)
WHEN MATCHED
THEN UPDATE SET T.Something= S.Something
Have the following error: "Incorrect syntax near 'MERGE'. You may need to set the compatibility level of the current database to a higher value to enable this feature. See help for the SET COMPATIBILITY_LEVEL option of ALTER DATABASE."
EDIT2:
I googled the above error message and it appeared to be due to a missing semi-colon at the end of the very last line before the MERGE statement. The MERGE command is working perfect!

It's not missing. SQL Server implements the standard MERGE statement which allows you to specify what happens when a match occurs or not between a source and a target. Check the documentation for examples.
Matches are made using a condition that can involve many columns. MERGE allows you to execute an INSERT, UPDATE or DELETE in the following cases:
A match is found based on the condition
A match occurs only in the source
A match occurs only in the target
This way you can update existing rows, insert rows that exist only in the source, delete rows that appear only in the target.
In your case, you could do something like:
MERGE tableTo AS T
USING tableFrom AS S
ON (T.product= S._product)
WHEN NOT MATCHED BY TARGET
THEN INSERT(product, something) VALUES(S._product, S.something)
WHEN MATCHED
THEN UPDATE SET T.Something= S.Something
OUTPUT $action, Inserted.*, Deleted.*;
This statement will insert or update rows as needed and return the values that were inserted or overwritten with the OUTPUT clause.

Use the Merge operation.
From the MSDN documentation:
Performs insert, update, or delete operations on a target table based
on the results of a join with a source table. For example, you can
synchronize two tables by inserting, updating, or deleting rows in one
table based on differences found in the other table.

Related

SQL server- delete row from table using Trigger

I have a problem with SQL server, I need to create a trigger that works that way:
Every time when I insert information to tblNotInterested (Inside the table I have two columns "email1" and "email2").
The trigger needs to check if "email1" and "email2" already exists in a different table named tblListOf.
If they exist I need to delete the row in which they were found.
Maybe you can do something like this:
DELETE FROM tblListOf
WHERE EXISTS (SELECT * FROM tblNotInterested
WHERE tblNotInterested.email1 = tblListOf.email1 and tblNotInterested.email2 = tblListOf.email2)
inserted and deleted tables can be quite useful, but they are definitely buyer beware because ...
they don't always lend themselves to easy documentation
they are invisible to client applications
they may have performance, or scalability issues, due to expensive locks
That being said, I believe the below code should work for this case. One final caution/question may be around nullable columns in your two tables. If either email1 or email2 is nullable on any table, I would consider re-evaluating this code.
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TRIGGER dbo.trig_i_tblNotInterested_MatchingEmail1AndEmail2
ON dbo.tblNotInterested
AFTER INSERT
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
DELETE L
FROM dbo.tblListOf L
INNER JOIN inserted ins
ON L.email1 = ins.email1
AND L.email2 = ins.email2
END
GO

SQL Server Triggers: How can I obtain a value used in the query string in my trigger's where clause?

I need to create a trigger in SQL Server which is conditionally fired when a user attempts to delete a row in the table.
I have 2 tables in the database Airlines:
Passenger - it has information on passengers. I need to create trigger on this table, obviously
Record - it records the flight(s) a passenger has been on
My trigger should work as follows:
If a passenger has never been on a flight, it should be deleted if attempted.
But if he/she has NOT been on a flight, it should restrict the action and print the number of times he has been on any flight.
The only thing (I hope) I am struggling with is:
How would I specify any WHERE clause inside a query in the trigger if I do not know which particular passenger I need to look for until a user attempts to delete it?
So, long story short: is there any way to obtain a value passed in a query's WHERE clause to be used in a trigger?
Thank you very much for your time!
Here is my code:
ALTER TRIGGER Restrict_Delete
ON Records
INSTEAD OF DELETE
AS
BEGIN
DECLARE #r_count INT
SET #r_count = (SELECT DISTINCT COUNT(*)
FROM Passenger P, Records R
WHERE P.passenger_id = R.passenger_id
AND P.passenger_id = ???)
IF #r_count > 0
BEGIN
ROLLBACK TRAN
PRINT ('Permission denied. ' + CAST (#r_count AS Varchar(3)) + ' record(s) exist.')
END
ELSE
PRINT 'No records exist. Record deleted!'
END
How do I determine the passenger_id in my query?
You can use the deleted and inserted tables!
These are special tables that exist in triggers and contain records copied from the actual tables. When you change a row in a table, a copy of the old row goes into the deleted table, and a copy of the new row goes into the inserted table. Since you're just deleting, you only need to use the deleted table.
Here's how the SQL could look inside your trigger:
DECLARE #r_count INT
SET #r_count = Count(*)
FROM Records R -- You don't actually need the Passenger table for this.
WHERE r.Passenger_id IN (
select d.Passenger_id
from Deleted d
)
IF #r_count > 0
BEGIN
Rollback Tran
PRINT ('Permission denied. ' + CAST (#r_count AS Varchar(3)) + ' record(s) exist.')
END
ELSE
PRINT 'No records exist. Record deleted!'
Something you need to be aware of, though: a trigger is called once per statement, not once per record. So if you delete two passengers with one DELETE statement, you'll get only one trigger call. The logic you had (and I adapted) will check for any record that was deleted by that DELETE statement. You could get quite a large number for #r_count if you're doing a bulk delete!
If you need to code around that, try to avoid using a cursor actually in your trigger: it will make deletes very slow.
Also, be aware that the PRINT statement will appear in SSMS and can be retrieved in ADO.NET with a bit of fiddling around, but doesn't appear in traces or get returned as part of a recordset. If you need to log this failure, you're going to need to write to a database table.

How to Create a Trigger On [table] Delete in MS SQL Server

I am taking a database class and I am completely lost on this one. I hope you can help me out. Here is the SQL trigger I have created based on our text book. It says the examples are all SQL server 2012 based, but when I test the trigger it cannot recognize some of the syntaxes. Is there a way to do this differently? The SHIPMENT_ITEM is child of SHIPMENT, and its a M-M relationship. Thanks :)
CREATE TRIGGER SHIPMENT_ITEM_SHIPMENT_DeleteCheck
ON [dbo].[DeleteShipmentItemView]
FOR DELETE
DECLARE
rowCount Int;
BEGIN
/* to determine if shipment item is last one in shipment */
SELECT Count (*) into rowCount
FROM [dbo].[SHIPMENT_ITEM]
WHERE [dbo].[SHIPMENT_ITEM].[ShipmentID] = old:[ShipmentID];
/*delte shipment item row regardless of wheter shipment is deleted */
DELETE [dbo].[SHIPMENT_ITEM]
WHERE [dbo].[SHIPMENT_ITEM].[ShipmentID] = old:[ShipmentID];
IF (rowCount = 1)
THEN
/*last shipment item in shipment, delete shipment */
DELETE [dbo].[SHIPMENT]
WHERE [dbo].[SHIPMENT].[ShipmentID] = old:[ShipmentID];
END IF;
END;
Level 15, State 1,
Procedure SHIPMENT_ITEM_SHIPMENT_DeleteCheck, Line 5 Incorrect syntax near the keyword 'SET'.
Procedure SHIPMENT_ITEM_SHIPMENT_DeleteCheck, Line 10 Incorrect syntax near the keyword 'rowCount'.
Procedure SHIPMENT_ITEM_SHIPMENT_DeleteCheck, Line 16 Incorrect syntax near 'old:'.
Procedure SHIPMENT_ITEM_SHIPMENT_DeleteCheck, Line 16 The label 'old' has already been declared. Label names must be unique within a query batch or stored procedure.
Procedure SHIPMENT_ITEM_SHIPMENT_DeleteCheck, Line 22 Incorrect syntax near 'old:'.
Procedure SHIPMENT_ITEM_SHIPMENT_DeleteCheck, Line 22 The label 'old' has already been declared. Label names must be unique within a query batch or stored procedure.
There are several things that doesn't match t-sql syntax in your trigger.
DECLARE
rowCount Int;
Variables in t-sql must start with a # sign.
The correct syntax would be DECLARE #rowCount Int;
SELECT Count (*) into rowCount
in t-sql, Select into will create a new table. you probably want SELECT #rowCount = Count (*) instead.
[dbo].[SHIPMENT_ITEM].[ShipmentID] = old:[ShipmentID];
Actually, I'm not sure what you want to accomplish in this line, but it's not t-sql. I think you want to get the number of rows that was deleted. If that is the case, it's SELECT COUNT(*) FROM deleted.
The way I understand your trigger, your goal is to delete a record in SHIPMENT table if there are no corresponding records in the SHIPMENT_ITEM table for it.
I would probably do something like this:
CREATE TRIGGER SHIPMENT_ITEM_SHIPMENT_DeleteCheck
ON dbo.SHIPMENT_ITEM
FOR DELETE
AS
DELETE
FROM dbo.SHIPMENT s
WHERE EXISTS (
SELECT 1
FROM deleted d
WHERE d.ShipmentID = s.ShipmentID
)
AND NOT EXISTS(
SELECT 1
FROM dbo.SHIPMENT_ITEM si
WHERE si.ShipmentID = s.ShipmentID
)
GO
Notes:
This trigger contains a where clause with both exists and not exists sub queries. This ensures that you delete only the shipment's records that corresponded to the shipment_item that you have just deleted, and only if there are no other shipment_item records with the same shipmnet id.
The trigger in the question is for a view, this is for the table itself. note that not all views in sql server are updatable, so you might need to create the trigger on the view and not directly on the shipment_item table. Also note that you can't use after triggers on a view in sql server
For more information, check the Create trigger page on MSDN.

perform more than one operation on MERGE ... THEN in SQL SERVER

I am using MERGE to perform UPSERT something. However in the THEN section I need to perform more than one operation, I need to both INSERT to table and also UPDATE something in another table.
My problem is that I don't seem to see how to perform more than one action.
I tried this:
MERGE tblCategories AS T
USING #RELEVANT_CATS AS S
ON (T.CatId = S.CatId)
WHEN NOT MATCHED BY TARGET
THEN
BEGIN
INSERT (BizID, RequestId) VALUES(S.CatId, #CatId);
END
The BEGIN and END are not allowed here for some reason.
Also tried this:
MERGE tblCategories AS T
USING #RELEVANT_CATS AS S
ON (T.CatId = S.CatId)
WHEN NOT MATCHED BY TARGET
THEN EXECUTE INSERT_CAT S.CatId, #CatId;
Where INSERT_CAT is some stored procedure that performs what I need, but again, this syntax is not allowed.
Any ideas?
Start a transaction.
Add the output clause to your merge and capture the results of the merge into a table variable.
Use the table variable to update the other table where the captured $action was 'INSERT'.
Commit the transaction.

SQL Server "AFTER INSERT" trigger doesn't see the just-inserted row

Consider this trigger:
ALTER TRIGGER myTrigger
ON someTable
AFTER INSERT
AS BEGIN
DELETE FROM someTable
WHERE ISNUMERIC(someField) = 1
END
I've got a table, someTable, and I'm trying to prevent people from inserting bad records. For the purpose of this question, a bad record has a field "someField" that is all numeric.
Of course, the right way to do this is NOT with a trigger, but I don't control the source code... just the SQL database. So I can't really prevent the insertion of the bad row, but I can delete it right away, which is good enough for my needs.
The trigger works, with one problem... when it fires, it never seems to delete the just-inserted bad record... it deletes any OLD bad records, but it doesn't delete the just-inserted bad record. So there's often one bad record floating around that isn't deleted until somebody else comes along and does another INSERT.
Is this a problem in my understanding of triggers? Are newly-inserted rows not yet committed while the trigger is running?
Triggers cannot modify the changed data (Inserted or Deleted) otherwise you could get infinite recursion as the changes invoked the trigger again. One option would be for the trigger to roll back the transaction.
Edit: The reason for this is that the standard for SQL is that inserted and deleted rows cannot be modified by the trigger. The underlying reason for is that the modifications could cause infinite recursion. In the general case, this evaluation could involve multiple triggers in a mutually recursive cascade. Having a system intelligently decide whether to allow such updates is computationally intractable, essentially a variation on the halting problem.
The accepted solution to this is not to permit the trigger to alter the changing data, although it can roll back the transaction.
create table Foo (
FooID int
,SomeField varchar (10)
)
go
create trigger FooInsert
on Foo after insert as
begin
delete inserted
where isnumeric (SomeField) = 1
end
go
Msg 286, Level 16, State 1, Procedure FooInsert, Line 5
The logical tables INSERTED and DELETED cannot be updated.
Something like this will roll back the transaction.
create table Foo (
FooID int
,SomeField varchar (10)
)
go
create trigger FooInsert
on Foo for insert as
if exists (
select 1
from inserted
where isnumeric (SomeField) = 1) begin
rollback transaction
end
go
insert Foo values (1, '1')
Msg 3609, Level 16, State 1, Line 1
The transaction ended in the trigger. The batch has been aborted.
You can reverse the logic. Instead of deleting an invalid row after it has been inserted, write an INSTEAD OF trigger to insert only if you verify the row is valid.
CREATE TRIGGER mytrigger ON sometable
INSTEAD OF INSERT
AS BEGIN
DECLARE #isnum TINYINT;
SELECT #isnum = ISNUMERIC(somefield) FROM inserted;
IF (#isnum = 1)
INSERT INTO sometable SELECT * FROM inserted;
ELSE
RAISERROR('somefield must be numeric', 16, 1)
WITH SETERROR;
END
If your application doesn't want to handle errors (as Joel says is the case in his app), then don't RAISERROR. Just make the trigger silently not do an insert that isn't valid.
I ran this on SQL Server Express 2005 and it works. Note that INSTEAD OF triggers do not cause recursion if you insert into the same table for which the trigger is defined.
Here's my modified version of Bill's code:
CREATE TRIGGER mytrigger ON sometable
INSTEAD OF INSERT
AS BEGIN
INSERT INTO sometable SELECT * FROM inserted WHERE ISNUMERIC(somefield) = 1 FROM inserted;
INSERT INTO sometableRejects SELECT * FROM inserted WHERE ISNUMERIC(somefield) = 0 FROM inserted;
END
This lets the insert always succeed, and any bogus records get thrown into your sometableRejects where you can handle them later. It's important to make your rejects table use nvarchar fields for everything - not ints, tinyints, etc - because if they're getting rejected, it's because the data isn't what you expected it to be.
This also solves the multiple-record insert problem, which will cause Bill's trigger to fail. If you insert ten records simultaneously (like if you do a select-insert-into) and just one of them is bogus, Bill's trigger would have flagged all of them as bad. This handles any number of good and bad records.
I used this trick on a data warehousing project where the inserting application had no idea whether the business logic was any good, and we did the business logic in triggers instead. Truly nasty for performance, but if you can't let the insert fail, it does work.
I think you can use CHECK constraint - it is exactly what it was invented for.
ALTER TABLE someTable
ADD CONSTRAINT someField_check CHECK (ISNUMERIC(someField) = 1) ;
My previous answer (also right by may be a bit overkill):
I think the right way is to use INSTEAD OF trigger to prevent the wrong data from being inserted (rather than deleting it post-factum)
UPDATE: DELETE from a trigger works on both MSSql 7 and MSSql 2008.
I'm no relational guru, nor a SQL standards wonk. However - contrary to the accepted answer - MSSQL deals just fine with both recursive and nested trigger evaluation. I don't know about other RDBMSs.
The relevant options are 'recursive triggers' and 'nested triggers'. Nested triggers are limited to 32 levels, and default to 1. Recursive triggers are off by default, and there's no talk of a limit - but frankly, I've never turned them on, so I don't know what happens with the inevitable stack overflow. I suspect MSSQL would just kill your spid (or there is a recursive limit).
Of course, that just shows that the accepted answer has the wrong reason, not that it's incorrect. However, prior to INSTEAD OF triggers, I recall writing ON INSERT triggers that would merrily UPDATE the just inserted rows. This all worked fine, and as expected.
A quick test of DELETEing the just inserted row also works:
CREATE TABLE Test ( Id int IDENTITY(1,1), Column1 varchar(10) )
GO
CREATE TRIGGER trTest ON Test
FOR INSERT
AS
SET NOCOUNT ON
DELETE FROM Test WHERE Column1 = 'ABCDEF'
GO
INSERT INTO Test (Column1) VALUES ('ABCDEF')
--SCOPE_IDENTITY() should be the same, but doesn't exist in SQL 7
PRINT ##IDENTITY --Will print 1. Run it again, and it'll print 2, 3, etc.
GO
SELECT * FROM Test --No rows
GO
You have something else going on here.
From the CREATE TRIGGER documentation:
deleted and inserted are logical (conceptual) tables. They are
structurally similar to the table on
which the trigger is defined, that is,
the table on which the user action is
attempted, and hold the old values or
new values of the rows that may be
changed by the user action. For
example, to retrieve all values in the
deleted table, use: SELECT * FROM deleted
So that at least gives you a way of seeing the new data.
I can't see anything in the docs which specifies that you won't see the inserted data when querying the normal table though...
I found this reference:
create trigger myTrigger
on SomeTable
for insert
as
if (select count(*)
from SomeTable, inserted
where IsNumeric(SomeField) = 1) <> 0
/* Cancel the insert and print a message.*/
begin
rollback transaction
print "You can't do that!"
end
/* Otherwise, allow it. */
else
print "Added successfully."
I haven't tested it, but logically it looks like it should dp what you're after...rather than deleting the inserted data, prevent the insertion completely, thus not requiring you to have to undo the insert. It should perform better and should therefore ultimately handle a higher load with more ease.
Edit: Of course, there is the potential that if the insert happened inside of an otherwise valid transaction that the wole transaction could be rolled back so you would need to take that scenario into account and determine if the insertion of an invalid data row would constitute a completely invalid transaction...
Is it possible the INSERT is valid, but that a separate UPDATE is done afterwards that is invalid but wouldn't fire the trigger?
The techniques outlined above describe your options pretty well. But what are the users seeing? I can't imagine how a basic conflict like this between you and whoever is responsible for the software can't end up in confusion and antagonism with the users.
I'd do everything I could to find some other way out of the impasse - because other people could easily see any change you make as escalating the problem.
EDIT:
I'll score my first "undelete" and admit to posting the above when this question first appeared. I of course chickened out when I saw that it was from JOEL SPOLSKY. But it looks like it landed somewhere near. Don't need votes, but I'll put it on the record.
IME, triggers are so seldom the right answer for anything other than fine-grained integrity constraints outside the realm of business rules.
MS-SQL has a setting to prevent recursive trigger firing. This is confirgured via the sp_configure stored proceedure, where you can turn recursive or nested triggers on or off.
In this case, it would be possible, if you turn off recursive triggers to link the record from the inserted table via the primary key, and make changes to the record.
In the specific case in the question, it is not really a problem, because the result is to delete the record, which won't refire this particular trigger, but in general that could be a valid approach. We implemented optimistic concurrency this way.
The code for your trigger that could be used in this way would be:
ALTER TRIGGER myTrigger
ON someTable
AFTER INSERT
AS BEGIN
DELETE FROM someTable
INNER JOIN inserted on inserted.primarykey = someTable.primarykey
WHERE ISNUMERIC(inserted.someField) = 1
END
Your "trigger" is doing something that a "trigger" is not suppose to be doing. You can simple have your Sql Server Agent run
DELETE FROM someTable
WHERE ISNUMERIC(someField) = 1
every 1 second or so. While you're at it, how about writing a nice little SP to stop the programming folk from inserting errors into your table. One good thing about SP's is that the parameters are type safe.
I stumbled across this question looking for details on the sequence of events during an insert statement & trigger. I ended up coding some brief tests to confirm how SQL 2016 (EXPRESS) behaves - and thought it would be appropriate to share as it might help others searching for similar information.
Based on my test, it is possible to select data from the "inserted" table and use that to update the inserted data itself. And, of interest to me, the inserted data is not visible to other queries until the trigger completes at which point the final result is visible (at least best as I could test). I didn't test this for recursive triggers, etc. (I would expect the nested trigger would have full visibility of the inserted data in the table, but that's just a guess).
For example - assuming we have the table "table" with an integer field "field" and primary key field "pk" and the following code in our insert trigger:
select #value=field,#pk=pk from inserted
update table set field=#value+1 where pk=#pk
waitfor delay '00:00:15'
We insert a row with the value 1 for "field", then the row will end up with the value 2. Furthermore - if I open another window in SSMS and try:
select * from table where pk = #pk
where #pk is the primary key I originally inserted, the query will be empty until the 15 seconds expire and will then show the updated value (field=2).
I was interested in what data is visible to other queries while the trigger is executing (apparently no new data). I tested with an added delete as well:
select #value=field,#pk=pk from inserted
update table set field=#value+1 where pk=#pk
delete from table where pk=#pk
waitfor delay '00:00:15'
Again, the insert took 15sec to execute. A query executing in a different session showed no new data - during or after execution of the insert + trigger (although I would expect any identity would increment even if no data appears to be inserted).

Resources