Switch isolation level from SNAPSHOT on active transaction and run DDL - sql-server

I would like to force MS SQL Server to use SNAPSHOT isolation level, but in the same transaction, I have to run DML and DDL operation. As I know we can't run DDL (most of DDL operation) in a transaction with SNAPSHOT isolation level.
In article https://learn.microsoft.com/en-us/sql/t-sql/statements/set-transaction-isolation-level-transact-sql?view=sql-server-2017 we can read
If a transaction starts in the SNAPSHOT isolation level, you can change it to another isolation level and then back to SNAPSHOT. A transaction starts the first time it accesses data.
So my idea is to run a transaction with SNAPSHOT isolation level do DML and then switch transaction to READ COMMITTED isolation level and here is a problem.
/* the problematic part */
EXEC print_curr_il 'on start'
SET TRANSACTION ISOLATION LEVEL SNAPSHOT
EXEC print_curr_il 'after setting il to snapshot'
BEGIN TRAN
EXEC print_curr_il 'before inserting'
INSERT INTO tbl2 VALUES ('some value')
SET TRANSACTION ISOLATION LEVEL READ COMMITTED
EXEC print_curr_il 'after setting il to read committed'
ALTER TABLE tbl1 ADD val int
COMMIT
/* simple tables */
CREATE TABLE [dbo].[tbl1](
[text] [nchar](10) NULL
)
GO
CREATE TABLE [dbo].[tbl2](
[text] [nchar](10) NULL
)
GO
/* procedeure to print current isolation level */
CREATE PROCEDURE [dbo].[print_curr_il]
#desc varchar(50)
AS
BEGIN
Declare #il varchar(50);
SELECT #il =
CASE transaction_isolation_level
WHEN 0 THEN 'Unspecified'
WHEN 1 THEN 'ReadUncommitted'
WHEN 2 THEN 'ReadCommitted'
WHEN 3 THEN 'Repeatable'
WHEN 4 THEN 'Serializable'
WHEN 5 THEN 'Snapshot'
END
FROM sys.dm_exec_sessions where session_id = ##SPID;
print #desc + ' Isolation Level = ' + #il;
END
GO
As a result, I get
on start Isolation Level = ReadCommitted
after setting il to snapshot Isolation Level = Snapshot
before inserting Isolation Level = Snapshot
(1 row affected)
after setting il to read committed Isolation Level = ReadCommitted
Msg 3964, Level 16, State 1, Line 11
Transaction failed because this DDL statement is not allowed inside a snapshot isolation transaction. Since metadata is not versioned, a metadata change can lead to inconsistency if mixed within snapshot isolation.
So it looks like impossible, but I can't find the strict answer that it is impossible.
I suspect that SET TRANSACTION ISOLATION LEVEL ... on an open transaction is changing the behavior of locking, but not whole transaction type.

Related

How do nested transactions work with triggers and different isolation levels in SQL Server?

I need to answer two questions. Here's an overview:
I have 3 tables:
CREATE TABLE A(x INT)
CREATE TABLE B(x INT)
CREATE TABLE C(x INT)
and 2 triggers:
ALTER TRIGGER ATr ON A
FOR INSERT AS
BEGIN
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
INSERT INTO B
SELECT *
FROM inserted;
COMMIT
END
ALTER TRIGGER BTr ON B
FOR INSERT AS
BEGIN
INSERT INTO C
SELECT * FROM inserted;
END
and the INSERT triggering the ATr trigger INSERT INTO A VALUES(3) is done on REPEATABLE READ isolation level, and the default isolation level for this database is READ COMMITTED.
And I need to answer two questions:
On which isolation level will the ATr trigger execute?
On which isolation level will the BTr trigger execute (if at all)?
I have problem understanding how these nested transactions are going to work. Will any transaction inside a LEVEL SERIALIZABLE transaction ever execute? Are they even nested in this case? What happens if a lower isolation level is inside a higher isolation level or the other way around?
The behavior is documented here SET TRANSACTION ISOLATION LEVEL
And you can examine this behavior by running
select CASE transaction_isolation_level
WHEN 0 THEN 'Unspecified'
WHEN 1 THEN 'ReadUncomitted'
WHEN 2 THEN 'ReadCommitted'
WHEN 3 THEN 'Repeatable'
WHEN 4 THEN 'Serializable'
WHEN 5 THEN 'Snapshot' END AS transaction_isolation_level
from sys.dm_exec_requests
where session_id = ##spid
wherever you are curious about the currently-active isolation level.

SQL Server transactions errors due to different Isolation Levels

I created the following stored procedure in order to examine the isolation level behavior in transaction:
CREATE PROCEDURE ReadCommittedIsolationLevel
AS
BEGIN
BEGIN TRANSACTION t1
BEGIN TRY
EXEC SnapShotIsolationLevel
COMMIT TRANSACTION
END TRY
BEGIN CATCH
PRINT ERROR_MESSAGE()
ROLLBACK TRANSACTION t1
END CATCH
END
CREATE PROCEDURE SnapShotIsolationLevel
AS
BEGIN
SET TRANSACTION ISOLATION LEVEL SNAPSHOT
BEGIN TRANSACTION t2
BEGIN TRY
SELECT TOP 20 *
FROM Orders
ORDER BY 1 DESC
COMMIT
END TRY
BEGIN CATCH
PRINT ERROR_MESSAGE()
ROLLBACK TRAN t2
END CATCH
END
And then I run this:
EXEC ReadCommittedIsolationLevel
I get this error:
Transaction failed in database 'MyDataBase' because the statement was run under snapshot isolation but the transaction did not start in snapshot isolation. You cannot change the isolation level of the transaction to snapshot after the transaction has started unless the transaction was originally started under snapshot isolation level.
Cannot roll back t2. No transaction or savepoint of that name was found.
If I remove the transactions and run it like plain stored procedures, it works fine.
Why is that?
The error gave you the correct explanation:
You cannot change the isolation level of the transaction to snapshot
after the transaction has started.
SQL Server does not have nested transactions, the only real transaction you have is the outer transaction started under Read Committed, the next begin tran does nothing except for incrementing ##trancount. You can read more on it here: A SQL Server DBA myth a day: (26/30) nested transactions are real by Paul Randal
Snapshot Isolation means consistent data at the transaction level, not at the statement level(RCSI), so you should change Isolation level to Snapshot at the beginning of the transaction or you cannot change it to Snapshot at all.

Lock a row in a table SQL Server

I need to lock a row in a table so no one can read this line while I'm running a procedure. I am using BEGIN TRAN in this procedure. So, this record I'm trying to block is uncommitted during the process.
Is it possible?
Depending on what is the purpose of your stored procedure:
- In case it modifies the mentioned row, you can base on transaction levels
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRANSACTION
--UPDATE/INSERT/DELETE your row here
...
COMMIT TRANSACTION
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
- Use lock hints
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRANSACTION
SELECT column1, column2
FROM yourTable WITH (ROWLOCK)
WHERE ID = YourRecordId
...
COMMIT TRANSACTION
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ

Fixing upsert issue with TRANSACTION ISOLATION LEVEL REPEATABLE READ?

I have a SQL statement that does an update, and then if the ##ROWCOUNT is 0, it will insert. this is basically a MERGE in SQL 2008. We are running into situations where two threads are failing on the update simultaneously. It will attempt to insert the same key twice in a table. We are using the Default Transaction isolation level, Read Committed. Will changing the level to repeatable reads fix this or do I have to go all the way to Serializable to make this work? Here is some code:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN TRAN;
UPDATE TableA
SET Duration = #duration
WHERE keyA = #ID
AND keyB = #IDB;
IF ##rowcount = 0
BEGIN
INSERT INTO TableA (keyA,keyB,Duration)
VALUES (#ID,#IDB,#duration);
END
COMMIT TRAN;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;";
You would need to go all the way up to SERIALIZABLE.
Under REPEATABLE READ if the row does not exist then both UPDATE statements can run concurrently without blocking each other and proceed to do the insert. Under SERIALIZABLE the range where the row would have been is blocked.
But you should also consider leaving the isolation level at default read committed and putting a unique constraint on keyA,keyB so any attempts to insert a dupe fail with an error.

What is the scope of isolation in nested transactions in SQL Server?

Consider the following SQL:
BEGIN TRAN
SET TRANSACTION ISOLATION LEVEL READ COMMITTED
INSERT Bands
( Name )
SELECT 'Depeche Mode'
UNION
SELECT 'Arcade Fire'
-- I've indented the inner transaction to make it clearer.
BEGIN TRAN
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
SELECT *
FROM Bands
COMMIT
-- What is the isolation level right here?
UPDATE Bands
SET Name = 'Modest Mouse'
WHERE Name = 'Oddest House'
COMMIT
In sum, we start a transaction and set its isolation level to READ COMMITTED. We then do some random SQL and start another, nested transaction. In this transaction we change the isolation level to READ UNCOMMITTED. We then commit that transaction and return to the other.
Now, my guess is that after the inner commit, the isolation level returns to READ COMMITTED. Is this correct?
You [Bob Probst] are correct. Interestingly, according to the documentation you linked:
If you issue SET TRANSACTION ISOLATION LEVEL in a stored procedure or trigger, when the object returns control the isolation level is reset to the level in effect when the object was invoked. For example, if you set REPEATABLE READ in a batch, and the batch then calls a stored procedure that sets the isolation level to SERIALIZABLE, the isolation level setting reverts to REPEATABLE READ when the stored procedure returns control to the batch.
So, the bottom line here is that SET TRANSACTION ISOLATION LEVEL has procedure affinity, not transaction affinity (as I had thought).
Awesome!
I don't think that is correct.
Refer to the remarks here: Set Transaction
Only one of the isolation level
options can be set at a time, and it
remains set for that connection until
it is explicitly changed.

Resources