SQL Serverのトランザクション分離レベルについて改めて整理する

データベースにおけるトランザクション分離レベルは重要ですが、それぞれの詳細な挙動をすぐに思い出すのは難しいことがあります。
本記事では実際に動かして挙動を確認しつつ各トランザクション分離レベルの特徴を整理してみました。

# トランザクション分離レベルとは

トランザクション分離レベルは、同時に複数のトランザクションが同じデータにアクセスする際に、そのトランザクションの間でどの程度のデータの整合性を保つかを制御するための機能です。

# トランザクション分離レベルの設定方法

トランザクション分離レベルは、以下のコマンドで設定します。

```sql
SET TRANSACTION ISOLATION LEVEL <分離レベル>
```

例えば、`READ COMMITTED`に設定する場合は以下のようになります。

```sql
SET TRANSACTION ISOLATION LEVEL READ COMMITTED
```

# トランザクション分離レベルの種類

トランザクション分離レベルには以下の5つがあります。

- READ UNCOMMITTED
- READ COMMITTED
- REPEATABLE READ
- SERIALIZABLE
- SNAPSHOT

この中でSNAPSHOT分離レベルを使用するためには、まず対象のデータベースでSNAPSHOT分離のサポートを有効にする必要があります。
これはデータベース設定で行います。

```sql
ALTER DATABASE [データベース名] SET ALLOW_SNAPSHOT_ISOLATION ON;
```

SNAPSHOT分離レベルは、トランザクションが開始された時点のデータのスナップショットを使用して、他のトランザクションによる変更の影響を受けずにデータを読み取ることができます。
これにより、データの一貫性を保ちながら、同時実行性を向上させることができます。


## READ_COMMITTED_SNAPSHOT について

データベースオプションのREAD_COMMITTED_SNAPSHOTを有効にすることで、**READ COMMITTED**分離レベルでもスナップショットを使用するように設定することができます。

```sql
ALTER DATABASE [データベース名] SET READ_COMMITTED_SNAPSHOT ON;
```

これにより、`READ COMMITTED`分離レベルのトランザクションが、共有ロックを取得せずにデータを読み取ることができます。

# トランザクション分離レベルの挙動まとめ

上記5つのトランザクション分離レベルと、READ_COMMITTED_SNAPSHOTのON/OFFの違いでの各挙動をまとめると以下のようになります。

| 分離レベル | ダーティーリード | ノンリピータブルリード | ファントムリード | SELECT時のロックの種類・ロックの範囲 | ロックの保持期間 | 他トランザクションとの更新競合によるエラーについて | 備考|
|------------|------------|------------|------------|------------|------------|------------|------------|
| READ UNCOMMITTED | 発生する | 発生する | 発生する | 読み取り対象のテーブルに対してのSch-Sのみ | SELECT句の実行中 | 発生しない | |
| READ COMMITTED <br> (READ_COMMITTED_SNAPSHOT=OFF) | 発生しない | 発生する | 発生する | 読み取り対象のテーブルに対してのSch-S + 対象レコードのSロック | SELECT句の実行中 | 発生しない | SQL Serverのデフォルト |
| READ COMMITTED <br> (READ_COMMITTED_SNAPSHOT=ON) | 発生しない | 発生する | 発生する | 読み取り対象のテーブルに対してのSch-Sのみ | SELECT句の実行中 | 発生しない | SQL Databaseのデフォルト |
| REPEATABLE READ | 発生しない | 発生しない | 発生する | 読み取り対象のテーブルに対してのSch-S + 対象レコードのSロック | トランザクション終了まで | 発生しない | |
| SERIALIZABLE | 発生しない | 発生しない | 発生しない | 読み取り対象のテーブルに対してのSch-S + 対象レコードのSロック + 存在しないレコードへのロックや範囲ロック | トランザクション終了まで | 発生しない | |
| SNAPSHOT | 発生しない | 発生しない | 発生しない | 読み取り対象のテーブルに対してのSch-S のみ | SELECT句の実行中 | エラーコード 3960 や 3961のエラーが発生する可能性がある | |

:::note info
- ダーティリード・ノンリピータブルリード・ファントムリードについて
  - ダーティリード・・・あるトランザクションが、まだコミット（確定）されていない他のトランザクションの変更を読み取ることを指す
  - ノンリピータブルリード・・・同一トランザクション内で、同じデータを2回以上読み取った際に、そのデータが他のトランザクションによって変更されていることを指す
  - ファントムリード・・・あるトランザクションがクエリを実行した結果、他のトランザクションがその間にデータを挿入、削除、変更しており、その結果として異なる行がクエリの結果に現れることを指す
- ロックの種類・範囲について
  - 状況によってはページロックやテーブルロックになることもある
  - 行などの下位リソースにロックをかける際に上位リソースに対するインテント共有ロックも発生しうる
  - Sch-S (スキーマ安定性ロック) は Sch-M(スキーマ修正ロック)としか競合しないSQL Serverにおける最小のロックモード
- スナップショットを使用している場合、TempDBの負荷増加や、Skipped Ghosted Recordsの増加などがあり得る
  - 参考: 
    - [SQL Serverのスナップショット分離レベル導入によるデータ基盤連携の課題解決](https://techblog.zozo.com/entry/sqlserver-transaction-isolation-level-snapshot)
    - [SQL Serverにおけるスナップショット分離レベルに関するトラブルシューティング](https://techblog.zozo.com/entry/sqlserver-transaction-isolation-level-snapshot)
:::

以降は上記表のそれぞれの挙動を実際に確認した記録となります。

# それぞれのトランザクション分離レベルでの挙動の確認

実際にトランザクション分離レベルを変更しながらクエリを実行し、挙動を確認しました。

:::note info
使用したSQL Serverのバージョン
Microsoft SQL Server 2016 (SP3-GDR) (KB5021129) - 13.0.6430.49 (X64)   Jan 22 2023 17:38:22   Copyright (c) Microsoft Corporation  Enterprise Edition (64-bit) on Windows Server 2016 Standard 10.0 <X64> (Build 14393: ) (Hypervisor) 
:::

## ダーティリード・ノンリピータブルリード・ファントムリードの発生有無確認

### READ UNCOMMITTED

<table>
    <thead>
        <tr>
            <th>セッションA</th> <th>セッションB</th> <th>説明</th>
        </tr>
    </thead>
    <tr>
        <td> 
            <code>
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED <br>
BEGIN TRAN <br>
SELECT IntValue FROM [dbo].[_SampleTable] WHERE ID = 1 <br>
            </code>
-> 結果 100が返ってくる
        </td>
        <td></td>
        <td>セッションAでのトランザクションの開始及び初期値の確認</td>
    </tr>
    <tr>
        <td></td>
        <td>
            <code>
BEGIN TRAN<br>
UPDATE [dbo].[_SampleTable] SET IntValue = 101 WHERE ID = 1
            </code>
        </td>
        <td>セッションBのトランザクション内でレコードを更新</td>
    </tr>
    <tr>
        <td> 
            <code>
SELECT IntValue FROM [dbo].[_SampleTable] WHERE ID = 1 <br>
            </code>
-> 結果 101が返ってくる <br>
<b>ダーティリードの発生</b>
        </td>
        <td></td>
        <td>コミットされていない結果がセッションAから見えてしまう</td>
    </tr>
    <tr>
        <td></td>
        <td>
            <code>
ROLLBACK
            </code>
        </td>
        <td></td>
    </tr>
    <tr>
        <td></td>
        <td>
            <code>
BEGIN TRAN<br>
UPDATE [dbo].[_SampleTable] SET IntValue = 102 WHERE ID = 1<br>
COMMIT
            </code>
        </td>
        <td>セッションBでレコードの更新をコミット</td>
    </tr>
    <tr>
        <td> 
            <code>
SELECT IntValue FROM [dbo].[_SampleTable] WHERE ID = 1 <br>
            </code>
-> 結果 102が返ってくる <br>
<b>ノンリピータブルリードの発生</b>
        </td>
        <td></td>
        <td>セッションAのトランザクション内ではレコードを更新していないのに値が変わってしまっている</td>
    </tr>
    <tr>
        <td> 
            <code>
SELECT IntValue FROM [dbo].[_SampleTable] WHERE ID = 1000 <br>
            </code>
-> 結果 は無し <br>
        </td>
        <td></td>
        <td>ID1000のレコードが存在しないことの確認</td>
    </tr>
    <tr>
        <td> 
        </td>
        <td>
            <code>
BEGIN TRAN<br>
INSERT INTO [dbo].[_SampleTable](ID, IntValue)VALUES(1000, 1100)<br>
COMMIT
            </code>
        </td>
        <td></td>
    </tr>
    <tr>
        <td> 
            <code>
SELECT IntValue FROM [dbo].[_SampleTable] WHERE ID = 1 <br>
            </code>
-> 結果 1100が返ってくる <br>
<b>ファントムリードの発生</b>
        </td>
        <td></td>
        <td>別のトランザクションによるレコードの追加が見えてしまっている</td>
    </tr>
    <tr>
        <td> 
        </td>
        <td>
            <code>
BEGIN TRAN<br>
DELETE FROM [dbo].[_SampleTable] WHERE ID = 1000<br>
COMMIT
            </code>
        </td>
        <td>セッションBの後片付け</td>
    </tr>
    <tr>
        <td> 
            <code>
ROLLBACK
            </code>
        </td>
        <td></td>
        <td>セッションAのトランザクション終了</td>
    </tr>
</table>

### READ COMMITTED (READ_COMMITTED_SNAPSHOT=OFF)

<table>
    <thead>
        <tr>
            <th>セッションA</th> <th>セッションB</th> <th>説明</th>
        </tr>
    </thead>
    <tr>
        <td> 
            <code>
SET TRANSACTION ISOLATION LEVEL READ COMMITTED <br>
BEGIN TRAN <br>
SELECT IntValue FROM [dbo].[_SampleTable] WHERE ID = 1 <br>
            </code>
-> 結果 100が返ってくる
        </td>
        <td></td>
        <td>セッションAでのトランザクションの開始及び初期値の確認</td>
    </tr>
    <tr>
        <td></td>
        <td>
            <code>
BEGIN TRAN<br>
UPDATE [dbo].[_SampleTable] SET IntValue = 101 WHERE ID = 1
            </code>
        </td>
        <td>セッションBのトランザクション内でレコードを更新</td>
    </tr>
    <tr>
        <td> 
            <code>
SELECT IntValue FROM [dbo].[_SampleTable] WHERE ID = 1 <br>
            </code>
-> 結果はセッションBのトランザクションが閉じるまで返ってこない <br>
        </td>
        <td></td>
        <td>セッションBで取得済みの排他ロックとセッションAで取得したい共有ロックが競合するため</td>
    </tr>
    <tr>
        <td>
        セッションB側のROLLBACKと同時に結果100が返ってくる
        </td>
        <td>
            <code>
ROLLBACK
            </code>
        </td>
        <td></td>
    </tr>
    <tr>
        <td></td>
        <td>
            <code>
BEGIN TRAN<br>
UPDATE [dbo].[_SampleTable] SET IntValue = 102 WHERE ID = 1<br>
COMMIT
            </code>
        </td>
        <td>セッションBでレコードの更新をコミット</td>
    </tr>
    <tr>
        <td> 
            <code>
SELECT IntValue FROM [dbo].[_SampleTable] WHERE ID = 1 <br>
            </code>
-> 結果 102が返ってくる <br>
<b>ノンリピータブルリードの発生</b>
        </td>
        <td></td>
        <td>セッションAのトランザクション内ではレコードを更新していないのに値が変わってしまっている</td>
    </tr>
    <tr>
        <td> 
            <code>
SELECT IntValue FROM [dbo].[_SampleTable] WHERE ID = 1000 <br>
            </code>
-> 結果 は無し <br>
        </td>
        <td></td>
        <td>ID1000のレコードが存在しないことの確認</td>
    </tr>
    <tr>
        <td> 
        </td>
        <td>
            <code>
BEGIN TRAN<br>
INSERT INTO [dbo].[_SampleTable](ID, IntValue)VALUES(1000, 1100)<br>
COMMIT
            </code>
        </td>
        <td></td>
    </tr>
    <tr>
        <td> 
            <code>
SELECT IntValue FROM [dbo].[_SampleTable] WHERE ID = 1 <br>
            </code>
-> 結果 1100が返ってくる <br>
<b>ファントムリードの発生</b>
        </td>
        <td></td>
        <td>別のトランザクションによるレコードの追加が見えてしまっている</td>
    </tr>
    <tr>
        <td> 
        </td>
        <td>
            <code>
BEGIN TRAN<br>
DELETE FROM [dbo].[_SampleTable] WHERE ID = 1000<br>
COMMIT
            </code>
        </td>
        <td>セッションBの後片付け</td>
    </tr>
    <tr>
        <td> 
            <code>
ROLLBACK
            </code>
        </td>
        <td></td>
        <td>セッションAのトランザクション終了</td>
    </tr>
</table>


### READ COMMITTED (READ_COMMITTED_SNAPSHOT=ON)

<table>
    <thead>
        <tr>
            <th>セッションA</th> <th>セッションB</th> <th>説明</th>
        </tr>
    </thead>
    <tr>
        <td> 
            <code>
SET TRANSACTION ISOLATION LEVEL READ COMMITTED <br>
BEGIN TRAN <br>
SELECT IntValue FROM [dbo].[_SampleTable] WHERE ID = 1 <br>
            </code>
-> 結果 100が返ってくる
        </td>
        <td></td>
        <td>セッションAでのトランザクションの開始及び初期値の確認</td>
    </tr>
    <tr>
        <td></td>
        <td>
            <code>
BEGIN TRAN<br>
UPDATE [dbo].[_SampleTable] SET IntValue = 101 WHERE ID = 1
            </code>
        </td>
        <td>セッションBのトランザクション内でレコードを更新</td>
    </tr>
    <tr>
        <td> 
            <code>
SELECT IntValue FROM [dbo].[_SampleTable] WHERE ID = 1 <br>
            </code>
-> 結果 100が返ってくる <br>
        </td>
        <td></td>
        <td></td>
    </tr>
    <tr>
        <td>
        </td>
        <td>
            <code>
ROLLBACK
            </code>
        </td>
        <td></td>
    </tr>
    <tr>
        <td></td>
        <td>
            <code>
BEGIN TRAN<br>
UPDATE [dbo].[_SampleTable] SET IntValue = 102 WHERE ID = 1<br>
COMMIT
            </code>
        </td>
        <td>セッションBでレコードの更新をコミット</td>
    </tr>
    <tr>
        <td> 
            <code>
SELECT IntValue FROM [dbo].[_SampleTable] WHERE ID = 1 <br>
            </code>
-> 結果 102が返ってくる <br>
<b>ノンリピータブルリードの発生</b>
        </td>
        <td></td>
        <td>セッションAのトランザクション内ではレコードを更新していないのに値が変わってしまっている</td>
    </tr>
    <tr>
        <td> 
            <code>
SELECT IntValue FROM [dbo].[_SampleTable] WHERE ID = 1000 <br>
            </code>
-> 結果 は無し <br>
        </td>
        <td></td>
        <td>ID1000のレコードが存在しないことの確認</td>
    </tr>
    <tr>
        <td> 
        </td>
        <td>
            <code>
BEGIN TRAN<br>
INSERT INTO [dbo].[_SampleTable](ID, IntValue)VALUES(1000, 1100)<br>
COMMIT
            </code>
        </td>
        <td></td>
    </tr>
    <tr>
        <td> 
            <code>
SELECT IntValue FROM [dbo].[_SampleTable] WHERE ID = 1 <br>
            </code>
-> 結果 1100が返ってくる <br>
<b>ファントムリードの発生</b>
        </td>
        <td></td>
        <td>別のトランザクションによるレコードの追加が見えてしまっている</td>
    </tr>
    <tr>
        <td> 
        </td>
        <td>
            <code>
BEGIN TRAN<br>
DELETE FROM [dbo].[_SampleTable] WHERE ID = 1000<br>
COMMIT
            </code>
        </td>
        <td>セッションBの後片付け</td>
    </tr>
    <tr>
        <td> 
            <code>
ROLLBACK
            </code>
        </td>
        <td></td>
        <td>セッションAのトランザクション終了</td>
    </tr>
</table>

### REPEATABLE READ

<table>
    <thead>
        <tr>
            <th>セッションA</th> <th>セッションB</th> <th>説明</th>
        </tr>
    </thead>
    <tr>
        <td> 
            <code>
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ <br>
BEGIN TRAN <br>
SELECT IntValue FROM [dbo].[_SampleTable] WHERE ID = 1 <br>
            </code>
-> 結果 100が返ってくる
        </td>
        <td></td>
        <td>セッションAでのトランザクションの開始及び初期値の確認</td>
    </tr>
    <tr>
        <td></td>
        <td>
            <code>
BEGIN TRAN<br>
UPDATE [dbo].[_SampleTable] SET IntValue = 101 WHERE ID = 1<br>
            </code>
-> セッションAのトランザクションで共有ロックが取得されたままとなっているため、セッションAのトランザクションが終了するまで更新ができない
        </td>
        <td>他トランザクションで更新できないためダーティリードもノンリピータブルリードも発生しない</td>
    </tr>
    <tr>
        <td></td>
        <td>
        クエリの実行を中止して
            <code>
ROLLBACK
            </code>
        </td>
        <td></td>
    </tr>
    <tr>
        <td> 
            <code>
SELECT IntValue FROM [dbo].[_SampleTable] WHERE ID = 1000 <br>
            </code>
-> 結果 は無し <br>
        </td>
        <td></td>
        <td>ID1000のレコードが存在しないことの確認</td>
    </tr>
    <tr>
        <td> 
        </td>
        <td>
            <code>
BEGIN TRAN<br>
INSERT INTO [dbo].[_SampleTable](ID, IntValue)VALUES(1000, 1100)<br>
COMMIT
            </code>
        </td>
        <td></td>
    </tr>
    <tr>
        <td> 
            <code>
SELECT IntValue FROM [dbo].[_SampleTable] WHERE ID = 1 <br>
            </code>
-> 結果 1100が返ってくる <br>
<b>ファントムリードの発生</b>
        </td>
        <td></td>
        <td>別のトランザクションによるレコードの追加が見えてしまっている</td>
    </tr>
    <tr>
        <td> 
            <code>
ROLLBACK
            </code>
        </td>
        <td>
            <code>
BEGIN TRAN<br>
DELETE FROM [dbo].[_SampleTable] WHERE ID = 1000<br>
COMMIT
            </code>
        </td>
        <td>セッションBの後片付け、セッションAのトランザクション終了</td>
    </tr>
</table>


### SERIALIZABLE

<table>
    <thead>
        <tr>
            <th>セッションA</th> <th>セッションB</th> <th>説明</th>
        </tr>
    </thead>
    <tr>
        <td> 
            <code>
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE <br>
BEGIN TRAN <br>
SELECT IntValue FROM [dbo].[_SampleTable] WHERE ID = 1 <br>
            </code>
-> 結果 100が返ってくる
        </td>
        <td></td>
        <td>セッションAでのトランザクションの開始及び初期値の確認</td>
    </tr>
    <tr>
        <td></td>
        <td>
            <code>
BEGIN TRAN<br>
UPDATE [dbo].[_SampleTable] SET IntValue = 101 WHERE ID = 1
            </code>
-> セッションAのトランザクションで共有ロックが取得されたままとなっているため、セッションAのトランザクションが終了するまで更新ができない
        </td>
        <td>他トランザクションで更新できないためダーティリードもノンリピータブルリードも発生しない</td>
    </tr>
    <tr>
        <td>
        </td>
        <td>
        クエリの実行を中止して
            <code>
ROLLBACK
            </code>
        </td>
        <td></td>
    </tr>
    <tr>
        <td> 
            <code>
SELECT IntValue FROM [dbo].[_SampleTable] WHERE ID = 1000 <br>
            </code>
-> 結果 は無し <br>
        </td>
        <td></td>
        <td>ID1000のレコードが存在しないことの確認</td>
    </tr>
    <tr>
        <td> 
        </td>
        <td>
            <code>
BEGIN TRAN<br>
INSERT INTO [dbo].[_SampleTable](ID, IntValue)VALUES(1000, 1100)<br>
            </code>
-> セッションAのトランザクションでID=1000に対する共有ロックが取得されているため、セッションAのトランザクションが終了するまでINSERTができない
        </td>
        <td>他トランザクションでレコード追加できないためファントムリードは発生しない</td>
    </tr>
    <tr>
        <td>
        </td>
        <td>
        クエリの実行を中止して
            <code>
ROLLBACK
            </code>
        </td>
        <td></td>
    </tr>
    <tr>
        <td> 
            <code>
ROLLBACK
            </code>
        </td>
        <td></td>
        <td>セッションAのトランザクション終了</td>
    </tr>
</table>

### SNAPSHOT

<table>
    <thead>
        <tr>
            <th>セッションA</th> <th>セッションB</th> <th>説明</th>
        </tr>
    </thead>
    <tr>
        <td> 
            <code>
SET TRANSACTION ISOLATION LEVEL SNAPSHOT <br>
BEGIN TRAN <br>
SELECT IntValue FROM [dbo].[_SampleTable] WHERE ID = 1 <br>
            </code>
-> 結果 100が返ってくる
        </td>
        <td></td>
        <td>セッションAでのトランザクションの開始及び初期値の確認</td>
    </tr>
    <tr>
        <td></td>
        <td>
            <code>
BEGIN TRAN<br>
UPDATE [dbo].[_SampleTable] SET IntValue = 101 WHERE ID = 1
            </code>
        </td>
        <td>セッションBのトランザクション内でレコードを更新</td>
    </tr>
    <tr>
        <td> 
            <code>
SELECT IntValue FROM [dbo].[_SampleTable] WHERE ID = 1 <br>
            </code>
-> 結果 100が返ってくる <br>
        </td>
        <td></td>
        <td></td>
    </tr>
    <tr>
        <td></td>
        <td>
            <code>
ROLLBACK
            </code>
        </td>
        <td>ダーティリードは発生しない</td>
    </tr>
    <tr>
        <td></td>
        <td>
            <code>
BEGIN TRAN<br>
UPDATE [dbo].[_SampleTable] SET IntValue = 102 WHERE ID = 1<br>
COMMIT
            </code>
        </td>
        <td>セッションBでレコードの更新をコミット</td>
    </tr>
    <tr>
        <td> 
            <code>
SELECT IntValue FROM [dbo].[_SampleTable] WHERE ID = 1 <br>
            </code>
-> 結果 100が返ってくる <br>
        </td>
        <td></td>
        <td>ノンリピータブルリードは発生しない</td>
    </tr>
    <tr>
        <td> 
            <code>
SELECT IntValue FROM [dbo].[_SampleTable] WHERE ID = 1000 <br>
            </code>
-> 結果 は無し <br>
        </td>
        <td></td>
        <td>ID1000のレコードが存在しないことの確認</td>
    </tr>
    <tr>
        <td> 
        </td>
        <td>
            <code>
BEGIN TRAN<br>
INSERT INTO [dbo].[_SampleTable](ID, IntValue)VALUES(1000, 1100)<br>
COMMIT
            </code>
        </td>
        <td></td>
    </tr>
    <tr>
        <td> 
            <code>
SELECT IntValue FROM [dbo].[_SampleTable] WHERE ID = 1 <br>
            </code>
-> 結果はなし <br>
        </td>
        <td></td>
        <td>ファントムリードは発生しない</td>
    </tr>
    <tr>
        <td> 
        </td>
        <td>
            <code>
BEGIN TRAN<br>
DELETE FROM [dbo].[_SampleTable] WHERE ID = 1000<br>
COMMIT
            </code>
        </td>
        <td>セッションBの後片付け</td>
    </tr>
    <tr>
        <td> 
            <code>
ROLLBACK
            </code>
        </td>
        <td></td>
        <td>セッションAのトランザクション終了</td>
    </tr>
</table>

## SELECTした時のロックの種類・範囲・保持期間の確認

トランザクション分離レベルによって、SELECT時のロックの種類・範囲・保持期間は異なります。
それぞれのトランザクション分離レベルで下記クエリを実行した上で次の情報を確認し、ロック周りの挙動を確認します。

```sql
SET TRANSACTION ISOLATION LEVEL [トランザクション分離レベル]
BEGIN TRAN
SELECT IntValue FROM [dbo].[_SampleTable] WHERE ID = 1
```
- 拡張イベント
  - ロックの取得や解放を記録して確認可能
- sys.dm_tran_locks
  - 現在アクティブなロック要求を確認可能

### READ UNCOMMITTED

- 拡張イベントを確認した結果
  - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299916/0e4e428a-89c6-cfef-0560-07624c361f7a.png)
  - ObjectID: 203147769 (_SampleTable)に対するSch-Sロックが一時的に取得されてすぐに解放されている
  - ObjectID 0 に対してのSch-Sロックも取得されている
    - これはSELECT句終了後も解放されず、トランザクション終了後に解放されている
    - ObjectID 0 はシステムオブジェクトであるため、詳細不明
      - 経験上問題になったことはないので基本的競合すること自体ないor少ないのではないかと思われる
- sys.dm_tran_locksを確認した結果
  - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299916/a42a1609-595c-8dff-d1f0-3da48bff84f3.png)
  - 上記 ObjectID 0 に対してのSch-Sロックのみ残存

### READ COMMITTED (READ_COMMITTED_SNAPSHOT=OFF)

- 拡張イベントを確認した結果
  - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299916/e9076e0a-b279-18f8-eb94-f9dc09b1650b.png)
  - KEYに対しての共有ロックが取得され、その後解放されている
- sys.dm_tran_locksを確認した結果
  - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299916/003b37c5-7bd7-b770-3e45-9098a2415f9b.png)
  - READ UNCOMMITTEDの時と同様の状態

### READ COMMITTED (READ_COMMITTED_SNAPSHOT=ON)

- 拡張イベントを確認した結果
  - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299916/ec3e7967-e6c8-4183-1807-ef577b5c8a97.png)
  - KEYに対しての共有ロックは発生せず、Sch-Sロックが取得され、解放されている
- sys.dm_tran_locksを確認した結果
  - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299916/ad67c74b-35fb-509c-40b5-40d9371aa050.png)
  - READ UNCOMMITTEDの時と同様の状態


### REPEATABLE READ

- 拡張イベントを確認した結果
  - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299916/d799eb34-6495-4395-781a-545aa092a7c0.png)
  - KEYに対しての共有ロックが取得され、解放されていない
- sys.dm_tran_locksを確認した結果
  - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299916/ddf09396-22cc-53bc-f1a0-5e189ae04d75.png)
  - KEYの共有ロックが維持されている


### SERIALIZABLE

- 拡張イベントを確認した結果
  - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299916/18887313-8237-b895-e325-2563e9ec9f1b.png)
  - KEYに対しての共有ロックが取得され、解放されていない
- sys.dm_tran_locksを確認した結果
  - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299916/41bd8f9d-2157-db83-d329-4422d537d897.png)
  - KEYの共有ロックが維持されている
  - SERIALIZABLE の場合、存在しないレコードに対してSELECTした場合もそのレコードに対する共有ロックが取得される
    - 実際に追加でSELECTした例
    - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299916/c491a7a5-802a-bb5f-7788-ccb75f59b3d5.png)


### SNAPSHOT

- 拡張イベントを確認した結果
  - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299916/66e83467-9e61-7c3c-247d-419fed332e43.png)
  - ObjectID: 203147769 (_SampleTable)に対するSch-Sロックが一時的に取得されてすぐに解放されている
- sys.dm_tran_locksを確認した結果
  - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299916/5a123a08-9582-299a-baef-742c1a2f5c8e.png)
  - READ UNCOMMITTEDの時と同様の状態

## 他のトランザクションとの更新時競合について

次のように他のトランザクションで更新されたレコードを更新するようなケースがあるとします。
<table>
    <thead>
        <tr>
            <th>セッションA</th> <th>セッションB</th> <th>説明</th>
        </tr>
    </thead>
    <tr>
        <td> 
            <code>
SET TRANSACTION ISOLATION LEVEL SERIALIABLE <br>
BEGIN TRAN <br>
            </code>
        </td>
        <td></td>
        <td>セッションAでのトランザクションの開始及び初期値の確認</td>
    </tr>
    <tr>
        <td></td>
        <td>
            <code>
BEGIN TRAN<br>
UPDATE [dbo].[_SampleTable] SET IntValue = 201 WHERE ID = 2<br>
COMMIT
            </code>
        </td>
        <td>セッションBのトランザクション内でレコードを更新</td>
    </tr>
    <tr>
        <td> 
            <code>
UPDATE [dbo].[_SampleTable] SET IntValue = 202 WHERE ID = 2<br>
            </code>
        </td>
        <td></td>
        <td>セッションBで更新されたレコードに対してセッションAで更新しようとする</td>
    </tr>
</table>

`READ UNCOMMITTED` , `READ COMMITTED` , `REPEATABLE READ` , `SERIALIZABLE` の場合は更新が成功しますが、`SNAPSHOT` の場合は次のようなエラーが生じます。

```
メッセージ 3960、レベル 16、状態 3、行 31
更新の競合により、スナップショット分離トランザクションが中断しました。スナップショット分離を使用してデータベース 'sample_db' のテーブル 'dbo._SampleTable' に直接または間接的にアクセスし、別のトランザクションによって変更または削除された行を更新、削除、または挿入することはできません。トランザクションを再試行するか、更新/削除ステートメントの分離レベルを変更してください。
```

したがって、エラーコード3960が発生したらリトライするなど、プログラム側で制御する必要があります。