Microsoft SQL Server (Part 2)
Neil Haddley • March 27, 2021
T-SQL, Stored Procedures and Snapshot Isolation.
Stored procedures
I used Transact SQL (T-SQL) to create Microsoft SQL Server Stored Procedures. Stored procedures are maintained in the database, making it easier to apply performance improvements that benefit all client applications. I wrote the swap procedure with isolation levels in mind, since concurrent access to shared data can cause transactions to interfere with each other.
Shuffling cards
I used a stored procedure to shuffle the contents of a table representing a deck of playing cards.
Swap cards
To shuffle the deck, I selected two cards at random and swapped their positions. Repeating this enough times produces a shuffled deck. I wrote a stored procedure to swap the data in two rows of the deck table.
Pick two cards, swap and repeat
To test SwapCards, I created:
- a PickACard function that selects a card from the deck at random
- a SwapCardsAtRandom stored procedure that uses PickACard (twice) and SwapCards to swap a random pair of cards
- client code that calls SwapCardsAtRandom 1,000 times
With a single client
Running a single copy of the client code calling SwapCardV1 worked correctly. I ended up with a shuffled deck containing 13 cards of each suit.

Shuffle results single client
Concurrency issues
However, running multiple copies of the client code revealed a concurrency issue with SwapCardV1.

Shuffle failure
Snapshot issolation
With concurrency introduced, the original SwapCards stored procedure failed silently — no errors were raised but transactions were stepping on each other. I used Snapshot isolation to fix the issue.
Multiple clients
With the updated SwapCards stored procedure, I was able to run multiple updates concurrently.

Shuffle success
Catching the errors
Snapshot isolation guarantees that reads in a transaction see a consistent snapshot and that the transaction only commits if no conflicting concurrent update occurred. With snapshot isolation enabled, there is a chance that a commit will fail and raise an error — which is the desired behaviour. In the code above, if SwapCards threw an error the client caught it, printed a message, and continued. I considered having the client retry the swap instead.

Concurrency exceptions (that the client code needs to handle)
.NET Core console app
$ dotnet run
I wrote C# code to call the SwapCards stored procedure 1,000 times.
Node console app
$ node index.js
I wrote JavaScript code to call the SwapCards stored procedure 1,000 times using the mssql node module.
Java console app
$ javac Program.java
then
$ java -cp ".:/Users/neilhaddley/sqljdbc_9.2/enu/mssql-jdbc-9.2.1.jre11.jar" Program
or
$ export CLASSPATH=.:/Users/neilhaddley/sqljdbc_9.2/enu/mssql-jdbc-9.2.1.jre11.jar
$ java Program
I wrote Java code to call the SwapCards stored procedure 1,000 times.
Using cross join to create a table the represents ...
SQL
1-- Create the Deck 2CREATE TABLE Cards 3( 4 Card CHAR(5) PRIMARY KEY 5) 6GO 7 8INSERT INTO Cards 9VALUES 10 ('Ace'), 11 ('2'), 12 ('3'), 13 ('4'), 14 ('5'), 15 ('6'), 16 ('7'), 17 ('8'), 18 ('9'), 19 ('10'), 20 ('Jack'), 21 ('Queen'), 22 ('King') 23GO 24 25CREATE TABLE Suits 26( 27 Suit CHAR(8) PRIMARY KEY 28) 29GO 30 31INSERT INTO Suits 32VALUES 33 ('Clubs'), 34 ('Diamonds'), 35 ('Hearts'), 36 ('Spades') 37GO 38 39SELECT Id = IDENTITY(INT, 1, 1), 40 Suits.Suit, 41 Cards.Card 42INTO Deck 43FROM Cards 44CROSS JOIN Suits 45GO 46 47ALTER TABLE Deck ADD CONSTRAINT Deck_PK PRIMARY KEY (Id) 48GO
SwapCardsV1
SQL
1-- Stored Procedure 'SwapCardsV1' 2CREATE PROCEDURE SwapCardsV1 3 @Card1 INT = 1, 4 @Card2 INT = 52 5AS 6BEGIN 7 8 BEGIN TRY 9 BEGIN TRANSACTION 10 11 SET NOCOUNT ON 12 13 -- Store the first card's details 14 SELECT * 15 INTO #Temp 16 FROM Deck 17 WHERE Id = @Card1 18 19 -- Move second card into first card's place 20 UPDATE Deck 21 SET Card = CardDetails.Card, 22 Suit = CardDetails.Suit 23 FROM ( 24 SELECT d.Card, 25 d.Suit 26 FROM Deck d 27 WHERE Id = @Card2 28 ) CardDetails 29 WHERE Id = @Card1 30 31 -- Move first card into second card's place 32 UPDATE Deck 33 SET Card = CardDetails.Card, 34 Suit = CardDetails.Suit 35 FROM ( 36 SELECT Card, 37 Suit 38 FROM #temp 39 ) CardDetails 40 WHERE Id = @Card2 41 42 COMMIT TRANSACTION; 43 END TRY 44 45 BEGIN CATCH 46 -- PRINT 'We were unable to make the swap rolling back [' + ERROR_MESSAGE() + '].'; 47 ROLLBACK TRANSACTION; 48 THROW 51000, 'Unable to swap card.', 1; 49 END CATCH 50END 51GO
SwapCards T-SQL Client Code
SQL
1-- User Defined Function 'PickACard' 2CREATE FUNCTION PickACard (@RAND FLOAT) 3RETURNS INT 4AS 5BEGIN 6 RETURN ( 7 SELECT FLOOR(@RAND * (52 - 1 + 1)) + 1 8 ) 9END 10GO 11 12-- Stored Procedure 'SwapCardsAtRandom' 13CREATE PROCEDURE SwapCardsAtRandom 14AS 15BEGIN 16 DECLARE @Rnd1 FLOAT 17 18 SET @Rnd1 = RAND() 19 20 DECLARE @Card1 INT 21 22 SET @Card1 = ( 23 SELECT dbo.PickACard(@Rnd1) 24 ) 25 26 DECLARE @Rnd2 FLOAT 27 28 SET @Rnd2 = RAND() 29 30 DECLARE @Card2 INT 31 32 SET @Card2 = ( 33 SELECT dbo.PickACard(@Rnd2) 34 ) 35 36 -- 'No need to swap a card with itself' 37 IF @Card1 != @Card2 38 BEGIN 39 EXECUTE dbo.SwapCardsV1 @Card1, 40 @Card2 41 END -- IF @Card1 != @Card2 42END -- CREATE PROCEDURE SwapCards 43GO 44 45-- Call Swap Cards Multiple times 46DECLARE @Counter INT 47SET @Counter = 0 48DECLARE @ErrorCounter INT 49SET @ErrorCounter = 0 50WHILE (@Counter < 1000) 51BEGIN 52 53 BEGIN TRY 54 EXECUTE SwapCardsAtRandom 55 END TRY 56 57 BEGIN CATCH 58 SET @ErrorCounter = @ErrorCounter+1 59 PRINT 'We were unable to swap cards [' + ERROR_MESSAGE() +']. Error Count ['+LTRIM(STR(@ErrorCounter,10))+']' 60 END CATCH 61 62 SET @Counter = @Counter + 1 63END 64GO 65 66-- Review results 67SELECT COUNT(*) 68FROM Deck 69GROUP BY Suit 70GO 71 72SELECT * 73FROM Deck 74ORDER BY Card,Suit 75GO
SwapCards with snapshot isolation
SQL
1-- Stored Procedure 'SwapCards' 2CREATE PROCEDURE SwapCards 3 @Card1 INT = 1, 4 @Card2 INT = 52 5AS 6BEGIN 7 SET TRANSACTION ISOLATION LEVEL SNAPSHOT; 8 9 BEGIN TRY 10 BEGIN TRANSACTION 11 12 SET NOCOUNT ON 13 14 -- Store the first card's details 15 SELECT * 16 INTO #Temp 17 FROM Deck 18 WHERE Id = @Card1 19 20 -- Move second card into first card's place 21 UPDATE Deck 22 SET Card = CardDetails.Card, 23 Suit = CardDetails.Suit 24 FROM ( 25 SELECT d.Card, 26 d.Suit 27 FROM Deck d 28 WHERE Id = @Card2 29 ) CardDetails 30 WHERE Id = @Card1 31 32 -- Move first card into second card's place 33 UPDATE Deck 34 SET Card = CardDetails.Card, 35 Suit = CardDetails.Suit 36 FROM ( 37 SELECT Card, 38 Suit 39 FROM #temp 40 ) CardDetails 41 WHERE Id = @Card2 42 43 COMMIT TRANSACTION; 44 END TRY 45 46 BEGIN CATCH 47 -- PRINT 'We were unable to make the swap rolling back [' + ERROR_MESSAGE() + '].'; 48 ROLLBACK TRANSACTION; 49 THROW 51000, 'Unable to swap card.', 1; 50 END CATCH 51END 52GO
Program.cs
CSHARP
1using System; 2using System.Data.SqlClient; 3using System.Data; 4 5namespace dotnet_sql { 6 class Program { 7 static void Main(string[] args) { 8 var cs = @ "User ID=sa;Password=Passw0rd123;Initial Catalog=Shuffle;Server=192.168.68.109;"; 9 10 using var con = new SqlConnection(cs); 11 con.Open(); 12 13 using(SqlCommand cmd = new SqlCommand("dbo.SwapCards", con)) { 14 cmd.CommandType = CommandType.StoredProcedure; 15 cmd.Parameters.Add("@Card1", SqlDbType.Int); 16 cmd.Parameters.Add("@Card2", SqlDbType.Int); 17 18 int errorCounter = 0; 19 for (int count = 0; count < 1000; count++) { 20 Random rn = new Random(); 21 int card1 = rn.Next(1, 52); 22 int card2 = rn.Next(1, 52); 23 24 cmd.Parameters["@Card1"].Value = card1; 25 cmd.Parameters["@Card2"].Value = card2; 26 27 try { 28 cmd.ExecuteNonQuery(); 29 } catch (Exception e) { 30 errorCounter++; 31 Console.WriteLine("We were unable to swap cards [" + e.Message + "]. Error Count [" + 32 errorCounter + "]"); 33 } 34 35 } 36 } 37 } 38 } 39}
index.js
JAVASCRIPT
1const mssql = require('mssql') 2 3const config = { 4 user: 'sa', 5 password: 'Passw0rd123', 6 server: '192.168.68.109', 7 database: 'Shuffle', 8 options: { enableArithAbort: false } 9}; 10 11const dbConn = mssql.connect(config, function (err) { 12 13 if (err) { 14 console.log(err); 15 return 16 } 17 18 const request = new mssql.Request(); 19 20 request.input('Card1', mssql.Int); 21 request.input('Card2', mssql.Int); 22 23 let errorCounter = 0; 24 25 (async () => { 26 for (x = 0; x < 1000; x++) { 27 28 const card1 = Math.floor(Math.random() * (52 - 1 + 1)) + 1 29 const card2 = Math.floor(Math.random() * (52 - 1 + 1)) + 1 30 31 request.parameters.Card1.value = card1; 32 request.parameters.Card2.value = card2; 33 34 if (card1 != card2) { 35 36 try { 37 await request.execute('dbo.SwapCards') 38 } catch (e) { 39 errorCounter++; 40 console.error("We were unable to swap cards [" + e.originalError.message + "]. Error Count [" + errorCounter + "]"); 41 } 42 43 } 44 } 45 dbConn.close(); 46 47 })().catch(err => { 48 console.error(err); 49 }); 50 51})
Program.java
JAVA
1import java.sql.Connection; 2import java.sql.DriverManager; 3import java.sql.SQLException; 4import java.sql.CallableStatement; 5import java.util.Random; 6 7public class Program { 8 9 10 public static void main(String[] args) { 11 12 String connectionUrl = "jdbc:sqlserver://192.168.68.109:1433;databaseName=Shuffle;user=sa;password=Passw0rd123"; 13 14 try (Connection con = DriverManager.getConnection(connectionUrl);) { 15 16 CallableStatement callableStatement = con.prepareCall("{call dbo.SwapCards(?,?)}"); 17 18 int errorCounter = 0; 19 for (int count = 0; count < 1000; count++) { 20 21 Random rn = new Random(); 22 int card1 = rn.nextInt(52 - 1 + 1) + 1; 23 int card2 = rn.nextInt(52 - 1 + 1) + 1; 24 25 try { 26 if (card1 != card2) { 27 callableStatement.setInt(1, card1); 28 callableStatement.setInt(2, card2); 29 callableStatement.execute(); 30 } 31 } catch (SQLException e1) { 32 errorCounter++; 33 System.out.println("We were unable to swap cards [" + e1.getMessage() + "]. Error Count [" 34 + Integer.toString(errorCounter) + "]"); 35 } 36 } 37 38 } catch (SQLException e) { 39 e.printStackTrace(); 40 } 41 } 42}
Full T-SQL code
SQL
1DROP DATABASE Shuffle 2GO 3 4-- Create the DB 5CREATE DATABASE Shuffle 6GO 7 8-- Allow snapshot isolation 9ALTER DATABASE Shuffle 10SET ALLOW_SNAPSHOT_ISOLATION ON 11GO 12 13-- Enable accelerated database recovery 14ALTER DATABASE Shuffle 15SET ACCELERATED_DATABASE_RECOVERY = ON; 16GO 17 18-- Use Shuffle DB 19USE Shuffle 20GO 21 22-- Create the Deck 23CREATE TABLE Cards 24( 25 Card CHAR(5) PRIMARY KEY 26) 27GO 28 29INSERT INTO Cards 30VALUES 31 ('Ace'), 32 ('2'), 33 ('3'), 34 ('4'), 35 ('5'), 36 ('6'), 37 ('7'), 38 ('8'), 39 ('9'), 40 ('10'), 41 ('Jack'), 42 ('Queen'), 43 ('King') 44GO 45 46CREATE TABLE Suits 47( 48 Suit CHAR(8) PRIMARY KEY 49) 50GO 51 52INSERT INTO Suits 53VALUES 54 ('Clubs'), 55 ('Diamonds'), 56 ('Hearts'), 57 ('Spades') 58GO 59 60SELECT Id = IDENTITY(INT, 1, 1), 61 Suits.Suit, 62 Cards.Card 63INTO Deck 64FROM Cards 65CROSS JOIN Suits 66GO 67 68ALTER TABLE Deck ADD CONSTRAINT Deck_PK PRIMARY KEY (Id) 69GO 70 71-- User Defined Function 'PickACard' 72CREATE FUNCTION PickACard (@RAND FLOAT) 73RETURNS INT 74AS 75BEGIN 76 RETURN ( 77 SELECT FLOOR(@RAND * (52 - 1 + 1)) + 1 78 ) 79END 80GO 81 82-- Stored Procedure 'SwapCardsV1' 83CREATE PROCEDURE SwapCardsV1 84 @Card1 INT = 1, 85 @Card2 INT = 52 86AS 87BEGIN 88 89 BEGIN TRY 90 BEGIN TRANSACTION 91 92 SET NOCOUNT ON 93 94 -- Store the first card's details 95 SELECT * 96 INTO #Temp 97 FROM Deck 98 WHERE Id = @Card1 99 100 -- Move second card into first card's place 101 UPDATE Deck 102 SET Card = CardDetails.Card, 103 Suit = CardDetails.Suit 104 FROM ( 105 SELECT d.Card, 106 d.Suit 107 FROM Deck d 108 WHERE Id = @Card2 109 ) CardDetails 110 WHERE Id = @Card1 111 112 -- Move first card into second card's place 113 UPDATE Deck 114 SET Card = CardDetails.Card, 115 Suit = CardDetails.Suit 116 FROM ( 117 SELECT Card, 118 Suit 119 FROM #temp 120 ) CardDetails 121 WHERE Id = @Card2 122 123 COMMIT TRANSACTION; 124 END TRY 125 126 BEGIN CATCH 127 -- PRINT 'We were unable to make the swap rolling back [' + ERROR_MESSAGE() + '].'; 128 ROLLBACK TRANSACTION; 129 THROW 51000, 'Unable to swap card.', 1; 130 END CATCH 131END 132GO 133 134-- Stored Procedure 'SwapCards' 135CREATE PROCEDURE SwapCards 136 @Card1 INT = 1, 137 @Card2 INT = 52 138AS 139BEGIN 140 SET TRANSACTION ISOLATION LEVEL SNAPSHOT; 141 142 BEGIN TRY 143 BEGIN TRANSACTION 144 145 SET NOCOUNT ON 146 147 -- Store the first card's details 148 SELECT * 149 INTO #Temp 150 FROM Deck 151 WHERE Id = @Card1 152 153 -- Move second card into first card's place 154 UPDATE Deck 155 SET Card = CardDetails.Card, 156 Suit = CardDetails.Suit 157 FROM ( 158 SELECT d.Card, 159 d.Suit 160 FROM Deck d 161 WHERE Id = @Card2 162 ) CardDetails 163 WHERE Id = @Card1 164 165 -- Move first card into second card's place 166 UPDATE Deck 167 SET Card = CardDetails.Card, 168 Suit = CardDetails.Suit 169 FROM ( 170 SELECT Card, 171 Suit 172 FROM #temp 173 ) CardDetails 174 WHERE Id = @Card2 175 176 COMMIT TRANSACTION; 177 END TRY 178 179 BEGIN CATCH 180 -- PRINT 'We were unable to make the swap rolling back [' + ERROR_MESSAGE() + '].'; 181 ROLLBACK TRANSACTION; 182 THROW 51000, 'Unable to swap card.', 1; 183 END CATCH 184END 185GO 186 187-- Stored Procedure 'SwapCardsAtRandom' 188CREATE PROCEDURE SwapCardsAtRandom 189AS 190BEGIN 191 DECLARE @Rnd1 FLOAT 192 193 SET @Rnd1 = RAND() 194 195 DECLARE @Card1 INT 196 197 SET @Card1 = ( 198 SELECT dbo.PickACard(@Rnd1) 199 ) 200 201 DECLARE @Rnd2 FLOAT 202 203 SET @Rnd2 = RAND() 204 205 DECLARE @Card2 INT 206 207 SET @Card2 = ( 208 SELECT dbo.PickACard(@Rnd2) 209 ) 210 211 -- 'No need to swap a card with itself' 212 IF @Card1 != @Card2 213 BEGIN 214 EXECUTE dbo.SwapCards @Card1, 215 @Card2 216 END -- IF @Card1 != @Card2 217END -- CREATE PROCEDURE SwapCards 218GO 219 220-- Call SwapCardsAtRandom Multiple times 221DECLARE @Counter INT 222SET @Counter = 0 223DECLARE @ErrorCounter INT 224SET @ErrorCounter = 0 225WHILE (@Counter < 1000) 226BEGIN 227 228 BEGIN TRY 229 EXECUTE SwapCardsAtRandom 230 END TRY 231 232 BEGIN CATCH 233 SET @ErrorCounter = @ErrorCounter+1 234 PRINT 'We were unable to swap cards [' + ERROR_MESSAGE() +']. Error Count ['+LTRIM(STR(@ErrorCounter,10))+']' 235 END CATCH 236 237 SET @Counter = @Counter + 1 238END 239GO 240 241-- Review results 242SELECT COUNT(*) 243FROM Deck 244GROUP BY Suit 245GO 246 247SELECT * 248FROM Deck 249ORDER BY Card,Suit 250GO