SQL Transactions with nolocks and TRANSACTION ISOLATION LEVEL READ COMMITTED - sql

I have a stored procedure like this.
CREATE PROCEDURE [dbo].[mysp]
AS
SET NOCOUNT ON
SET TRANSACTION ISOLATION LEVEL READ COMMITTED
BEGIN
BEGIN TRAN
DECLARE #TrackingCode INT
SELECT #TrackingCode = DefaultsData
FROM dbo.Defaults
WHERE DefaultsID=77
UPDATE dbo.Defaults
SET DefaultsData = #PassedTrackingCode+1
WHERE DefaultsID=77
SELECT #TrackingCode
COMMIT TRAN
END
Assuming that we execute this stored procedure at the same time (concurrently) twice, what will be the #TrackingCode value that is returned at both the time. What if I used NOLOCK on the SELECT statement in the stored proc.

1) If your intention is to generate unique tracking codes then it's a bad idea. You could do this simple test:
CREATE TABLE dbo.Defaults (
DefaultsData INT,
DefaultsID INT PRIMARY KEY
);
GO
INSERT INTO dbo.Defaults VALUES (21, 77);
GO
CREATE PROCEDURE [dbo].[mysp]
AS
SET NOCOUNT ON
SET TRANSACTION ISOLATION LEVEL READ COMMITTED
BEGIN
BEGIN TRAN
DECLARE #TrackingCode INT
SELECT #TrackingCode = DefaultsData
FROM dbo.Defaults
WHERE DefaultsID=77
WAITFOR DELAY '00:00:05' -- 5 seconds delay
UPDATE dbo.Defaults
SET DefaultsData = #TrackingCode+1
WHERE DefaultsID=77
SELECT #TrackingCode -- Maybe it should return the new code
COMMIT TRAN
END;
GO
and then open a new window in SQL Server Management Studio (Ctrl + N) and execute (F5)
EXEC [dbo].[mysp]
and also open a second new window and execute (in less than 5 seconds)
EXEC [dbo].[mysp]
In this case, you will get the same value. NOLOCK is a bad idea (generally speaking) and in this case doesn't help you.
2) If (I repeat my self - sorry) your intention is to generate unique tracking codes then you could use
ALTER PROCEDURE [dbo].[mysp]
#TrackingCode INT OUTPUT
AS
BEGIN
SET NOCOUNT ON;
UPDATE dbo.Defaults
SET #TrackingCode = DefaultsData = DefaultsData + 1 -- It generate the new code
WHERE DefaultsID=77;
END;
GO
or you could use sequences (SQL Server 2012+).

Related

Is transaction isolation level 100% reliable in SQL Server?

I'm doing some test about the isolation level in SQL Server.
First I create a table called test with this structure:
Then I run the test code with 2 threads at the same time:
use test-db;
go
declare #count int = 0;
while #count<5000
begin
set transaction isolation level read committed;
begin transaction;
declare #max int;
select #max = coalesce(max(sequence_no),0) from test;
print #max;
insert into test (prefix, sequence_no, thread) values ('AAA', #max+1, 1);
commit transaction;
set #count = #count+1;
end;
The second thread just change the thread number:
use test-db;
go
declare #count int = 0;
while #count<5000
begin
set transaction isolation level read committed;
begin transaction;
declare #max int;
select #max = coalesce(max(sequence_no),0) from test;
print #max;
insert into test (prefix, sequence_no, thread) values ('AAA', #max+1, 2);
commit transaction;
set #count = #count+1;
end;
Under read committed mode, the code should wait for read the max number of sequence when the other thread is not commit, which means they won't generate a same sequence_no.
But it often give me a error:
Violation of PRIMARY KEY constraint 'PK_test'. Cannot insert duplicate key in object 'dbo.test-db'. The duplicate key value is (AAA, 2402).
I test it in repeatable read mode again, and it's the same.
Can someone explain why the reading query between transaction will get crash?
Does it means that isolation level isn't 100% reliable?
You need to use SERIALIZABLE isolation and an UPDLOCK is also required to avoid deadlocks:
SELECT #max = coalesce(max(sequence_no),0)
FROM dbo.test WITH (UPDLOCK, SERIALIZABLE);
This is awful for concurrency so you should really use an identity or a sequence.

I have a trigger to execute a SP when one column is updated. If I add an insert operation it runs the SP for any changes to the table

I am looking to get the SP run only when the Instructions column has something added or altered.
This code works for updating of the Instructions column
AFTER Update
AS BEGIN
SET NOCOUNT on
if update ([Instructions])
BEGIN
DECLARE #ID INT
SELECT #ID = (SELECT [ID] FROM inserted)
EXEC [dbo].[Gem_AddNoteToDelivery] #ID
END
END
GO
ALTER TABLE [dbo].[DeliveryNote] ENABLE TRIGGER [Gemini_DeliveryNote_AddNote]
GO
My Stored Procedure is this
ALTER PROCEDURE [dbo].[Gem_AddNoteToDelivery]
#ID INT output
AS
BEGIN
SET NOCOUNT ON;
DECLARE #DInst VARCHAR (4000)
DECLARE #DELNN VARCHAR (32)
Select #Dinst = [Instructions],
#DELNN = [DelNoteNumber]
FROM [dbo].[DeliveryNote] WHERE [id]=#ID
INSERT INTO [dbo].[Notes]
--([UserID],[OperatorID],[UserName],[DateDB],[ModuleType],[RecordID],[RecordNo],[Flags],[Details],[Priority],[NotesType])
VALUES
(115,0,'Automation',getdate(),4,#ID,#DELNN,
0x0000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000,#DInst,1,Null)
END
GO
If I alter the Trigger to read
AFTER Update,Insert
then the SP runs for updates to all columns not just the Instructions one.
The database is not mine so I am unable to alter tables.
Any help most appreciated.

How to delete an hierarchy tree in SQL

I have built an hierarchy and I'm able to insert, move, but I having problems with deleting.
I created this stored procedure to delete an Hierarchy Tree.
I call the procedure using exec [tag].[usp_TagDeleteHierarchyTree] 45 but the records are being displayed instead of being deleted.
Sample data
In the messages I get:
CREATE PROCEDURE [tag].[usp_TagDeleteHierarchyTree]
#TagId float
AS
BEGIN
SET NOCOUNT ON;
DECLARE #ParentNode hierarchyid
Begin Try
Begin Transaction
-- Parent node
Select #ParentNode = [Node] From [tag].[Process] Where [Tag_Id] = #TagId
-- Delete records
Delete From [tag].[Process] Where [Node].IsDescendantOf(#ParentNode) = 1
Commit Transaction
End Try
Begin Catch
Rollback Transaction
Return ERROR_MESSAGE()
End Catch
END
I can't figure out what is wrong, any idea?
Regards
Elio Fernandes

Create SQL Server procedure in a transaction

I need to create two procedures in a SQL Server transaction. If failure, I need to rollback the create(s) and any other executed queries in this transaction. I know the create statement must be the first statement in query batch, but I need to know how handle the transaction with multiple batches.
BEGIN TRANSACTION
CREATE PROCEDURE [dbo].[SP_SP-1]
#id BIGINT
AS
BEGIN
SET NOCOUNT ON;
-- SQL statements
END
GO
CREATE PROCEDURE [dbo].[SP_SP-2]
#id BIGINT
AS
BEGIN
SET NOCOUNT ON;
-- SP-2 statements
END
GO
UPDATE Table
SET Value = '1.0.0.5'
COMMIT TRANSACTION / ROLLBACK TRANSACTION
Below is one method to execute multiple batches in a transaction. This uses a temp table to indicate if any batch erred and perform a final COMMIT or ROLLLBACK accordingly.
Another method is to encapsulate statements that must be in single-statement batch (CREATE PROCEDURE, CREATE VIEW, etc.) but that can get rather ugly when quotes within the literal text must be escaped.
CREATE TABLE #errors (error varchar(5));
GO
BEGIN TRANSACTION
GO
CREATE PROCEDURE [dbo].[USP_SP-1]
#id bigint
AS
BEGIN
SET NOCOUNT ON;
-- SP Statments
END;
GO
IF ##ERROR <> 0 INSERT INTO #errors VALUES('error');
GO
CREATE PROCEDURE [dbo].[USP_SP-2]
#id BIGINT
AS
BEGIN
SET NOCOUNT ON;
-- SP-2 Statments
END;
GO
IF ##ERROR <> 0 INSERT INTO #errors VALUES('error');
GO
UPDATE Table SET Value='1.0.0.5'
GO
IF ##ERROR <> 0 INSERT INTO #errors VALUES('error');
GO
IF EXISTS(SELECT 1 FROM #errors)
BEGIN
IF ##TRANCOUNT > 0 ROLLBACK;
END
ELSE
BEGIN
IF ##TRANCOUNT > 0 COMMIT;
END;
GO
IF OBJECT_ID(N'tempdb..#errors', 'U') IS NOT NULL
DROP TABLE #errors;
GO
I suggest you to study more about this subject in Handling Transactions in Nested SQL Server Stored Procedures.
From the beginning, your syntax is wrong. You cannot begin a transaction and then create a procedure, you need to do just the opposite:
CREATE PROCEDURE [dbo].[SP_SP-1]
#id bigint
AS
BEGIN
BEGIN TRY
BEGIN TRANSACTION
SET NOCOUNT ON;
-- SP-2 Statments
Update Table set Value='1.0.0.5'
END TRY
BEGIN CATCH
--handle error and perform rollback
ROLLBACK
SELECT ERROR_NUMBER() AS ErrorNumber
SELECT ERROR_MESSAGE() AS ErrorMessage
END CATCH
END
It is best practice to use TRY and CATCH when attempting to perform update inside transaction scope.
Please read more and investigate using the link I provided to get a bigger picture.
To use Transaction, you need to know what is the meaning of transaction. It's meaning of 'Unit of work either in commit state or rollback state'.
So when you use transaction, you must know that where you declare and where you close. So you must start and end transaction in the parent procedure only than it will work as a unit of work i.e. whatever no of query execute of DML statement, it uses the same transaction.
I do not understand why your update statement outside of procedure and transaction portion too.
It should be (See my comments, you can use TRY Catch same as c sharp) :
Create PROCEDURE [dbo].[SP_SP-1]
#id bigint
AS
BEGIN
Begin Transaction
SET NOCOUNT ON;
-- SP Statments
Exec SP_SP-2 #id --here you can pass the parameter to another procedure, but do not use transaction in another procedure, other wise it will create another transaction
If ##Error > 0 than
Rollback
Else
Commit
End
END
GO
--Do not use transaction in another procedure, otherwise, it will create another transaction which has own rollback and commit and do not participate in the parent transaction
Create PROCEDURE [dbo].[SP_SP-2]
#id BIGINT
AS
BEGIN
SET NOCOUNT ON;
-- SP-2 Statments
END
GO
i find this solution to execute the procedure as string execution , it`s a workaround to execute what i want
Begin Try
Begin Transaction
EXEC ('
Create PROCEDURE [dbo].[SP_1]
#id bigint
AS
BEGIN
SET NOCOUNT ON;
SP-1
END
GO
Create PROCEDURE [dbo].[SP_Inc_Discovery_RunDoc]
#id bigint
AS
BEGIN
SET NOCOUNT ON;
Sp-2
END')
Update Table set Value='1.0.0.5'
Commit
End Try
Begin Catch
Rollback
Declare #Msg nvarchar(max)
Select #Msg=Error_Message();
RaisError('Error Occured: %s', 20, 101,#Msg) With Log;
End Catch

How to log errors even if the transaction is rolled back?

Lets say we have following commands:
SET XACT_ABORT OFF;
SET IMPLICIT_TRANSACTIONS OFF
DECLARE #index int
SET #index = 4;
DECLARE #errorCount int
SET #errorCount = 0;
BEGIN TRANSACTION
WHILE #index > 0
BEGIN
SAVE TRANSACTION Foo;
BEGIN TRY
-- commands to execute...
INSERT INTO AppDb.dbo.Customers VALUES('Jalal', '1990-03-02');
-- make a problem
IF #index = 3
INSERT INTO AppDb.dbo.Customers VALUES('Jalal', '9999-99-99');
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION Foo; -- I want to keep track of previous logs but not works! :(
INSERT INTO AppDb.dbo.LogScripts VALUES(NULL, 'error', 'Customers', suser_name());
SET #errorCount = #errorCount + 1;
END CATCH
SET #index = #index - 1;
END
IF #errorCount > 0
ROLLBACK TRANSACTION
ELSE
COMMIT TRANSACTION
I want to execute a batch, keep all errors in log and then, if no error was occurred, commit all changes. How can implement it in Sql Server?
The transaction is tied to the connection, and as such, all writes will be rolled back on the outer ROLLBACK TRANSACTION (irrespective of the nested savepoints).
What you can do is log the errors to an in-memory structure, like a Table Variable, and then, after committing / rolling back the outer transaction, you can then insert the logs collected.
I've simplified your Logs and Customers tables for the purpose of brevity:
CREATE TABLE [dbo].[Logs](
[Description] [nvarchar](max) NULL
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
CREATE TABLE [dbo].[Customers](
[ID] [int] NOT NULL,
[Name] [nvarchar](50) NULL
);
GO
And then you can track the logs in the table variable:
SET XACT_ABORT OFF;
SET IMPLICIT_TRANSACTIONS OFF
GO
DECLARE #index int;
SET #index = 4;
DECLARE #errorCount int
SET #errorCount = 0;
-- In memory storage to accumulate logs, outside of the transaction
DECLARE #TempLogs AS TABLE (Description NVARCHAR(MAX));
BEGIN TRANSACTION
WHILE #index > 0
BEGIN
-- SAVE TRANSACTION Foo; As per commentary below, savepoint is futile here
BEGIN TRY
-- commands to execute...
INSERT INTO Customers VALUES(1, 'Jalal');
-- make a problem
IF #index = 3
INSERT INTO Customers VALUES(NULL, 'Broken');
END TRY
BEGIN CATCH
-- ROLLBACK TRANSACTION Foo; -- Would roll back to the savepoint
INSERT INTO #TempLogs(Description)
VALUES ('Something bad happened on index ' + CAST(#index AS VARCHAR(50)));
SET #errorCount = #errorCount + 1;
END CATCH
SET #index = #index - 1;
END
IF #errorCount > 0
ROLLBACK TRANSACTION
ELSE
COMMIT TRANSACTION
-- Finally, do the actual insertion of logs, outside the boundaries of the transaction.
INSERT INTO dbo.Logs(Description)
SELECT Description FROM #TempLogs;
One thing to note is that this is quite an expensive way to process data (i.e. attempt to insert all data, and then roll back a batch if there were any problems encountered). An alternative here would be to validate all the data (and return and report errors) before attempting to insert any data.
Also, in the example above, the Savepoint serves no real purpose, as even 'successful' Customer inserts will be eventually rolled back if any errors were detected for the batch.
SqlFiddle here - The loop is completed, and despite 3 customers being inserted, the ROLLBACK TRANSACTION removes all successfully inserted customers. However, the log is still written, as the Table Variable is not subjected to the outer transaction.