SERIALIZABLE ISOLATION LEVEL

Here is a brief explanation of the Serializable isolation level.

“A serial execution is one in which each SQL-transaction executes to completion before the next SQL-transaction begins.” You can see how that would slow things down.

If a transaction that is running in Serializable mode accesses or changes tables then those tables remain locked until the transaction completes. Although there are rare occasions where this may be the desired effect, most of the time serializable mode is an undesirable thing.

Take the example of a [users] table which has user login information that is frequently hit to validate user access and their ability to log into the system.

All queries serializable:

If every query running against the system was running in serializable mode, then it would effectively be single threaded, assume you have to users attempting to log in to the system, they would in serializable mode have to run one after another until al complete, rather than all being able to hit the [users] table at the same time to verify their login info.

Some queries serializable:

Take that same users table where you have an occasionally query in serializable mode that hits the [users] table. When those occasional serializable queries are not running everyone accesses the users table just fine without any blocking, but as soon as that serializable query accesses the table you would start seeing blocking, then the serializable query finished and all goes back to the non blocking mode.

When a table is accessed all access to that table is blocked (unless using NOLOCK or other dirty read options).

Recommended action.  Catch the queries that are in serializable mode, track them down and determine if they are needed to be in serializable mode.

You may not even know that your queries are being run in serializable mode, it may be your programming tools that are adding this such as ADO, .NET or other components.

Sample Code:

First we create a database to use for the demo, and a couple of tables to use. The Users table is the one that we will have the locking issues with, and the LoginHistory table is just used to track how frequently we are inserting data, and to see when blocking occurs. We start by filling in the users table with 2 real users and 1000 automatically generated users by using the first 8 characters of a GUID for the login name, just so we don’t have an empty table.

CREATE DATABASE SerializableTest;
GO
USE SerializableTest;
GO

CREATE TABLE Users
(
	[id] INTEGER IDENTITY NOT NULL,
	[username] NVARCHAR(MAX) NOT NULL,
	[password] NVARCHAR(MAX) NOT NULL,
	[lastLogin] DATETIME NULL
);

-- used to track throughput of changes
CREATE TABLE LoginHistory
(
	[id] INTEGER IDENTITY NOT NULL,
	[userId] INTEGER NULL, 
	[timeLogged] DATETIME 
);

GO
SET NOCOUNT ON;
INSERT INTO Users ([username], [password]) VALUES ('Fred', 'password123');
GO
INSERT INTO Users ([username], [password]) VALUES (left(cast(newid() as varchar(100)), 8), 'password123');
GO 1000
INSERT INTO Users ([username], [password]) VALUES ('Mary', 'password456');
SET NOCOUNT OFF;

Next we create a stored procedure to simulate the login verification process.

CREATE PROCEDURE verifyLogin
(
	@username NVARCHAR(MAX),
	@password NVARCHAR(MAX),
	@id       INTEGER OUTPUT
)
AS
BEGIN
	SET NOCOUNT ON;

	SELECT @id = ID 
	  FROM Users 
	 WHERE [username] = @username
	   AND [password] = @password;

	IF @id IS NOT NULL
	BEGIN
		UPDATE Users SET [lastLogin] = getdate() where ID = @id;
		
		INSERT INTO LoginHistory ([userId], [timeLogged]) VALUES (@id, GETDATE());
	END
END

Next we test the procedure with our 2 users, and we see what shows up in the LoginHistory table.


DECLARE @userID as integer = NULL;
EXEC verifyLogin @username = 'Fred', @password = 'password123', @id = @userID  OUTPUT;
print concat('userid = ', @userID);
GO
DECLARE @userID as integer = NULL;
EXEC verifyLogin @username = 'Mary', @password = 'password456', @id = @userID  OUTPUT;
print concat('userid = ', @userID);
GO

SELECT * FROM LoginHistory;

Now we need to simulate some load so we have 2 sets of looping code that each run in 2 tabs, for a total of 4 other SSMS tabs running the verify login check.

-- run in several other tabs
DECLARE @userID as integer = NULL;
EXEC verifyLogin @username = 'Fred', @password = 'password123', @id = @userID  OUTPUT;
print concat('userid = ', @userID);
GO 100000



-- run in several other tabs
DECLARE @userID as integer = NULL;
EXEC verifyLogin @username = 'Mary', @password = 'password456', @id = @userID  OUTPUT;
print concat('userid = ', @userID);
GO 100000

With 4 SSMS tabs occupied running the verifyLogin procedure in a tight look we can check the LoginHistory table to see the throughput with the following query. Note the use of NOLOCK so that we can still check this table even when blocking occurs.

SELECT TOP 100 * 
FROM LoginHistory WITH (NOLOCK)
ORDER BY timeLogged DESC;

From that login history table unless you are running on a really slow sever you should see dozens of logins per second logged there.

Run the same query a few times again and again to see that we are in deed getting dozens of logins per second.

Next we run the same stored procedure in serializable mode, then in the other window check the throughput. You should see blocking for 30 seconds, based on this query using the WAITFOR command to simulate a long running process. Not the use of the SET TRANSACTION ISOLATION LEVEL SERIALIZABLE command. This is what some programming tools use as the default when you open a connection.

BEGIN TRAN
	SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
	DECLARE @userID as integer = NULL;
	EXEC verifyLogin @username = 'Mary', @password = 'password456', @id = @userID  OUTPUT;
	print concat('userid = ', @userID);


	
    waitfor delay '00:00:30'

	EXEC verifyLogin @username = 'Mary', @password = 'password456', @id = @userID  OUTPUT;
	print concat('userid = ', @userID);

	SET TRANSACTION ISOLATION LEVEL READ COMMITTED
COMMIT TRAN

When you jump to the other tab and run the check script for LoginHistory you will notice that until the above SERIALIZABLE query finishes running, you will not get any logins tracked in the LoginHistory table because everything is blocked by the serializable query.

SELECT TOP 100 * 
FROM LoginHistory WITH (NOLOCK)
ORDER BY timeLogged DESC;

Summary:

Unless you have a really good reason for it it is unlikely that you should be running in serializable mode, however there may be many cases where this is happening without you knowing about it.

I hope this information has helped you to better understand serializable isolation level on SQL Server.

Note: All tests were run on SQL Server 2019, however they should perform the same on any version of SQL Server from SQL 2005 or newer.


Enroll Today!
SteveStedman5
SteveStedman5
Steve and the team at Stedman Solutions are here for all your SQL Server needs.
Contact us today for your free 30 minute consultation..
We are ready to help!

Leave a Reply

Your email address will not be published. Required fields are marked *

*

To prove you are not a robot: *