Trigger handle multiple inserts - sql-server

I've got a trigger:
CREATE TRIGGER tgr_incheck_vlucht
ON PassagierVoorVlucht
AFTER INSERT, UPDATE
AS
BEGIN
IF ##ROWCOUNT= 0 BEGIN RETURN END
SET NOCOUNT ON
BEGIN TRY
IF EXISTS
(SELECT *
FROM inserted I
WHERE EXISTS(SELECT *
FROM PassagierVoorVlucht P inner join Vlucht V on P.vluchtnummer = V.vluchtnummer
WHERE I.inchecktijdstip >= vertrektijdstip))
BEGIN
RAISERROR('Inchecktijdstip moet voor de aankomsttijdliggen', 16,1)
END
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0 BEGIN ROLLBACK TRANSACTION END
DECLARE #ErrorMessage NVARCHAR(4000) = ERROR_MESSAGE(),
#errorSeverity INT = ERROR_SEVERITY(),
#errorState INT = ERROR_STATE()
RAISERROR (#ErrorMessage, #errorSeverity, #errorState)
END CATCH
END
now i have written some test statements:
INSERT INTO PassagierVoorVlucht
VALUES(850, 5316, 1, '2002-01-01 13:37:00.000', 21),
(1002, 5316, 1, '2004-01-01 13:37:00.000', 21),
(1601, 5316, 1, '2004-05-01 13:37:00.000', 21),
(1602, 5316, 1, '2004-05-01 13:37:00.000', 21)
the trigger works for only ONE insert row at the time not for the whole block. How can i write the trigger that it can handle multiple inserts?

The answer is you cannot... Fundamentally, triggers are fired for each insertion, not a batch of code. This is why it is called a trigger, or a response.
Furthermore, if your table had an Insert Identity column, triggers will not prevent gaps in the rows since they function just like sequences are: activated at the row level and after the statement completes (unless INSTEAD OF).
Perhaps helpful in explaining triggers is the following from MSDN:
CREATE TRIGGER (Transact-SQL)
Although a TRUNCATE TABLE statement is in effect a DELETE statement, it does not activate a trigger because the operation does not log individual row deletions. However, only those users with permissions to execute a TRUNCATE TABLE statement need be concerned about inadvertently circumventing a DELETE trigger this way.
Notice the part about individual row deletions as well as the point about scope. Perhaps understanding more what your requirements are might make the solution clear.
Now, you may use an INSTEAD OF trigger to fire before the attempted code executes, but that likely is not going to be a solution, unless you had some staging table setup or similar for it to function. Instead of replaces the entire DML or DDL statement.
If you wish to protect the integrity of your data consider the use of stored or pre-planned procedures.
If that is unacceptable or impractical, other constraints like referential keys or even staging tables (depending on the need for the timeliness of data) which can later be imported could be options.

To do this you need to use a temporary table inside your trigger and store all the inserted data in it and then loop that table using a while loop.
here is an example where we want to add all the inserted users into a audit table using a trigger for insert
alter trigger tr_userData_forInsert
on userData
for insert
AS
BEGIN
declare #id int, #name varchar(20)
select *
into #inserted_data
from inserted
while ( exists(select id from #inserted_data) )
begin
select top 1 #id = id, #name = full_name
from #inserted_data
insert into loggs
values ( concat( 'a user with ',#id, ' and name of ', #name
, ' is added at ', getdate() ) )
delete from #inserted_data
where id = #id
end
end
I already used this in more than one situation and it work perfect

Related

"With Recompile" For "Trigger"

Why not exists recompile option for trigger?
Suddenly the performance of one of our procedure (multiple SELECTs, multiple tables, insert into table) went from returning data in around 1 secs to 10-30secs.
After adding various debugging and logging we noticed that the performance would increase from the slow 10-30secs, back to sub-second speeds. (because alter trigger one of the table)
Just to clarify. The sequence of events:
Slow performance of Insert
Alter trigger table
Fast performance of Insert
I think slow performance associated with create wrong plan cash. because, before call insert command on the procedure, I write print datetime and the beginning of the the trigger command, add print datetime, so when call the procedure before alter trigger, The time difference between the first print and the second print is 20 sec, but when alter trigger, back to sub-second speeds. It should be noted that the commands in the trigger are not complicated
so, I need to add recompile option to trigger like procedure
it is trigger Script sample:
create trigger t_test on tbl AFTER insert
as
begin
begin try
declare #yearid int,
#id int
select #id = id,#yearid = yearid
from inserted
if exists(select * from FinancialYear where id = #yearid and flag = 0)
begin
raiserror('year not correct',16,1)
end
DECLARE #PublicNo BIGINT=(SELECT ISNULL(MAX(PublicNo),0)+1 FROM tbl)
update tbl
set PublicNo = #PublicNo
where #id
insert into tbl2
values (...)
end try
begin catch
print error_message()
end catch
end

SQL Server SCOPE_IDENTITY() vs ##IDENTITY

Would there be any benefit of not using SCOPE_IDENTITY() and switching to ##IDENTITY? For the area I'm talking about is part of an install script that sets up a database for our customers. It's inserting a record in one table and using the identifier key from that table and inserting it into a foreign key into another. We are doing this twice.
We seem to have a rare condition in which the 2nd time this happens, we are inserting the id from the first insert into the 2nd table for both passes, causing issues with the data. There is a chance that something else altogether is causing this, but my lead seemed to zeroed in on the SCOPE_IDENTITY() as possibly being the culprit.
Declare #TheId int
Insert into dbo.TableName (Name) Values ('xxxx')
Select #TheId = SCOPE_IDENTITY()
-- some code here that uses #TheId
-- ...
Insert into dbo.TableName (Name) Values ('yyyy')
Select #TheId = SCOPE_IDENTITY()
-- some code here that uses #TheId
-- at this point, we may have the condition that SCOPE_IDENTITY() still has the value before that 2nd insert...
The only way scope_identity() could have the prior id value in this context is if the INSERT statement does not create any rows. In that situation, ##IDENTITY isn't gonna fix anything. In fact, ##IDENTITY is less specific, and therefore could only hope to make things worse.
What you can do is use a different variable for the second insert. Or, you could set #TheId back to NULL before the second insert runs. In this way, you'll be able to tell if something went wrong. ##rowcount is also useful for this.
I did see this in the comments:
"The second insert did not fail as the record was found in the database."
I put it to you perhaps the record was already in the database, before the code ran. Moreover, if there is a constraint on the table this could be the reason why the insert fails.
Within the scope of the proc or script the #TheId created by the first insert is not same object as the #TheId created by the second insert. While it's possible to reuse variables it's not a good practice imo when it comes to multiple DML statements within a code block. In this script I add TRY/CATCH and SET XACT_ABORT ON to ensure a complete rollback of all DML statements within the block.
Something like this
set nocount on;
set xact_abort on;
begin transaction
begin try
Insert into dbo.TableName (Name) Values ('xxxx');
if ##rowcount=1
begin
Declare #Id1 int = SCOPE_IDENTITY();
-- some code here that uses #Id1
-- ...
end
else
throw 50000, 'The first insert failed', 1;
Insert into dbo.TableName (Name) Values ('yyyy');
if ##rowcount=1
begin
Declare #Id2 int = SCOPE_IDENTITY();
-- some code here that uses #Id2
-- ...
end
else
throw 50000, 'The second insert failed', 1;
commit transaction
end try
begin catch
/* put error handling here */
rollback transaction
end catch
Thanks everyone for the help. We will likely go with creating a new variable for the 2nd insert.

How to check if sp_rename is done successfully?

I am running the following query:
SELECT * INTO dbo.2015_10_2_cs FROM dbo.2015_10_2
IF NOT EXISTS
(SELECT type FROM sys.indexes WHERE object_id = object_id('dbo.2015_10_2_cs')
AND NAME ='cci' AND type = 5)
BEGIN
CREATE CLUSTERED COLUMNSTORE INDEX cci
ON dbo.2015_10_2_cs
DROP TABLE dbo.2015_10_2
EXEC sp_rename "dbo.2015_10_2_cs" , "dbo.2015_10_2"
END
and I want to make sure that the part where I am renaming the table dbo.2015_10_2_cs to dbo.2015_10_2 is done successfully (without losing any data).
The step inside the loop should be surrounded with SQL transaction to keep the process safe and reliable (in case if any step will fail).
Could anyone help with this? Thanks in advance.
EXEC sp_rename "dbo.2015_10_2_cs" , "dbo.2015_10_2"
This will not do what you expect. The new table will be named [dbo].[dbo.2015_10_2] if you specify the schema name in the new table name. Renamed tables are implicitly in the existing table's schema since one must use ALTER SCHEMA instead of sp_rename to move an object between schemas.
There are a number of other problems with your script. Because the table name starts with a number, it doesn't conform to regular identifier naming rules and must be enclosed in square brackets or double quotes. The literal parameters passed to sp_rename should be single quotes. You can also check to stored procedure return code to ascertain success or failure. The example below performs these tasks in a transaction with structured error handling.
DECLARE #rc int;
BEGIN TRY
BEGIN TRAN;
IF NOT EXISTS
(SELECT type FROM sys.indexes WHERE object_id = object_id(N'dbo.2015_10_2_cs')
AND NAME ='cci' AND type = 5)
BEGIN
CREATE CLUSTERED COLUMNSTORE INDEX cci
ON dbo.[2015_10_2_cs];
DROP TABLE dbo.[2015_10_2];
EXEC #rc = sp_rename 'dbo.[2015_10_2_cs]' , '2015_10_2';
IF #rc <> 0
BEGIN
RAISERROR('sp_rename returned return code %d',16,1);
END;
END;
COMMIT;
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0 ROLLBACK;
THROW;
END CATCH;
You can use an EXISTS checking for the tablename and schema.
IF NOT EXISTS (SELECT 'table does not exist' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = N'2015_10_2'AND TABLE_SCHEMA = 'dbo')
BEGIN
RAISERROR('The table doesn''t exist!!!!', 16, 1)
END
sp_rename won't make you lose table contents, it will just change the table reference name and update all it's contraints and indexes references. It will also raise an error if the table to rename does not exist. Maybe what you want is to wrap your process in a transaction and rollback if something fails.
EDIT:
For basic transaction handling you can use the following. Please read the documentation for using transaction, it might take a while to know how it works correctly.
IF OBJECT_ID('tempdb..#Test') IS NOT NULL
DROP TABLE #Test
CREATE TABLE #Test (Number INT)
SELECT AmountRecords = COUNT(1) FROM #Test -- AmountRecords = 0
BEGIN TRY
BEGIN TRANSACTION
-- Do your statements here
INSERT INTO #Test (Number)
VALUES (1)
DECLARE #errorVariable INT = CONVERT(INT, 'NotAnInteger!!') -- Example of error: can't convert
COMMIT
END TRY
BEGIN CATCH -- If something goes wrong
IF ##TRANCOUNT > 0 -- ... and transaction is still open
ROLLBACK -- Revert statements from the BEGIN TRANSACTION onwards
END CATCH
SELECT AmountRecords = COUNT(1) FROM #Test -- AmountRecords = 0 (the transaction was rolled back and the INSERT reverted)
Basically you use BEGIN TRANSACTION to initiate a restore point to go back to if something fails. Then use a COMMIT once you know everything is OK (from that point onwards, other users will see the changes and modifications will be persisted). If something fails (you need TRY/CATCH block to handle errors) you can issue a ROLLBACK to revert your changes.

SQL Server error handling in cursor inside trigger

I'm new to SQL Server error handling, and my English isn't too clear, so I apologize in advance for any misuderstandings.
The problem is: I insert multiple records into a table. The table has an AFTER INSERT trigger, which is processing the records one by one in the FETCH WHILE cycle with a cursor. If something error happens, everything is rolling back. So if there is just one wrong field in the inserted records, I lost all of them. And the insert rolls back also, so I can't find the wrong record. So I need to handle the errors inside of the cursor, to rollback only the wrong record.
I made a test database with 3 tables:
tA
VarSmallint smallint
VarTinyint tinyint
String varchar(20)
tB
ID int (PK, identity)
Timestamp datetime (default: getdate())
VarSmallint smallint
VarTinyint tinyint
String varchar(20)
tC
ID int PK
Timestamp datetime
VarTinyint1 tinyint
VarTinyint2 tinyint
String varchar(10)
tA contains 3 records with 1 wrong one. I insert this content into tB.
tB has the trigger, and inserts the records into tC ony by one.
tC has only tinyint variables, so there can be problem to insert values greater than 255. This is the point where the error occurs for the test!
My trigger is:
ALTER TRIGGER [dbo].[trg_tB]
ON [dbo].[tB]
AFTER INSERT
AS
BEGIN
IF ##rowcount = 0
RETURN;
SET NOCOUNT ON;
DECLARE
#ID AS int,
#Timestamp AS datetime,
#VarSmallint AS smallint,
#VarTinyint AS tinyint,
#String AS varchar(20),
DECLARE curNyers CURSOR DYNAMIC
FOR
SELECT
[ID], [Timestamp], [VarSmallint], [VarTinyint], [String]
FROM INSERTED
ORDER BY [ID]
OPEN curNyers
FETCH NEXT FROM curNyers INTO #ID, #Timestamp, #VarSmallint, #VarTinyint, #String
WHILE ##FETCH_STATUS = 0
BEGIN
BEGIN TRY
BEGIN TRAN
INSERT INTO [dbo].[tC]([ID], [Timestamp], [VarTinyint1], [VarTinyint2], [String])
VALUES (#ID, #Timestamp, #VarSmallint, #VarTinyint, #String)
COMMIT TRAN
END TRY
BEGIN CATCH
ROLLBACK TRAN
INSERT INTO [dbo].[tErrorLog]([ErrorTime], [UserName], [ErrorNumber],
[ErrorSeverity], [ErrorState],
[ErrorProcedure], [ErrorLine],
[ErrorMessage], [RecordID])
VALUES (SYSDATETIME(), SUSER_NAME(), ERROR_NUMBER(),
ERROR_SEVERITY(), ERROR_STATE(),
ERROR_PROCEDURE(), ERROR_LINE(),
ERROR_MESSAGE(), #ID)
END CATCH
FETCH NEXT FROM curNyers INTO #ID, #Timestamp, #VarSmallint, #VarTinyint, #String
END
CLOSE curNyers
DEALLOCATE curNyers
END
If I insert 2 good records with 1 wrong, everything is rolling back and I got an error:
Msg 3609, Level 16, State 1, Line 1
The transaction ended in the trigger. The batch has been aborted.
Please help me! How to modify this trigger to work well?
If I insert the wrong record, I need:
All the inserted records in tB
All the good records in tC
Error logged in tErrorLog
Thanks!
You have TWO major disasters in your trigger:
do not use a cursor inside a trigger - that's just horrible! Triggers fire whenever a given operation happens - you have little control over when and how many times they fire. Therefore, in order not to compromise your system performance too much, triggers should be very small, fast, nimble - do not do any heavy lifting and extensive processing in a trigger. A cursor is anything but nimble and fast - it's a resource-hog, processor-hog, memory-leaking monster - AVOID those whenever you can, and most definitely inside a trigger! (and you don't need them, 99% of the cases, anyway)
You can rewrite your whole logic into this one single, fast, set-based statement:
ALTER TRIGGER [dbo].[trg_tB]
ON [dbo].[tB]
AFTER INSERT
AS
BEGIN
INSERT INTO [dbo].[tC]([ID], [Timestamp], [VarTinyint1], [VarTinyint2], [String])
SELECT
[ID], [Timestamp], [VarSmallint], [VarTinyint], [String]
FROM
INSERTED
END
Never call COMMIT TRAN inside a trigger. The trigger executes inside the context and transaction of the statement that caused it to fire - if everything is OK, just let the trigger finish and then the transaction will be committed just fine. If you need to abort, call ROLLBACK. But never ever call COMMIT TRAN in the middle of a trigger. Just don't.....
I deleted the TRIGGER, and copy-pasted the code from it into a STORED PROCEDURE.
Then I added a row Status to tB, and set defaults to 0.
1 is "Record processed OK", 2 is "Record processing fault".
I fill the cursor with WHERE Status = 0.
In the TRY section I update the status to 1, in the CATCH section I UPDATE it to 2.
I have no jobs, so I run the SP from Windows scheduler with a batch file with the SQLCMD command.
Now the processing works well, moreover it worked well for the first time. Thanks for the help!

Rollback Transaction when concurrency check fails

I have a stored procedure which does a lot of probing of the database to determine if some records should be updated
Each record (Order) has a TIMESTAMP called [RowVersion]
I store the candidate record ids and RowVersions in a temporary table called #Ids
DECLARE #Ids TABLE (id int, [RowVersion] Binary(8))
I get the count of candidates with the the following
DECLARE #FoundCount int
SELECT #FoundCount = COUNT(*) FROM #Ids
Since records may change from when i SELECT to when i eventually try to UPDATE, i need a way to check concurrency and ROLLBACK TRANSACTION if that check fails
What i have so far
BEGIN TRANSACTION
-- create new combinable order group
INSERT INTO CombinableOrders DEFAULT VALUES
-- update orders found into new group
UPDATE Orders
SET Orders.CombinableOrder_Id = SCOPE_IDENTITY()
FROM Orders AS Orders
INNER JOIN #Ids AS Ids
ON Orders.Id = Ids.Id
AND Orders.[RowVersion] = Ids.[RowVersion]
-- if the rows updated dosnt match the rows found, then there must be a concurrecy issue, roll back
IF (##ROWCOUNT != #FoundCount)
BEGIN
ROLLBACK TRANSACTION
set #Updated = -1
END
ELSE
COMMIT
From the above, i'm filtering the UPDATE with the stored [RowVersion] this will skip any records that have since been changed (hopefully)
However i'm not quite sure if i'm using transactions or optimistic concurrency in regards to TIMESTAMP correctly, or if there are better ways to achieve my desired goals
It's difficult to understand what logic you are trying to implement.
But, if you absolutely must perform several non-atomic actions in a procedure and make sure that the whole block of code is not executed again while it is running (for example, by another user), consider using sp_getapplock.
Places a lock on an application resource.
Your procedure may look similar to this:
CREATE PROCEDURE [dbo].[YourProcedure]
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
BEGIN TRANSACTION;
BEGIN TRY
DECLARE #VarLockResult int;
EXEC #VarLockResult = sp_getapplock
#Resource = 'UniqueStringFor_app_lock',
#LockMode = 'Exclusive',
#LockOwner = 'Transaction',
#LockTimeout = 60000,
#DbPrincipal = 'public';
IF #VarLockResult >= 0
BEGIN
-- Acquired the lock
-- perform your complex processing
-- populate table with IDs
-- update other tables using IDs
-- ...
END;
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION;
END CATCH;
END
When you SELECT the data, try using HOLDLOCK and UPDLOCK while inside of an explicit transaction. It's going to mess with the concurrency of OTHER transactions but not yours.
http://msdn.microsoft.com/en-us/library/ms187373.aspx

Resources