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

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?

Related

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

use INSERT inside definition of VIEW: CREATE VIEW AS INSERT INTO

If I want to do something relatively complicated - something usually done by a stored procedure. Is it possible to make it automatic using a VIEW?
My specific case:
I want output table = input table A + some rows input table B. In a stored procedure, I can make a copy of table A and then INSERT INTO it, but it's not allowed in a view.
Simplified example:
input table is [test_album], and output table = input table + singer Prince.
--create test data
IF OBJECT_ID('[dbo].[test_album]', 'U') IS NOT NULL
DROP TABLE [dbo].[test_album]
CREATE TABLE [test_album] (
id int not null identity(1, 1) primary key,
singer VARCHAR(50) NULL,
album_title VARCHAR(100) NULL
)
INSERT INTO [test_album] (singer, album_title)
VALUES ('Adale', '19'),
('Michael Jaskson', 'Thriller')
--this can be executed as sql code or in stored proc
SELECT *
INTO [result_table]
FROM [test_album]
INSERT INTO [result_table] ([singer])
VALUES ('Prince')
select *
from [result_table]
--id singer album_title
--1 Adale 19
--2 Michael Jaskson Thriller
--3 Prince NULL
----as expected
But I can do this INSERT INTO inside a view.
Real-life case:
additional singers are in a table [extra_singers]
[test_album] may have many other columns (or schema may change) so it's ideal not to type all column names in the code.
--create test data
IF OBJECT_ID('[dbo].[test_album]', 'U') IS NOT NULL
DROP TABLE [dbo].[test_album]
IF OBJECT_ID('[dbo].[extra_singers]', 'U') IS NOT NULL
DROP TABLE [dbo].[extra_singers]
IF OBJECT_ID('[dbo].[result_table]', 'U') IS NOT NULL
DROP TABLE [dbo].[result_table]
CREATE TABLE [test_album] (
id int not null identity(1, 1) primary key,
singer VARCHAR(50) NULL,
album_title VARCHAR(100) NULL,
many_other_columns VARCHAR(100) NULL
)
INSERT INTO [test_album] (singer, album_title)
VALUES ('Adale', '19'),
('Michael Jaskson', 'Thriller')
CREATE TABLE [extra_singers] (
[id] int not null identity(1, 1) primary key,
[name] VARCHAR(50) NULL )
INSERT INTO [extra_singers] ([name])
VALUES ('Prince'),
('Taylor Swift')
--append [extra_singers] to [test_album]
--this can be executed as sql code or in stored proc
SELECT *
INTO [result_table]
FROM [test_album]
INSERT INTO [result_table] ([singer])
SELECT [name]
FROM [extra_singers]
Is there an alternative to this (that is automatic)?
any help's appreciated. Thank u-
a partial solution I can think of:
create view test_view as
select *
from [test_album]
union all
select 3 as id,
'Prince' as singer,
NULL as album_title
but you have to know all the column names in [test_album] and you can't let column [id] do auto-increment
So you may be misunderstanding what a view does, or what an insert is. A view is simply a wrapper around a single select query. It contains exactly one select statement, and nothing else. An insert permanently adds a row of data to a persisted table. The example you gave where you just union the row you want seems valid enough. And certainly if it's the same row you want every time, you would not want to be inserting (or even trying to insert) that row into the underlying table each time
This raises a couple questions though.
If you're always going to be unioning the same single row every time, why not jut add that row to the table?
If, lets say, you don't want that row in the underlying table, cool. But if it's always the same static values, why do you need to include it in the view? Can't it just be assumed it's there?
If it can't be assume to always be the same, you certainly don't want to be changing the VIEW body every time you need it to change. So if it is going to change and you don't want to insert it into the base table, maybe make a second table containing the values you want appended to the base table in the view. Then union the base table and the "extra values" table together instead of a single, hard coded row constructor.

Insert using stored procedure

I want to insert a record into multiple tables at single time using a stored procedure. But if it already exists, that record could not be inserted. How can it? I need help. I have link between the tables.
CREATE TABLE [dbo].[tblrole]
(
[roleid] [INT] IDENTITY(1, 1) NOT NULL,
[rolename] [VARCHAR](50) NULL,
PRIMARY KEY CLUSTERED ([roleid] ASC)
)
It's normal that you cannot insert a duplicate record if you have a unique primary key.
You have been talking about multiple tables, but you've schown us just one table definition.
I I've understood well your problem, you would something like this:
create proc insert_data
-- params are coming here
as
if not exists(select 1 from your_target_table1 where column = #condition)
-- your insert comes here
else
-- do nothing or log en error in an error table or do an update
if not exists(select 1 from your_target_table2 where column = #condition)
-- your insert comes here
else
-- do nothing or log en error in an error table or do an update
-- and soon

Supposedly easy trigger

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)
)

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