As we’ve started explaining, the isolation behavior of a transaction can be tweaked to your needs. This is generally done by setting the isolation level of a transaction. Put simply, isolation levels determine how concurrent transactions
behave. Do they block each other? Do they let each other step over
themselves? Or do they present a snapshot of a previous stable state of
data in the event of an overstepped condition?
You can set isolation levels by using the SET TRANSACTION ISOLATION LEVEL statement, which uses the following syntax:
SET TRANSACTION ISOLATION LEVEL
{ READ UNCOMMITTED
| READ COMMITTED
| REPEATABLE READ
| SNAPSHOT
| SERIALIZABLE
}
A sample usage of this statement with the BEGIN TRANSACTION statement is shown here:
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
BEGIN TRANSACTION
SELECT TestColumn FROM TestTable
COMMIT
As you can see from the syntax, SQL Server supports these five isolation levels:
Read uncommitted
Read committed
Repeatable read
Snapshot
Serializable
1. Read Uncommitted Isolation Level
By
specifying the read uncommitted isolation level, you essentially tell
the database to violate all locks and read the current immediate state
of data. But by doing so, you might end up with a dirty
read—reading data that is not yet committed. You should therefore avoid
this isolation level if your application requires precise, committed
data because transactions using this isolation level can return logically incorrect data.
Let’s explore this isolation level by using an example.
Example 4-1. Using SqlCommand to execute a simple T-SQL command.
CREATE DATABASE MyDB
GO
USE MyDB
GO
CREATE TABLE TestTable
(
TestID INT IDENTITY PRIMARY KEY,
TestColumn INT
)
INSERT INTO TestTable(TestColumn) VALUES(100)
Use either SSMS or SSDT to execute the script shown in Example 1 to create a simple test table.
Open two separate query windows, and in each instance, use the MyDB database that contains the TestTable table created in step 1. These two instances will be used to simulate two users running two concurrent transactions.
In instance 1, execute an UPDATE on the row of data by running the following code block:
BEGIN TRANSACTION
UPDATE TestTable SET TestColumn=200 WHERE TestId=1
In instance 2, execute the following query:
SELECT TestColumn FROM TestTable WHERE TestId=1
You will notice that your SELECT
query is blocked. This makes sense because you are trying to read the
same data that instance 1 is busy modifying. Unless instance 1 issues a
COMMIT or a ROLLBACK, your query will remain blocked or will simply time out.
Cancel your blocked SELECT query by pressing Alt+Break or by clicking the Cancel button on the toolbar. Then execute the following statement to set the isolation level of your SELECT query to read uncommitted on the connection held by instance 2:
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
Execute the SELECT query again, as follows:
SELECT TestColumn FROM TestTable WHERE TestId=1
You will find that the query isn’t blocked; it produces 200 as a result.
Go back to instance 1, and issue a ROLLBACK.
Back in instance 2, execute the same SELECT query again. You should receive 100 as the result.
As
you might have noticed, instance 2 returned different results for the
same query at different times. As a matter of fact, the value 200 was
never committed to the database, but because you explicitly requested a
dirty read by specifying the READ UNCOMMITTED isolation level,
you ended up reading data that was never meant to be final. So the
downside is that you ended up reading logically incorrect data. On the
upside, however, your query was not blocked.
2. Read Committed Isolation Level
Read
committed is the default isolation level. As you will see shortly, read
committed is the default because it strikes the best balance between
data integrity and performance. This isolation level respects locks and
prevents dirty reads from occurring. In the example you saw earlier,
until you explicitly requested that the isolation level be changed to read
uncommitted, the connection worked at the read committed isolation
level, which caused the second transaction (the autocommit mode
transaction in the SELECT query) to get blocked by the transaction executing the UPDATE query.
A
read committed isolation level prevents dirty reads, but phantom reads
and nonrepeatable reads are still possible when using this isolation
level. This is because the read committed isolation level does not
prevent one transaction from changing the same data at the same time as
another transaction is reading from it.
A phantom read can occur in the following type of situation:
Transaction 1 begins.
Transaction 1 reads a row.
Transaction 2 begins.
Transaction 2 deletes the row that was read by transaction 1.
Transaction
2 commits. Transaction 1 can no longer repeat its initial read because
the row no longer exists, resulting in a phantom row.
A nonrepeatable read can occur in the following type of situation:
Transaction 1 begins.
Transaction 1 reads a row.
Transaction 2 begins.
Transaction 2 changes the value of the same row read by transaction 1.
Transaction 2 commits.
Transaction
1 reads the row again. Transaction 1 has inconsistent data because the
row now contains different values from the previous read, all within
the scope of transaction 1.