T-SQL Insert into in the BEGIN CATCH - sql-server

Thanks in advance for the help.
What I'm trying to achieve is handling the constraint violation of the FK (Municipality code) and when that's the case I want to insert the record in a fallout table.
In this block of code there result of the select can be null and therefore will throw an exception as I have a not null clause on the target.
TARGET.MUNICIPALITYCODE = (SELECT m.MUNICIPALITYCODE FROM MUNICIPALITY m WHERE SOURCE.MUNICIPALITYCODE = m.MUNICIPALITYCODE)
I wanted to be able to treat the exception on the BEGIN CATCH block and INSERT into a table of my choice the values that I was using on the SOURCE.
Does anyone knows if it is possible?
CREATE PROCEDURE upsertStagingToStreet
AS
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRY
BEGIN TRAN
MERGE STREET AS TARGET
USING STREET_STAGING AS SOURCE
ON (TARGET.correlationkey = SOURCE.correlationkey)
WHEN MATCHED
THEN UPDATE SET TARGET.Qty = SOURCE.Qty,
TARGET.MUNICIPALITYCODE = (SELECT m.MUNICIPALITYCODE FROM MUNICIPALITY m WHERE SOURCE.MUNICIPALITYCODE = m.MUNICIPALITYCODE)
WHEN NOT MATCHED BY TARGET
THEN INSERT (MUNICIPALITYCODE, STREENAME,STREECODE) VALUE ((SELECT m.MUNICIPALITYCODE FROM MUNICIPALITY m WHERE SOURCE.MUNICIPALITYCODE = m.MUNICIPALITYCODE),SOURCE.STREETCODE,SOURCE.STREETCODE)
COMMIT
END TRY
begin catch
# I'm not able to figure this part out.
INSERT INTO STREET_FALLOUT (MUNICIPALITYCODE, STREENAME,STREECODE,ERRORREASON) VALUES (SOURCE.MUNICIPALITYCODE,STREETNAME,STREETCODE,ERROR_MESSAGE())
end catch

You are approaching this wrong. It is not possible to insert some of the rows and catch errors on others.
Instead, just query the non-matching rows, and merge only the matching ones.
Note the lack of error-handling, and the inclusion of XACT_ABORT ON. This is the correct way, as all errors will cause the transaction to rollback anyway.
Note the SOURCE table in the merge is pre-joined with MUNICIPALITY, so only matching rows can appear.
A MERGE statement must have a semi-colon terminator, which is good practice anyway.
CREATE PROCEDURE upsertStagingToStreet
AS
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SET XACT_ABORT, NOCOUNT ON;
BEGIN TRAN
INSERT INTO STREET_FALLOUT
(MUNICIPALITYCODE, STREENAME, STREECODE, ERRORREASON)
SELECT
SOURCE.MUNICIPALITYCODE,
SOURCE.STREETCODE,
SOURCE.STREETCODE,
'Missing MUNICIPALITYCODE'
FROM STREET_STAGING AS SOURCE
WHERE NOT EXISTS (SELECT 1
FROM MUNICIPALITY m
WHERE SOURCE.MUNICIPALITYCODE = m.MUNICIPALITYCODE
);
WITH SOURCE AS (
SELECT SOURCE.*
FROM STREET_STAGING AS SOURCE
JOIN MUNICIPALITY m ON SOURCE.MUNICIPALITYCODE = m.MUNICIPALITYCODE
)
MERGE STREET AS TARGET
USING SOURCE
ON (TARGET.correlationkey = SOURCE.correlationkey)
WHEN MATCHED THEN
UPDATE SET
Qty = SOURCE.Qty,
MUNICIPALITYCODE = SOURCE.MUNICIPALITYCODE
WHEN NOT MATCHED BY TARGET THEN
INSERT (MUNICIPALITYCODE, STREENAME, STREECODE)
VALUES (SOURCE.MUNICIPALITYCODE, SOURCE.STREETCODE, SOURCE.STREETCODE)
;
COMMIT;
GO

Related

MSSQL effective trigger INSTEAD OF INSERT

I have a case when using instead-of-insert trigger is necessary. My colleagues and I wonder which one is more effective (memory usage, time to run, etc.).
The trigger checks whether the record exists in table, if no inserts the new row, otherwise updates existing row by its key. The primary key in this example is composite key of (DocumentId, VatRate).
The first variant is with checking whether the record already exists:
CREATE TRIGGER docvatsum_trg
ON DocumentVatSummary
INSTEAD OF INSERT
AS
BEGIN
IF EXISTS (
SELECT 1 FROM DocumentVatSummary a
JOIN inserted b ON (a.DocumentId = b.DocumentId AND a.VatRate = b.VatRate)
)
BEGIN
UPDATE DocumentVatSummary
SET
DocumentVatSummary.VatBase = i.VatBase,
DocumentVatSummary.VatTotal = i.VatTotal
FROM inserted i
WHERE
DocumentVatSummary.DocumentId = i.DocumentId AND
DocumentVatSummary.VatRate = i.VatRate
END
ELSE
BEGIN
INSERT INTO DocumentVatSummary
SELECT * FROM inserted
END
END;
The second variant tries to insert and if insert fails an update follows:
CREATE TRIGGER docvatsum_trg
ON DocumentVatSummary
INSTEAD OF INSERT
AS
BEGIN
SAVE TRANSACTION savepoint
BEGIN TRY
INSERT INTO DocumentVatSummary
SELECT * FROM inserted
END TRY
BEGIN CATCH
IF XACT_STATE() = 1
BEGIN
ROLLBACK TRAN savepoint
UPDATE DocumentVatSummary
SET
DocumentVatSummary.VatBase = i.VatBase,
DocumentVatSummary.VatTotal = i.VatTotal
FROM inserted i
WHERE
DocumentVatSummary.DocumentId = i.DocumentId AND
DocumentVatSummary.VatRate = i.VatRate
END
END CATCH
END;
Note: Rollback to savepoint is required, because of TRY-CATCH implementation in running transaction in TSQL.
Which one is better and why? If you have better solution, please share.
Use MERGE in your trigger as explained here:
MERGE SYNTAX
Code Example:
DECLARE #SummaryOfChanges TABLE(Change VARCHAR(20));
MERGE INTO Sales.SalesReason AS Target
USING (VALUES ('Recommendation','Other'),
('Review', 'Marketing'),
('Internet', 'Promotion'))
AS Source (NewName, NewReasonType)
ON Target.Name = Source.NewName
WHEN MATCHED THEN
UPDATE SET ReasonType = Source.NewReasonType
WHEN NOT MATCHED BY TARGET THEN
INSERT (Name, ReasonType) VALUES (NewName, NewReasonType)
OUTPUT $action INTO #SummaryOfChanges;

Get Key values of the Row whose values are not Updated during Multiple row update

This is a Continuation of my previous question
sql update for dynamic row number
This time I am having an updated requirement.
I am having 2 tables
CraftTypes & EmployeeCraftTypes.
I need to update multiple rows in the CraftType Table and
I was able to update it as per the answer provided by TheGameiswar
Now there is a modification in the requirement.
In the table CraftTypes, there is a foreign key reference for the column CraftTypeKey with the table EmployeeCraftsTypes.
If there exist an entry for CraftTypeKey in the EmployeeCrafttypes table, then the row should not be updated.
Also the CraftTypeKey's whose row values are not updated must be obtained for returning the FK_restriction status of the rows.
This is the sql query I am using.
CREATE TYPE [DBO].[DEPARTMENTTABLETYPE] AS TABLE
( DepartmentTypeKey SMALLINT, DepartmentTypeName VARCHAR(50),DepartmentTypeCode VARCHAR(10) , DepartmentTypeDescription VARCHAR(128) )
ALTER PROCEDURE [dbo].[usp_UpdateDepartmentType]
#DEPARTMENTDETAILS [DBO].[DEPARTMENTTABLETYPE] READONLY
AS
BEGIN
SET NOCOUNT ON;
DECLARE #rowcount1 INT
BEGIN
BEGIN TRY
BEGIN TRANSACTION
UPDATE D1
SET
D1.[DepartmentTypeName]=D2.DepartmentTypeName
,D1.[DepartmentTypeCode]=D2.DepartmentTypeCode
,D1.[DepartmentTypeDescription]=D2.DepartmentTypeDescription
FROM
[dbo].[DepartmentTypes] D1
INNER JOIN
#DEPARTMENTDETAILS D2
ON
D1.DepartmentTypeKey=D2.DepartmentTypeKey
WHERE
D2.[DepartmentTypeKey] not in (select 1 from [dbo].[EmployeeDepartment] where [DepartmentTypeKey]=D2.DepartmentTypeKey)
SET #ROWCOUNT1=##ROWCOUNT
COMMIT
END TRY
BEGIN CATCH
SET #ROWCOUNT1=0
ROLLBACK TRAN
END CATCH
IF #rowcount1 =0
SELECT -174;
ELSE
SELECT 100;
END
END
Please Help
And Thanks in Advance
Ok
I think I figured out a way for it this time. I am not sure this is the right way, but its enough for me to meet the requirements.
I selected the distinct rows with FK reference from EmployeeCraftsTypes table as a second select query.
Now I can get the Row values which are not getting updated due to FK constraint.
This is the sql query I have used
ALTER PROCEDURE [dbo].[usp_UpdateCraftType]
#CRAFTDETAILS [DBO].[CRAFTTABLETYPE] READONLY
AS
BEGIN
SET NOCOUNT ON;
DECLARE #STATUSKEY TINYINT = (SELECT DBO.GETSTATUSKEY('ACTIVE'))
DECLARE #ROWCOUNT1 INT
BEGIN
BEGIN TRY
BEGIN TRANSACTION
UPDATE C1
SET
[C1].[CraftTypeName]=C2.CRAFTTYPENAME
,[C1].[CRAFTTYPEDESCRIPTION]=C2.CRAFTTYPEDESCRIPTION
,[C1].[StatusKey]=C2.[StatusKey]
FROM
[dbo].[CRAFTTYPES] C1
INNER JOIN
#CRAFTDETAILS C2
ON
C1.CRAFTTYPEKEY=C2.CRAFTTYPEKEY
WHERE
C2.[CRAFTTYPEKEY] NOT IN (SELECT EC.[CRAFTTYPEKEY] from [dbo].[EmployeeCrafts] EC where EC.[CRAFTTYPEKEY]=C2.[CRAFTTYPEKEY])
SET #ROWCOUNT1=##ROWCOUNT
COMMIT
END TRY
BEGIN CATCH
SET #ROWCOUNT1=0
ROLLBACK TRAN
END CATCH
--SET #ROWCOUNT1 = ##ROWCOUNT
IF #ROWCOUNT1 =0
SELECT -172;
ELSE
BEGIN
SELECT 100;
SELECT DISTINCT EC.[CRAFTTYPEKEY],'Value Already Assigned' as Reason
FROM [DBO].[EmployeeCrafts] EC
JOIN #CRAFTDETAILS C3
on C3.[CRAFTTYPEKEY]=EC.[CRAFTTYPEKEY]
WHERE EC.[CRAFTTYPEKEY]=C3.[CRAFTTYPEKEY]
END
END
END
Now in the Web API side I can check if there is any update failure by checking the rowcount for the second table.
If the row count is more than 0, then update error message can be generated
Hope it will be helpful to someone ....

T-SQL Trigger - prevent duplicate, but not always

I would like to avoid INSERTS and UPDATES which could duplicate field's value, but not always.
I have a varchar field Cat_Catalog. in table Catalog.
I can have two rows with Cat_Catalog's value "123" duplicated, but I cannot have duplicated field Cat_Catalog which starts with 'KAT' word (so I cannot have 2 rows with "KAT123" Cat_Catalog's value)
The following trigger i made doesn't work fine because field that's going to be updated starts with KAT trigger always raise error (variable #IfExist always return true - it is probably because of AFTER UPDATE,INSERT syntax).
I would like to avoid using INSTEAD OF syntax because updates are generated by some API which to i have no documentation and I'm not sure what to do in case when value doesn't starts with 'KAT'.
GO
/****** Object: Trigger [dbo].[Catalog_InsertUpdateCatalog] ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
--If first three characters are 'KAT'
--then check for duplicate and raiseerror
ALTER TRIGGER [dbo].[Catalog_InsertUpdateCatalog] ON [dbo].[Catalog]
FOR UPDATE,INSERT
AS
set nocount on;
DECLARE #CatalogInsert varchar(50)
DECLARE #IfKat varchar(10) = 'FALSE'
DECLARE #IfExist varchar(10) = 'FALSE'
SELECT #CatalogInsert = Cat_Catalog
FROM inserted
--Does It starts with 'KAT' ?
IF (#CatalogInsert like 'KAT%')
BEGIN
SET #IfKat = 'TRUE'
END
--Check for Duplicate
IF EXISTS(
Select * from Test.dbo.Catalog t
where t.Cat_Catalog = #CatalogInsert
)
BEGIN
SET #IfExist = 'TRUE'
END
IF ( #IfExist = 'TRUE' and #IfKat = 'TRUE' )
BEGIN
RAISERROR ('Catalog allready exists: %s , ISKAT:%s , EXIST:%s', 16, 1, #CatalogInsert, #IfKat, #IfExist);
END
`
The problem is that I don't know how can I check the current value to be updated allready exists in Catalog table (check must be done before update).
Using CHECK constraint instead of a trigger would be a better solution, since triggers are executed much later in the transaction and eventual rollback could be expensive. You can define check constraint as follows:
CREATE FUNCTION IsDuplicate(#col varchar(50))
RETURNS BIT
AS
BEGIN
IF CHARINDEX('KAT', #col) = 1 AND (SELECT COUNT(*) FROM [Catalog] WHERE [Cat_Catalog] = #col) > 1
return 1;
return 0;
END;
GO
ALTER TABLE [Catalog]
ADD CONSTRAINT chkForDuplicates CHECK (dbo.IsDuplicate([Cat_Catalog]) = 0)
GO
Have in mind that if you already have duplicate "KATxxx" values in the table you'll have to either delete them or create the constraint with NOCHECK clause.
Please, try this new version:
ALTER TRIGGER [dbo].[Catalog_InsertUpdateCatalog] ON [dbo].[Catalog]
FOR UPDATE,INSERT
AS BEGIN
set nocount on;
--Check for Duplicate
IF EXISTS(
Select 1
From (
-- Updated
SELECT
COUNT(*) OVER (PARTITION BY Cat_Catalog) Cnt,
Cat_Catalog
FROM dbo.Catalog
WHERE
Cat_Catalog LIKE 'KAT%'
) t
Join inserted i ON t.Cat_Catalog = i.Cat_Catalog AND Cnt > 1
)
BEGIN
RAISERROR ('Catalog allready exists!', 16, 1);
ROLLBACK TRANSACTION;
END
END
This trigger check the existence of rows with the same [Cat_Catalog] field as in the inserted (or updated) rows. If duplicated rows exists and [Cat_Catalog] starts with 'KAT' trigger RAISE error + rollback transaction.
UPDATED: I change trigger. It should now work correctly (i test it). Trigger FOR UPDATE, INSERT fires after changes take place in table. So we need check duplicate rows in the table.
I do it throught COUNT(*) OVER(PARTITION BY Cat_Catalog) but you may check it in a more familiar way:
SELECT
COUNT(*) Cnt,
Cat_Catalog
FROM
dbo._Catalog
WHERE
Cat_Catalog LIKE 'KAT%'
GROUP BY
Cat_Catalog
Use Instead OF Trigger instead After Trigger.
If your problem with Instead OF Triggers, then you need to use NOT Eqals operator to avoid such wrong indication to passed. I supposed to say
DECLARE #CATALOG_PK BIGINT;
SELECT #CATALOG_PK = CATALOG_PK FROM INSERTED
--Check for Duplicate
IF EXISTS(
Select * from Test.dbo.Catalog t
where t.Cat_Catalog = #CatalogInsert
AND CATALOG_PK <>#CATELOG_PK LIKE t.CATALOG LIKE 'KAT%'
)
BEGIN
SET #IfExist = 'TRUE'
END
However this will fail in-case of multiple UPDATES or INSERTS done.
Try these two triggers:
CREATE TRIGGER [MyCatalog_InsertCatalog] ON [dbo].[MyCatalog]
FOR INSERT
AS
IF EXISTS(
SELECT I.*
FROM Inserted I
INNER JOIN MyCatalog M
ON M.Cat_Catalog = I.Cat_Catalog
WHERE I.Cat_Catalog LIKE 'KAT%' )
BEGIN
RAISERROR ('Insert Catalog already exists: ', 16, 1);
ROLLBACK TRANSACTION;
END
GO
CREATE TRIGGER [MyCatalog_UpdateCatalog] ON [dbo].[MyCatalog]
FOR UPDATE
AS
IF EXISTS(
SELECT I.*
FROM Inserted I
INNER JOIN MyCatalog M
ON M.Cat_Catalog = I.Cat_Catalog
INNER JOIN deleted d
ON d.Cat_Id = i.Cat_Id
WHERE I.Cat_Catalog LIKE 'KAT%' AND d.Cat_Catalog <> i.Cat_Catalog)
BEGIN
RAISERROR ('Update Catalog already exists: ', 16, 1);
ROLLBACK TRANSACTION;
END
EDIT
You could combine both triggers into one:
CREATE TRIGGER [MyCatalog_InsertUpdateCatalog] ON [dbo].[MyCatalog]
FOR INSERT, UPDATE
AS
IF EXISTS(
SELECT I.*
FROM Inserted I
INNER JOIN MyCatalog M
ON M.Cat_Catalog = I.Cat_Catalog
INNER JOIN deleted d
ON d.Cat_Id = i.Cat_Id
WHERE I.Cat_Catalog LIKE 'KAT%' AND d.Cat_Catalog <> i.Cat_Catalog)
BEGIN
RAISERROR ('Update Catalog already exists: ', 16, 1);
ROLLBACK TRANSACTION;
END
ELSE BEGIN
IF EXISTS(
SELECT I.*
FROM Inserted I
INNER JOIN MyCatalog M
ON M.Cat_Catalog = I.Cat_Catalog
WHERE I.Cat_Catalog LIKE 'KAT%' )
BEGIN
RAISERROR ('Insert Catalog already exists: ', 16, 1);
ROLLBACK TRANSACTION;
END
END

T-SQL trigger that checks if airplane seats are taken

I need to make a trigger that checks if an airplane seat is taken before a customer can be inserted into the table.
I have the following Trigger so far:
CREATE TRIGGER CheckIfSeatIsUnique
ON PassagierVoorVlucht
AFTER insert, update
AS
BEGIN
IF ##ROWCOUNT = 0 RETURN
SET NOCOUNT ON
BEGIN TRY
IF EXISTS (SELECT 1 FROM PassagierVoorVlucht P Join inserted I on P.vluchtnummer=i.vluchtnummer Where P.stoel = I.stoel)
BEGIN
RAISERROR('The chosen seat is taken', 16, 1)
END
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
ROLLBACK TRANSACTION
DECLARE #ErrorMessage NVARCHAR(4000) = ERROR_MESSAGE()
DECLARE #ErrorSeverity INT = ERROR_SEVERITY()
DECLARE #ErrorState INT = ERROR_STATE()
RAISERROR (#ErrorMessage, #ErrorSeverity, #ErrorState)
END CATCH
END
The problem I have, is that the trigger checks if the seat is taken AFTER the insert was done, So the seat will always be taken no matter what.
Is there some way to check if the seat is taken before the insert is done?
Edit: It must also be possible to enter NULL on seat, because the seatnumber isn't known till a few days before the flight
If you have a unique identifier on the table, you can join it into the EXISTS() query to filter out any records that were attempted to insert.
The fiddle below examples this, though it assumes you're taking care of an null handling you need to outside of this.
http://sqlfiddle.com/#!6/93a8a
CREATE TRIGGER CheckIfUnique_mydata_value ON dbo.data
AFTER insert, update
AS
BEGIN
--check if we passed multiple values
IF EXISTS(SELECT 1 FROM inserted GROUP BY value HAVING COUNT(*) > 1)
BEGIN
RAISERROR('You tried to insert a duplicate value within the result set. Ensure you only pass unique values!', 16,1)
END
--check if we inserted a value that already exists that is not me (works for updates on me too!)
IF EXISTS(SELECT 1 FROM dbo.data m INNER JOIN inserted i ON m.value = i.value AND m.id <> i.id)
BEGIN
RAISERROR('Duplicate Value found',16,1)
END
END;

Select / Insert version of an Upsert: is there a design pattern for high concurrency?

I want to do the SELECT / INSERT version of an UPSERT. Below is a template of the existing code:
// CREATE TABLE Table (RowID INT NOT NULL IDENTITY(1,1), RowValue VARCHAR(50))
IF NOT EXISTS (SELECT * FROM Table WHERE RowValue = #VALUE)
BEGIN
INSERT Table VALUES (#Value)
SELECT #id = SCOPEIDENTITY()
END
ELSE
SELECT #id = RowID FROM Table WHERE RowValue = #VALUE)
The query will be called from many concurrent sessions. My performance tests show that it will consistently throw primary key violations under a specific load.
Is there a high-concurrency method for this query that will allow it to maintain performance while still avoiding the insertion of data that already exists?
You can use LOCKs to make things SERIALIZABLE but this reduces concurrency. Why not try the common condition first ("mostly insert or mostly select") followed by safe handling of "remedial" action? That is, the "JFDI" pattern...
Mostly INSERTs expected (ball park 70-80%+):
Just try to insert. If it fails, the row has already been created. No need to worry about concurrency because the TRY/CATCH deals with duplicates for you.
BEGIN TRY
INSERT Table VALUES (#Value)
SELECT #id = SCOPE_IDENTITY()
END TRY
BEGIN CATCH
IF ERROR_NUMBER() <> 2627
RAISERROR etc
ELSE -- only error was a dupe insert so must already have a row to select
SELECT #id = RowID FROM Table WHERE RowValue = #VALUE
END CATCH
Mostly SELECTs:
Similar, but try to get data first. No data = INSERT needed. Again, if 2 concurrent calls try to INSERT because they both found the row missing the TRY/CATCH handles.
BEGIN TRY
SELECT #id = RowID FROM Table WHERE RowValue = #VALUE
IF ##ROWCOUNT = 0
BEGIN
INSERT Table VALUES (#Value)
SELECT #id = SCOPE_IDENTITY()
END
END TRY
BEGIN CATCH
IF ERROR_NUMBER() <> 2627
RAISERROR etc
ELSE
SELECT #id = RowID FROM Table WHERE RowValue = #VALUE
END CATCH
The 2nd one appear to repeat itself, but it's highly concurrent. Locks would achieve the same but at the expense of concurrency...
Edit:
Why not to use MERGE...
If you use the OUTPUT clause it will only return what is updated. So you need a dummy UPDATE to generate the INSERTED table for the OUTPUT clause. If you have to do dummy updates with many calls (as implied by OP) that is a lot of log writes just to be able to use MERGE.
// CREATE TABLE Table (RowID INT NOT NULL IDENTITY(1,1), RowValue VARCHAR(50))
-- be sure to have a non-clustered unique index on RowValue and RowID as your clustered index.
IF EXISTS (SELECT * FROM Table WHERE RowValue = #VALUE)
SELECT #id = RowID FROM Table WHERE RowValue = #VALUE
ELSE BEGIN
INSERT Table VALUES (#Value)
SELECT #id = SCOPEIDENTITY()
END
As always, gbn's answer is correct and ultimately lead me to where I needed to be. However, I found a particular edge case that wasn't covered by his approach. That being a 2601 error which identifies a Unique Index Violation.
To compensate for this, I've modified his code as follow
...
declare #errornumber int = ERROR_NUMBER()
if #errornumber <> 2627 and #errornumber <> 2601
...
Hopefully this helps someone!

Resources