Supposedly easy trigger - sql

I've created a trigger which is to block inserted records with a date already existing in a table.
CREATE TRIGGER [dbo].[SpecialOffers_Insert]
ON [dbo].[SpecialOffers]
FOR INSERT,UPDATE
AS
SET NOCOUNT ON
IF EXISTS (SELECT * FROM inserted WHERE SPO_DateFrom IN (SELECT SPO_DateFrom FROM dbo.SpecialOffers))
BEGIN
RAISERROR('Error. ', 16, 1)
ROLLBACK TRAN
SET NOCOUNT OFF
RETURN
END
SET NOCOUNT OFF
It is added to a table:
CREATE TABLE [dbo].[SpecialOffers](
[SPO_SpoId] [int] IDENTITY(1,1) NOT NULL,
[SPO_DateFrom] [datetime] NOT NULL,
[SPO_DateTo] [datetime] NOT NULL)
The table is empty but when trying to insert such record:
INSERT INTO dbo.SpecialOffers (SPO_DateFrom, SPO_DateTo) VALUES ('2015-01-15','2015-01-15')
I got the Error from the trigger. How should I modify the trigger not to get the error?

If the goal is to block inserted records with date already existing in a table, you don't need a trigger - just create a unique constraint on the date field:
ALTER TABLE [dbo].[SpecialOffers]
ADD CONSTRAINT SpecialOffersUQ UNIQUE (SPO_DateFrom)

If you wanted a trigger to prevent overlaps, why didn't you say so:
CREATE TABLE [dbo].[SpecialOffers](
[SPO_SpoId] [int] IDENTITY(1,1) NOT NULL,
[SPO_DateFrom] [datetime] NOT NULL,
[SPO_DateTo] [datetime] NOT NULL,
constraint CK_SO_NoTimeTravel CHECK (SPO_DateFrom <= SPO_DateTo)
)
GO
CREATE TRIGGER NoOverlaps
on dbo.SpecialOffers
after insert,update
as
set nocount on
if exists (
select *
from dbo.SpecialOffers so1
inner join
dbo.SpecialOffers so2
on
so1.SPO_DateFrom < so2.SPO_DateTo and
so2.SPO_DateFrom < so1.SPO_DateTo and
so1.SPO_SpoId != so2.SPO_SpoId
inner join
inserted i
on
so1.SPO_SpoId = i.SPO_SpoId
)
begin
RAISERROR('No overlaps',16,1)
ROLLBACK
end
Examples:
--Works
INSERT INTO SpecialOffers (SPO_DateFrom,SPO_DateTo)
values ('20010101','20011231')
GO
--Fails (Trigger)
INSERT INTO SpecialOffers (SPO_DateFrom,SPO_DateTo)
values ('20010101','20011231')
GO
--Fails (Constraint)
INSERT INTO SpecialOffers (SPO_DateFrom,SPO_DateTo)
values ('20011231','20010101')
GO
--Fails (Trigger)
INSERT INTO SpecialOffers (SPO_DateFrom,SPO_DateTo)
values ('20020101','20021231'),
('20020701','20030630')
I also added a check constraint so that I didn't have to deal with nonsense data in the trigger.
You might have to change swap some of the <s for <=s or vice-versa, depending on what definition of intervals you want to use (i.e. are DateFrom and DateTo meant to be inclusive or exclusive endpoints for the interval they're describing?)

Since the trigger runs in the transaction context of the SQL statement that fired it, after this INSERT, there will be a row in your table dbo.SpecialOffers with the SPO_DateFrom values you've just inserted and the SELECT from the table will succeed ...
Therefore, the trigger will assume that there's already been a value - and it throws the error (as designed).
You could rewrite the trigger to not look at the newly inserted rows, but anything else - but as others have pointed out, a UNIQUE constraint does that much more simply

You should check if the rows you found are actually NOT the ones you have just inserted. Change the line
IF EXISTS (
SELECT * FROM inserted
WHERE SPO_DateFrom IN (
SELECT SPO_DateFrom
FROM dbo.SpecialOffers)
)
To
IF EXISTS (
SELECT * FROM inserted a
WHERE SPO_DateFrom IN (
SELECT SPO_DateFrom
FROM dbo.SpecialOffers b
WHERE a.SPO_SpoId <> b.SPO_SpoId)
)

Related

Insert where not exists but thread safe (I don't want duplicates)

I need to insert values into a table if a value with a matching ID does not exist like in this thread: SQL - Insert Where Not Exists
But I need to make sure if an other thread executes a query in exactly the same time, I won't get two the same rows.
This is my table:
CREATE TABLE [dbo].[Localizations]
(
[id] [int] IDENTITY(1,1) NOT NULL,
[name] [nvarchar](50) NOT NULL,
[regionId] [int] NOT NULL
) ON [PRIMARY]
This my current query which inserts a new localization row if a localization row with regionId = x doesn't exist (unfortunately it works incorrectly - now I have duplicates in my database):
-- firstly I execute something like that (my real queries are more complex):
DECLARE #id int = (SELECT [id] FROM [dbo].[Localizations] WHERE regionId = 1);
-- secondly I execute something like that (my real queries are more complex):
IF (#id IS NULL)
BEGIN
INSERT INTO [dbo].[Localizations]
VALUES ('Test', 1);
END
It caused that now I have MANY rows with the same regionId, I can't remove them now, they are used in different tables :( :( :( Because of that, I can't create the unique constraint on the regionId column because I have duplicates :( :(
Could you tell me if the following query doesn't create duplicates with the same regionId if many threads execute that query in the same time? I have read this thread:
SQL - Insert Where Not Exists
but I am not sure and I don't want to insert more duplicates :(
INSERT INTO [dbo].[Localizations] (name, regionId)
SELECT 'Test', 1
WHERE NOT EXISTS (SELECT *
FROM [dbo].[Localizations]
WHERE regionId = 1)
After you remove the duplicates and add a unique constraint you can change your batch to prevent sessions from attempting to insert duplicates like this:
BEGIN TRANSACTION;
DECLARE #id int = (SELECT [id] FROM [dbo].[Localizations] WITH (UPDLOCK,HOLDLOCK) WHERE regionId = 1);
-- secondly I execute something like that (my real queries are more complex):
IF (#id is null)
BEGIN
INSERT INTO [dbo].[Localizations] VALUES('Test', 1);
END
COMMIT TRANSACTION;
This will force the first query to take and hold an update lock on the row or empty range, which will ensure that the INSERT will succeed, and any other session running this code will block until the transaction is committed.
You already know the answer, you should remove duplicities and add unique constraint. Until then, your data are broken.
If you want just a patch for new data, you can create unique filtered index on regionId, where you filter on regionId > lastDuplicitValue. But if you do not care about duplicities you already have, why care about the new ones?

How to create trigger that updates field on parent with the sum from 2 children

Updated to include screenshot - I need to create a trigger to update a field on a parent table with the sum of the values from two child tables. When the parent record is saved it should calculate ParentTotalEmployees = Sum(CountryTotEmployees) + Sum(StateTotEmployees). I can get it to populate if I only reference one child table but I haven't been able to figure out how to include the second child table.
ALTER TRIGGER [dbo].[DD_UpdateTotEmp] ON [dbo].[DEALDATA]
AFTER INSERT,DELETE,UPDATE
AS
BEGIN
;WITH GrandTotCountry AS (
SELECT c.QDEALDATA1,
SUM(QTOTCOUNTRYEMP) AS TotCountryEmp
FROM
DEALDATA1 c
GROUP BY c.QDEALDATA1
),
GrandTotState AS (
SELECT c.QDEALDATA,
SUM(QNUMSTATEEMP) AS TotStateEmp
FROM
DEALDATA2 c
GROUP BY c.QDEALDATA)
UPDATE T1
SET T1.QGRANDTOTEMP = (SELECT TotCountryEmp
FROM GrandTotCountry T2
WHERE T2.QDEALDATA=i.QDEALDATA)
FROM DEALDATA T1
INNER JOIN Inserted i ON T1.QDEALDATA=i.QDEALDATA
END
OR THIS ONE
CREATE TRIGGER [dbo].[DD_UpdateTotEmp] ON [dbo].[DEALDATA]
AFTER INSERT,DELETE,UPDATE
AS
BEGIN
UPDATE T1
SET T1.QGRANDTOTEMP = (SELECT SUM(QTOTCOUNTRYEMP)
FROM DEALDATA1 T2
WHERE T2.QDEALDATA=i.QDEALDATA)
FROM DEALDATA T1
INNER JOIN Inserted i ON T1.QDEALDATA=i.QDEALDATA
END
Sample Data
USE TEMPDB
GO
-- Parent Table
CREATE TABLE [dbo].[DEALDATA](
[QDEALDATA] [varchar](36) NOT NULL PRIMARY KEY CLUSTERED,
[MATTERSYSID] [varchar](36) NULL,
[QGRANDTOTEMP] [numeric](12, 0) NULL )
GO
INSERT INTO DEALDATA VALUES ('1404fcb1','C333897E',NULL);
INSERT INTO DEALDATA VALUES ('a51f9f8a','8AE3F809',NULL);
GO
-- Country Emp Table
CREATE TABLE [dbo].[DEALDATA1](
[QDEALDATA1] [varchar](36) NOT NULL PRIMARY KEY CLUSTERED,
[QDEALDATA] [varchar](36) NULL,
[QCOUNTRY] [varchar](40) NULL,
[QTOTCOUNTRYEMP] [numeric](12, 0) NULL )
GO
INSERT INTO DEALDATA1 VALUES ('60ae5737','a51f9f8a','Monaco',5);
INSERT INTO DEALDATA1 VALUES ('62ceecb9','a51f9f8a','Australia',10);
INSERT INTO DEALDATA1 VALUES ('a645fcd1','1404fcb1','United States',100);
GO
-- State Emp Table
CREATE TABLE [dbo].[DEALDATA2](
[QDEALDATA2] [varchar](36) NOT NULL PRIMARY KEY CLUSTERED,
[QDEALDATA] [varchar](36) NULL,
[QEMPSTATE] [varchar](40) NULL,
[QNUMSTATEEMP] [numeric](12, 0) NULL )
GO
INSERT INTO DEALDATA2 VALUES ('453b7b64','a51f9f8a','NY',50);
INSERT INTO DEALDATA2 VALUES ('e803b38f','a51f9f8a','KY',50);
INSERT INTO DEALDATA2 VALUES ('413954e1','1404fcb1','MO',20);
INSERT INTO DEALDATA2 VALUES ('ef2213e5','1404fcb1','HI',10);
GO
Thank you in advance in helping me with this.
A trigger (insert, Update, and/or Delete) belongs to a particular table. If you need a trigger on two tables (or many tables) you will need two triggers (or many triggers).
However, you can write a stored-procedure and call it from two triggers. And Since you have used after trigger, you don't need to use Inserted, Deleted objects.
It can be like this:
ALTER TRIGGER Trigger1 ON Table1
AFTER INSERT,DELETE,UPDATE
AS
BEGIN
EXEC‌ TheProcedure
END‌
and
ALTER TRIGGER Trigger2 ON Table2
AFTER INSERT,DELETE,UPDATE
AS
BEGIN
EXEC‌ TheProcedure
END‌
As you see the notes bellow, the above code performance is really bad. The best you can do is to redesign your tables. However, if you prefer slight modification on your data base design, you can create two aggregate tables for your child tables and then use a VIEW‌ to combine them into a single result.
Here is the solution. Thanks to all who responded!
UPDATE dst
SET dst.GrandTotEmp = COALESCE(tot1.TotCountryEmp, 0) + COALESCE(tot2.TotStateEmp, 0)
FROM DEALDATA as dst
JOIN inserted AS i ON dst.QDEALDATA = i.QDEALDATA
LEFT JOIN GrandTotCountry AS tot1 ON tot1.QDEALDATA = dst.QDEALDATA
LEFT JOIN GrantTotState AS tot2 ON tot2.QDEALDATA = dst.QDEALDATA

AFTER UPDATE trigger gets fired AFTER INSERT - sql server

I have 2 datatriggers, one is supposed to fire after update and the other one after insert. This is how the beginning of the update trigger looks like:
USE [Database]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER TRIGGER [dbo].[UpdateTrigger]
ON [dbo].[Table1]
AFTER UPDATE
AS
BEGIN
(...)
The after insert trigger looks exactly the same, apart from the fact that there's AFTER INSERT instead of AFTER UPDATE. When I update the Table1, only update trigger is fired. However, when I insert, both triggers are fired. Why? How can I solve it?
When trigger fires there are 2 virtual tables: DELETED and INSERTED. When you insert records you have the records only in INSERTED table, however when you update the record you have old records in DELETED table and new ones in the INSERTED tables. So, basically when you do UPDATE, you are doing DELETE and INSERT. You can easily solve that by identifying if there are records in DELETED table. If there are then it was UPDATE, if there is no then it was INSERT.
I was having the same experience as JerryBox where the separate update trigger would fire when the insert trigger fired.
What I wanted was on inserting a new record an insert trigger would insert the datetime stamp when the record was created and insert the username the record was created by. Conversely, if record was updated add/update last modified datetime and last modified by username.
In my case, when a new record was inserted into the table, the insert trigger would fire and add values to fields in the SAME TABLE (i.e. CreatedBy and CreatedDateTime fields). These additions were ‘seen’ by the update trigger as an update to an existing record so ModifiedBy and ModifiedDateTime fields would also be inserted.
In other words, inserting a new record would cause both insert and update triggers to fire.
Reading the comments on the posting by Dmitrij Kultasev, I came up with the following solution:
Combine both INSERT and UPDATE triggers into a single TRIGGER xxx
ON xxx AFTER INSERT, UPDATE ...
Check for old records in the DELETED table
If records exist in DELETED table then update actions are performed
If no records exist in DELETED table then insert actions are
performed
Combining both triggers into a single trigger has outcome that update of records by an AFTER INSERT trigger does not fire the AFTER UPDATE trigger.
USE [Database]
GO
CREATE TABLE [dbo].[Table1](
[id] [int] IDENTITY(1,1) PRIMARY KEY,
[Data] [nvarchar](255) NULL,
[CreatedBy] [nvarchar](255) NULL,
[CreatedDateTime] [datetime] NULL,
[ModifiedBy] [nvarchar](255) NULL,
[ModifiedDateTime] [datetime] NULL)
GO
CREATE TRIGGER [dbo].[UpdateTrigger]
ON [dbo].[Table1]
AFTER INSERT, UPDATE
AS
BEGIN
IF EXISTS (SELECT 1 FROM deleted)
UPDATE [dbo].[Table1]
SET [ModifiedBy] = SYSTEM_USER,
[ModifiedDateTime] = SYSDATETIME()
FROM inserted i
WHERE i.id = [dbo].[Table1].id
ELSE
UPDATE [dbo].[Table1]
SET [CreatedBy] = SYSTEM_USER,
[CreatedDateTime] = SYSDATETIME()
FROM inserted i
WHERE i.id = [dbo].[Table1].id
END
GO
Edits: Formatting, Improved explanation in response to feedback from andronicus

SQL Trigger updating after

I have a question regarding triggers and i'm not sure if this is the proper way to do this. But I have a table called 'tblWell' with id, welllonglat (geography) and welllong and welllat which are decimal value types. I'm using lightswitch to enter data which doesn't support the geography value type. So they enter long and lat and insert. Now I need to update the welllonglat (geography) value type field after the newly record has been inserted so I thought a trigger would work. I'm not that great with triggers so wondering how I get the value of welllong and welllat if the newly inserted record and then update that record welllonglat using this function. tblWell.WellLongLat=geography::Point(tblWell.WellLat, tblWell.WellLong, 4326)
right now says parameter 1 is null.. how do I get those values.. or how else can I do this? code is below thanks
Alter TRIGGER trgAfterInsert ON [tblWell]
After INSERT
AS
begin
set nocount on
DECLARE #long decimal
DECLARE #lat decimal
update
[tblWell]
set tblWell.WellLongLat=geography::Point(tblWell.WellLat, tblWell.WellLong, 4326)
FROM
tblWell
end
go
according to answer below. the following code should work but I still get a null error
Alter TRIGGER trgAfterInsert ON [tblWell]
After INSERT
AS
Begin
set nocount on
UPDATE A
SET WellLongLat = geography::Point(B.WellLat, B.WellLong, 4326)
FROM [tblWell] A
INNER JOIN INSERTED B
ON A.WellUID = B.WellUID
where B.WellLat IS NOT NULL and B.WellLong IS NOT NULL
END
go
Here is the table structure
CREATE TABLE [dbo].[tblWell](
[WellUID] [int] IDENTITY(1,1) NOT NULL,
[WellLocation] [varchar](500) NULL,
[WellLongLat] [geography] NULL,
[WarehouseID] [int] NULL,
[ProgrammedCost] [money] NULL,
[ProductionTypeID] [int] NULL,
[WellTypeID] [int] NULL,
[OperatorID] [int] NULL,
[WellLong] [decimal](9, 6) NULL,
[WellLat] [decimal](9, 6) NULL,
CONSTRAINT [PK_tblWell] PRIMARY KEY CLUSTERED
(
[WellUID] ASC
)
You need to use the INSERTED pseudo table:
ALTER TRIGGER trgAfterInsert ON [tblWell] AFTER INSERT
AS
BEGIN
SET NOCOUNT ON
UPDATE A
SET WellLongLat = geography::Point(B.WellLat, B.WellLong, 4326)
FROM [tblWell] A
INNER JOIN INSERTED B
ON A.id = B.id
END
If you want to check the existence of WellLat and WellLong, you can use a WHERE:
ALTER TRIGGER trgAfterInsert ON [tblWell] AFTER INSERT
AS
BEGIN
SET NOCOUNT ON
IF EXISTS(SELECT 1 FROM INSERTED WHERE WellLat IS NOT NULL
AND WellLong IS NOT NULL)
BEGIN
UPDATE A
SET WellLongLat = geography::Point(B.WellLat, B.WellLong, 4326)
FROM [tblWell] A
INNER JOIN INSERTED B
ON A.id = B.id
WHERE B.WellLat IS NOT NULL AND B.WellLong IS NOT NULL
END
END
Lamak's answer is good. here's why yours does not work. You are trying to set values from tbWell. so what your query is doing is going through every row and setting WellLongLat according to that row's WellLat and WellLong values. I imagine you have a null value for WellLat or WellLong somewhere thus the null error.
Doing as Lamak demonstrates you get the values of the just inserted row out of the INSERTED table and then match them to the row just inserted based on the id column (which should be unique). thus you end up updating one row (if id is unique) and only row and using the new values. now technically I suppose you don't HAVE to use inserted in the Set part of the update, but its better form to do so.
small note: your declares do nothing in the trigger because you never use them so no point to them

Trigger for insert on identity column

I have a table A with an Identity Column which is the primary key.
The primary key is at the same time a foreign key that points towards another table B.
I am trying to build an insert trigger that inserts into Table B the identity column that is about to be created in table A and another custom value for example '1'.
I tried using ##Identity but I keep getting a foreign key conflict. Thanks for your help.
create TRIGGER dbo.tr ON dbo.TableA FOR INSERT
AS
SET NOCOUNT ON
begin
insert into TableB
select ##identity, 1;
end
alexolb answered the question himself in the comments above. Another alternative is to use the IDENT_CURRENT function instead of selecting from the table. The drawback of this approach is that it always starts your number one higher than the seed, but that is easily remedied by setting the seed one unit lower. I think it feels better to use a function than a subquery.
For example:
CREATE TABLE [tbl_TiggeredTable](
[id] [int] identity(0,1) NOT NULL,
[other] [varchar](max)
)
CREATE TRIGGER [trgMyTrigger]
ON [tbl_TriggeredTable]
INSTEAD OF INSERT,UPDATE,DELETE
SET identity_insert tbl_TriggeredTable ON
INSERT INTO tbl_TriggeredTable (
[id],
[other]
)
SELECT
-- The identity column will have a zero in the insert table when
-- it has not been populated yet, so we need to figure it out manually
case i.[id]
when 0 then IDENT_CURRENT('tbl_TriggeredTable') + IDENT_INCR('tbl_TriggeredTable')
ELSE i.[id]
END,
i.[other],
FROM inserted i
SET identity_insert tbl_TriggeredTable OFF
END