2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

こんにちは、ポーラ・オルビスホールディングスで内製開発エンジニアをやっている高田です。

私の所属するITプロダクト開発チーム(内製開発のチーム)で各々持ち寄った技術テーマを1人10分ずつのLT形式で発表しあう勉強会を定期的に開催しています。

今回のLT会は「webアプリケーションを開発する上での技術を扱おう(基本的な内容でもOK)」という方針で、私はトランザクション分離レベルをテーマに選びました。

LT会の中では、RDBMSの中でも今我々のチームが触れているMySQLにフォーカスしてみました。

このテーマが調べてみると結構面白くて、かつ10分のLTの中では掘り下げきれなかった内容もあったので、せっかくなので記事にしてみました。

LT会で発表したこと

LT会では一般的なDBのACID特性や、トランザクション分離レベルについて軽く説明したあと、以下の表を対象にして、ダーティリード、ファジーリード(Non-repeatable read)、ファントムリードをMySQLでそれぞれ実際に起こしてみて、その結果を発表しました。

分離レベル ダーティリード ファジーリード ファントムリード
READ UNCOMMITTED 〇 *1
READ COMMITTED × *2 〇 *3
REPEATABLE READ × × *4
SERIALIZABLE × × ×
  • *1 : READ UNCOMMITTEDダーティリードが起こる事
  • *2 : READ COMMITTEDダーティリードが起こらないこと
  • *3 : READ COMMITTEDファジーリードが起こる事
  • *4 : REPEATABLE READファジーリードが起こらないこと

*1 ~ *4 のダーティリード・ファジーリードの検証は上記の表の通りの結果が得られたのですが、ファントムリードはMySQLのREPEATABLE READでは発生しませんでした。
READ COMMITTEDでは発生しました)

※ACID特性や、トランザクション分離レベルの説明についてはこの記事では割愛します

※MySQLのREPEATABLE READでのファントムリードの成否検証のエビデンスは後に掲載しています

その後調べたこと

なぜMySQLではREPEATABLE READでファントムリードが発生しない?

MySQLではREPEATABLE READでファントムリードが発生しなかった原因を調べてみたところ、MySQLではスナップショット読み取りで独立性を確保し、ロック読み取りをする場合はさらにギャップロックやネクストキーロックといったロックの仕様を用いてファントムリードを防止しているとのことです。

また、MySQLのデフォルトの分離レベルはこのREPEATABLE READなので、デフォルトの状態で強い独立性を持っていることが分かります。

参考 : https://dev.mysql.com/doc/refman/8.0/ja/innodb-transaction-isolation-levels.html

ほかのRDBMSではどう?

主要なRDBMSのデフォルトの分離レベルは以下の通り

RDBMS デフォルト分離レベル
MySQL REPEATABLE READ
PostgreSQL READ COMMITTED
Oracle READ COMMITTED
SQL Server READ COMMITTED

PostgreSQLではデフォルトの分離レベルはREAD COMMITTEDですが、REPEATABLE READではやはりトランザクション開始時点のスナップショットを見ることでファントムリードの発生を抑止しているとのことでした。

参考 :

ここまでで

  • RDBMSによってデフォルトの分離レベルは異なること
  • REPEATABLE READでもファントムリードが起こらない場合がある事

がわかりました。

実際に確かめてみたい

ここまでで実際に触って確かめたいと思ったことは以下の3つです。

  • MySQLのネクストキーロックの挙動
  • 各RDBMSのデフォルト分離レベルのファントムリードの成否
  • SERIALIZABLE以外にファントムリードを防げるレベルの有無

この記事では、2つ目と3つ目に対して、実際に主要RDBMSを触って検証していこうと思います。

SERIALIZABLE がファントムリードを防止できるのはSQL標準通りなので、この記事では割愛していきます。

また、1つ目の MySQLのネクストキーロックの挙動 については後日別の記事で触れていこうと思います。

各RDBMSのファントムリードの成否を検証

MySQL、PostgreSQL、Oracle、SQL Serverを対象に、デフォルトの分離レベルとREPEATABLE READを選択した時のファントムリードの成否を実施検証していきます。

MySQL

まず、MySQLのデフォルトの分離レベルがREPEATABLE READであることを確認します。

mysql> SELECT @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| REPEATABLE-READ         |
+-------------------------+

初期状態

mysql> SELECT * FROM items ORDER BY id;
+----+-------+
| id | value |
+----+-------+
|  1 |    90 |
|  2 |   200 |
|  3 |   300 |
|  4 |    99 |
|  5 |   999 |
+----+-------+

実施するテーブルの状態は上記の通りです。

SessionA

mysql> START TRANSACTION;
mysql> SELECT * FROM items WHERE value < 100;
+----+-------+
| id | value |
+----+-------+
|  1 |    90 |
|  4 |    99 |
+----+-------+

SessionAではvalue<100となるレコードを対象に参照していきます。

SessionB

mysql> START TRANSACTION;
mysql> INSERT INTO items (id, value) VALUES (6, 10);
mysql> COMMIT;

SessionBではvalue<100に該当するレコードを挿入してコミットします。

SessionA

mysql> SELECT * FROM items WHERE value < 100;
+----+-------+
| id | value |
+----+-------+
|  1 |    90 |
|  4 |    99 |
+----+-------+

mysql> COMMIT;

SessionBで挿入したレコードがSessionAでは参照されないことが確認できました。

これにてデフォルトの分離レベル=REPEATABLE READでファントムリードが発生しないことが確認できました。
またネクストキーロックの検証ではFOR UPDATEで読み取りロックをかけましたが、ロックをかけなくともファントム行が見えない仕様になっていることも確認できました。
(スナップショットを取っているため)

PostgreSQL

デフォルトの分離レベル(READ COMMITTED)

デフォルトの分離レベルがREAD COMMITTEDであることと、検証するテーブルの初期状態を確認します。

初期状態確認

testdb=# SHOW transaction_isolation;

 transaction_isolation
-----------------------
 read committed

testdb=# SELECT * FROM items ORDER BY id;

 id | value
----+-------
  1 |    90
  2 |   200
  3 |   300
  4 |    99
  5 |   999

テーブルの初期状態はMySQLで実施したときと同じです。

2つのセッションでファントムリードの発生を検証していきます。

SessionA

testdb=# BEGIN;
BEGIN
testdb=*# SELECT * FROM items WHERE value < 100 ORDER BY id;
 id | value
----+-------
  1 |    90
  4 |    99

先ほどと同様にSessionAではvalue<100となるレコードを対象に参照していきます。

SessionB

testdb=# BEGIN;
BEGIN
testdb=*# INSERT INTO items (id, value) VALUES (6, 10);
INSERT 0 1
testdb=*# COMMIT;
COMMIT

SessionBではvalue<100に該当するレコードを挿入してコミットします。

SessionA

testdb=*# SELECT * FROM items WHERE value < 100 ORDER BY id;
 id | value
----+-------
  1 |    90
  4 |    99
  6 |    10
(3 rows)

testdb=*# COMMIT;

今度はSessionBで挿入したレコードがSessionAから参照できてしまいました。

分離レベルがREAD COMMITTEDであるためファントムリードが発生しました。

ファントムリードが抑止できるレベル

今度は明示的に分離レベルをREPEATABLE READにして試してみましょう。

SessionA

testdb=# BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN
testdb=*# SELECT * FROM items WHERE value < 100 ORDER BY id;
 id | value
----+-------
  1 |    90
  4 |    99
  6 |    10
(3 rows)

先ほどと同様にSessionAではvalue<100となるレコードを対象に参照していきます。

SessionB

testdb=# BEGIN;
BEGIN
testdb=*# INSERT INTO items (id, value) VALUES (7, 20);
INSERT 0 1
testdb=*# COMMIT;
COMMIT

SessionBではvalue<100に該当するレコードを挿入してコミットします。

SessionA

testdb=*# SELECT * FROM items WHERE value < 100 ORDER BY id;
 id | value
----+-------
  1 |    90
  4 |    99
  6 |    10
(3 rows)

testdb=*# COMMIT;
COMMIT

testdb=# SELECT * FROM items WHERE value < 100 ORDER BY id;
 id | value
----+-------
  1 |    90
  4 |    99
  6 |    10
  7 |    20
(4 rows)

今度はSessionBで挿入したレコードがSessionAから参照されないことが確認できました。

明示的に分離レベルをREPEATABLE READにした場合は、ファントムリードが発生しませんでした。
(もちろんCOMMITした後は追加行が参照可能になります)

SQL Server

デフォルトの分離レベル(READ COMMITTED)

まずはデフォルトの分離レベルを確認します。

確認

1> DBCC USEROPTIONS;
2> GO
Set Option
                    Value
-------------------------------------------------------------------------------------------------------------------------------- ----------------------------------------------
textsize
                    4096
language
                    us_english
dateformat
                    mdy
datefirst
                    7
lock_timeout
                    -1
ansi_null_dflt_on
                    SET
ansi_warnings
                    SET
ansi_padding
                    SET
ansi_nulls
                    SET
concat_null_yields_null
                    SET
isolation level
                    read committed

分離レベルがREAD COMMITTEDであることが確認できました。

次に検証を行うテーブルを確認します。

テーブル確認

1> SELECT * FROM dbo.items ORDER BY id;
2> GO
id          value
----------- -----------
          1          90
          2         200
          3         300
          4          99
          5         999

これまでと同様に2つのセッションで検証していきます。

SessionA

1> BEGIN TRAN;
2> SELECT * FROM dbo.items WHERE value < 100 ORDER BY id;
3> GO
id          value
----------- -----------
          1          90
          4          99

SessionAではvalue<100となるレコードを対象に参照していきます。

SessionB

1> BEGIN TRAN;
2> INSERT INTO dbo.items(id, value) VALUES (6, 10);
3> COMMIT;
4> GO

SessionBではvalue<100に該当するレコードを挿入してコミットします。

SessionA

1> SELECT * FROM dbo.items WHERE value < 100 ORDER BY id;
2> GO
id          value
----------- -----------
          1          90
          4          99
          6          10

(3 rows affected)
1> COMMIT;
2> GO

SessionBで挿入したレコードがSessionAから参照できてしまいました。

分離レベルがREAD COMMITTEDであるためファントムリードが発生しました。

REPEATABLE READの場合

今度は分離レベルをREPEATABLE READにして検証していきます。

SessionA

1> SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
2> GO
1> BEGIN TRAN;
2> SELECT * FROM dbo.items WHERE value < 100 ORDER BY id;
3> GO
id          value
----------- -----------
          1          90
          4          99
          6          10

(3 rows affected)

明示的にREPEATABLE READを設定し、SessionAでvalue<100となるレコードを対象に参照していきます。

SessionB

1> BEGIN TRAN;
2> INSERT INTO dbo.items(id, value) VALUES (7, 20);
3> COMMIT;
4> GO

SessionBではvalue<100に該当するレコードを挿入してコミットします。

SessionA

1> SELECT * FROM dbo.items WHERE value < 100 ORDER BY id;
2> GO
id          value
----------- -----------
          1          90
          4          99
          6          10
          7          20

(4 rows affected)
1> COMMIT;
2> GO

SessionBで挿入したレコードがSessionAから参照できてしまいました。

SQL ServerではREPEATABLE READでもファントムリードが発生してしまいました。

ファントムリードが抑止できるレベル

SQL ServerではSERIALIZABLE以外にSNAPSHOTという分離レベルを用いることでファントムリードを防げるとのことだったので、SNAPSHOTを試してみます。
SNAPSHOTALTER DATABASEを実行して有効化する必要があるので、そこから実行していきます。

SessionA

1> ALTER DATABASE testdb SET ALLOW_SNAPSHOT_ISOLATION ON;
2> GO
1> SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
2> GO
1> BEGIN TRAN;
2> SELECT * FROM dbo.items WHERE value < 100 ORDER BY id;
3> GO
id          value
----------- -----------
          1          90
          4          99
          6          10
          7          20

明示的にSNAPSHOTを設定し、SessionAでvalue<100となるレコードを対象に参照していきます。

SessionB

1> BEGIN TRAN;
2> INSERT INTO dbo.items(id, value) VALUES (8, 10);
3> COMMIT;
4> GO

SessionBではvalue<100に該当するレコードを挿入してコミットします。

SessionA

1> SELECT * FROM dbo.items WHERE value < 100 ORDER BY id;
2> GO
id          value
----------- -----------
          1          90
          4          99
          6          10
          7          20

(4 rows affected)
1> COMMIT;
2> GO

今度はSessionBで挿入したレコードがSessionAから参照されないことが確認できました。

SQL Serverでは分離レベルをSNAPSHOTにした場合に、ファントムリードを防止することができました。

Oracle

デフォルトの分離レベル(READ COMMITTED)

まずは検証を行うテーブルを確認します。

テーブルの状態

SQL> SELECT * FROM items ORDER BY id;

        ID      VALUE
---------- ----------
         1         90
         2        200
         3        300
         4         99
         5        999

これまでと同様に、2つのセッションを使って検証を進めていきます。
(Oracleではトランザクションは暗黙的に開始されるため BEGIN に相当するクエリを省略できます。)

SessionA

SQL> SELECT * FROM items WHERE value < 100 ORDER BY id;

        ID      VALUE
---------- ----------
         1         90
         4         99

SessionAでvalue<100となるレコードを対象に参照していきます。

SessionB

SQL> INSERT INTO items (id, value) VALUES (6, 10);

1 row created.

SQL> COMMIT;

Commit complete.

SessionBではvalue<100に該当するレコードを挿入してコミットします。

SessionA

SQL> SELECT * FROM items WHERE value < 100 ORDER BY id;

        ID      VALUE
---------- ----------
         1         90
         4         99
         6         10

SessionBで挿入したレコードがSessionAから参照できてしまいました。

やはりデフォルトのREAD COMMITTEDではファントムリードが発生してしまいました。

ファントムリードが抑止できるレベル

OracleではREPEATABLE READがサポートされていませんでした。
READ ONLYがスナップショットを取るとのことなので、READ ONLYで検証してみました。

SessionA

SQL> SET TRANSACTION READ ONLY;

Transaction set.

SQL> SELECT * FROM items WHERE value < 100 ORDER BY id;

        ID      VALUE
---------- ----------
         1         90
         4         99
         6         10

明示的にREAD ONLYを設定し、SessionAでvalue<100となるレコードを対象に参照していきます。

SessionB

SQL> INSERT INTO items (id, value) VALUES (7, 20);

1 row created.

SQL> COMMIT;

Commit complete.

SessionBではvalue<100に該当するレコードを挿入してコミットします。

SessionA

SQL> SELECT * FROM items WHERE value < 100 ORDER BY id;

        ID      VALUE
---------- ----------
         1         90
         4         99
         6         10

今度はSessionBで挿入したレコードがSessionAから参照されないことが確認できました。

READ ONLYを設定することでファントムリードが抑止されました。

もちろんCOMMITしてトランザクションを一度閉じればid=7が見えるようになります

SQL> COMMIT;

Commit complete.

SQL> SELECT * FROM items WHERE value < 100 ORDER BY id;

        ID      VALUE
---------- ----------
         1         90
         4         99
         6         10
         7         20

わかったこと

各RDBMSのデフォルト分離レベルでのファントムリードの成否がわかりました。

MySQLではデフォルトの分離レベルでファントムリードが発生しないこと、
ほかのRDBMSはデフォルトの分離レベルではファントムリードが発生するものの、
SERIALIZABLE以外にもファントムリードを抑止できる分離レベルが実装されていることが分かりました。

まとめ

  • RDBMSごとにデフォルトの分離レベルも、REPEATABLE READ選択時の挙動も、ファントムリードの防ぎ方も異なる

各RDBMSのデフォルトの分離レベルとファントムリードの防ぎ方をまとめると以下のようになります。

RDBMS デフォルト分離レベル デフォルトでのファントムリード ファントムリードの抑止手段
MySQL REPEATABLE READ 発生しない デフォルトで抑止
PostgreSQL READ COMMITTED 発生する REPEATABLE READSERIALIZABLEの分離レベル
SQL Server READ COMMITTED 発生する SNAPSHOTSERIALIZABLEの分離レベル
Oracle READ COMMITTED 発生する READ ONLYSERIALIZABLEの分離レベル

感想

RDBMSの個性がとてもよく現れる検証となったと思います。

この結果に優劣はなく、あくまで独立性と並行性のバランス調整の結果であるため、この調査・検証の結果をプロジェクト・プロダクトの特性に合ったRDBMSを選定する視点として活かしていきたいと思います。

また、webアプリケーションの開発を行う身としては、RESTful × ステートレスな設計と実装を意識して、DBに整合性を丸投げしないこと、DBの内部実装に依存しないことを引き続き心がけていきたいと思いました。

あとこういう検証をしてみると普段触らないRDBMSを触る機会になって面白いですね。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?