世界を動かす技術を、日本語で。

データベーストランザクションとは何ですか?

概要

  • SQLデータベースの根幹となる トランザクション の基本動作と重要性を解説
  • MySQLPostgres におけるトランザクション管理の違いと仕組み
  • 一貫性のある読み取り(Consistent Reads)や アイソレーションレベル の比較
  • 同時書き込み時の 競合処理 やロック機構の概要
  • データベース設計・運用で知っておくべき代表的なポイントを網羅

トランザクションの基本

  • トランザクション とは、複数のデータベース操作を 原子的な単位 でまとめて実行する処理単位
  • 典型的なトランザクションは、 読み取り・作成・更新・削除 など複数のクエリ実行を含む
  • MySQLやPostgresでは、begin;で開始しcommit;で確定、途中でrollback;による 取り消し も可能
  • トランザクション中の変更は コミット前は他セッションから不可視、コミットで一斉反映
  • 障害発生時 や意図的な中断時は、データベースが ロールバック復旧機構 (例:PostgresのWAL)で一貫性維持

トランザクションの同時実行とデータの可視性

  • 複数セッションが同じデータを扱う場合、 コミット前の変更は他セッションから見えない
  • ロールバック時は、 全ての変更がなかったこと になるため、他セッションへ影響を与えない
  • Consistent Reads (一貫性のある読み取り)で、各トランザクションは 独立したデータのスナップショット を参照

Postgresのマルチバージョン制御(MVCC)

  • Postgresは 行の複数バージョン を管理し、各バージョンにxmin(作成トランザクションID)とxmax(削除トランザクションID)を付与
  • 新しいトランザクションが行を更新すると、 新バージョンが生成 され、古いバージョンも一定期間保持
  • コミット前の変更は他トランザクションから不可視、コミット後にのみ新バージョンが参照可能
  • VACUUM FULL コマンドで、不要な古いバージョンを削除しテーブルを圧縮

MySQLのUndo Log方式

  • MySQLは 行データを即時上書き しつつ、 Undo Log で直近の変更履歴を保持
  • 各行にxid(最新更新トランザクションID)やptr(Undo Log参照)などのメタデータを付与
  • 他トランザクションからはUndo Logを参照して 必要な過去バージョンを動的復元
  • Postgresのような VACUUM操作は不要 だが、Undo Logの管理が重要

アイソレーションレベル

  • データベースは 4段階のアイソレーションレベル をサポート
    • Serializable :最も厳格。全トランザクションが直列実行されたかのように振る舞う
    • Repeatable Read :同一トランザクション内で同じSELECTは常に同じ結果
    • Read Committed :コミット済みの変更のみ参照可能。 非再現読取ファントムリード が発生
    • Read Uncommitted :未コミットの変更も参照可能。 ダーティリード が発生しやすい
  • アイソレーションレベルが高いほど 一貫性は高いがパフォーマンス低下 の可能性

ファントムリード・非再現読取・ダーティリード

  • ファントムリード :同一SELECTで結果が変化する現象。Repeatable Read以下で発生
  • 非再現読取 :同じ行を複数回SELECTし、他のコミット済み変更で値が変わる現象。Read Committed以下で発生
  • ダーティリード :未コミットの他トランザクション変更を読めてしまう現象。Read Uncommittedで発生

同時書き込み時の競合制御

  • MySQL行レベルロック で競合処理

    • 共有ロック(Sロック) :複数トランザクションが同時に取得可能(主に読取時)
    • 排他ロック(Xロック) :1つのトランザクションのみ取得可能(更新時)
  • Postgres や他DBも、アイソレーションレベルや内部実装により異なるロック戦略を採用

まとめ

  • トランザクション管理は データ整合性とパフォーマンスの両立 に不可欠
  • MySQLPostgres は異なる実装で同様の目的を達成
  • 適切な アイソレーションレベル 選択と ロック制御 理解が、堅牢なアプリケーション設計の鍵

Hackerたちの意見

planetscaleのブログを読むのは本当に楽しい!このビジュアライゼーションを作るのにどんなツールを使ってるのか気になるな。

ここで作者です。ありがとう!このビジュアルはjsとgsapで作ってるよ。(https://gsap.com)

この文章はちょっと物足りないな。他の記事と同じように、SQL標準で説明されている現象を通じて隔離レベルを紹介してるけど、もっと直感的なアプローチがあると思う。厳密な直列性の概念からこの問題空間を定義する方が、スムーズだと思うんだ。これはスレッドセーフの概念の一般化でもあるし、ソフトウェアエンジニアなら誰でも直感的に理解できるはず。直列性が欠けると、実行依存の挙動が起きて、診断が難しいバグが発生することが多い。だから、すべてのシステムは直列性を目指すべきで、データベースはそれを実現するためのツールになり得る。さまざまな非直列化レベルのデータベーストランザクションの隔離は、直列性の保証を緩和したもので、データベースがその保証を強制しなくなるから、ユーザーが他の手段でそれを確保する必要がある。隔離現象は、非直列性のさまざまなコーナーケースを視覚化するのに役立つツールだけど、必ずしもそれに結びついているわけではない。すべてのSQL現象を観察しながら直列性を達成することも可能だよ。例えば、慎重に書かれたコントローラーを持つKubernetesクラスターは直列性を持つことができる。

じゃあ、もっと良い説明を勧めてくれる?

https://aphyr.com/posts/327-jepsen-mariadb-galera-cluster もっと記号や引用があれば、もっと良くなるよ。

ここで作者です。いいフィードバックありがとう!トランザクション、隔離レベル、MVCCの組み合わせは、一度に全部カバーするのが本当に大変なんだ。特に、複数のDB間での比較を試みたから、技術的な深さと経験の少ない人へのアクセスのしやすさのバランスを常に考えてる。長々とした読み物にならないようにしないとね。

ほとんどのRDBMSは、必要なら直列化可能な隔離を提供してるよ。でも、実際にはあまり必要ないことが多い。直列化可能な隔離を不必要に使うと、トランザクション間の調整が増えて、同時実行性やスループットが減少するのがデメリットだね。

(厳密な) シリアライズ可能性の概念 [("S")] は、実際にはスレッドセーフの概念の一般化なんだよね。「厳密な」(L + S) が括弧に入ってる理由がよくわからないけど、リニアライズ可能性("L")がSMPシステムでの安全性に最も似てるってこと?

最近のデータベースツールは、トランザクションやACID特性よりも、更新の即時共有を優先してることが多いね。例えば、Airtableでは、フィールドを更新すると、同じテーブルを開いている同僚の画面にもすぐにその更新が表示される。でも、そのデメリットはAirtableがトランザクションを行わないこと。トランザクションを行わないことのデメリットは、データの不整合が起こる可能性があることだね。詳しくはこちらで: https://visualdb.com/blog/concurrencycontrol/

競合に対して一発かましながら、さりげなく製品の宣伝してるね。

これ、実はバックエンドエンジニアの面接で好きな質問の一つだったんだよね。みんなトランザクションは使ったことあるけど、経験によって理解度が違うんだよね。もちろん、隔離レベルを暗記してるとは期待しないけど、いくつかの種類があってそれぞれの動きが違うって知ってるだけでも、結構いい感じだし、物事の裏側に興味があるってことが伝わるよ。

名目上同じ隔離レベルでも、異なるデータベースシステムでは動作が異なることがあるから、一般的にはケースバイケースで詳細を調査する必要があるよ。

この段階では、xminやxmaxとは関係なく、他のトランザクションが未コミットのデータを見れないからなんだ。何か見落としてるのかな、これって不完全な説明じゃない?それに、コミットの可視化が変だと思うんだけど、テーブルのヘッダーを「指してる」けど、xmaxは「裏で」更新されるの?xmax/xminって「データベースがコミットされたかどうかを知るメカニズム」じゃないの?それに、サブトランザクションもあるから、この説明はさらに矛盾してるかも?でも、可視化や説明は楽しめたよ、ありがとう!

記事がxmax/xminの概念をすっ飛ばしてると思う。これらは異なる隔離レベルが実際にどう機能するかを理解するために基本的なものだからね。記事からセクションが意図せず落ちてしまったのかと疑うくらい、かなり違和感があるよ。

よく驚かれるのが、PostgresとMySQLがデフォルトでシリアライズ可能じゃないってこと(つまり、ACIDのIが完全じゃない)。彼らはリードコミットを使ってる。この記事ではそのことに触れてなかった気がするけど、見逃したのかな。記事ではリードコミットが「少し」パフォーマンスが良いって言ってるけど、私の経験ではかなり速かったよ。どこで聞いたか忘れたけど、その理由でこのデフォルトを選んだって言ってた気がする。リードコミットを使うってことは、ロックの詳細を考慮しなきゃいけないってことだね。UNIQUEは単に不正なデータ入力を防ぐだけじゃなくて、レースコンディションを避けるためにも必要なことがあるからね。でも、今はそれを知ってるから、シリアライズ可能のパフォーマンス低下を受け入れるよりは、リトライや他の注意点に対処する方がいいかな。https://www.postgresql.org/docs/current/transaction-iso.html

SERIALIZABLEの問題は、パフォーマンス以外にも、トランザクションが競合やデッドロック、タイムアウトで失敗する可能性があるから、アプリケーションコードはそういうケースを認識して、トランザクションをリトライする戦略を持っておく必要があるんだ。

PostgresやMySQLはデフォルトでシリアライズ可能じゃないけど、OracleやSQL Serverはリードコミットがデフォルトで、シリアライズ可能じゃないんだよね。シリアライズ可能は教科書では良さそうに見えるけど、実際にはあんまり使われてない。

最近のMySQLとMariaDBは、InnoDBテーブルに対してリピート可能読み取りがデフォルトで、リードコミットじゃないよね。: https://dev.mysql.com/doc/refman/8.4/en/set-transaction.html... https://mariadb.com/docs/server/reference/sql-statements/adm... MyISAMについてはよく知らないけど(そもそも誰が使ってるの?;-))。

クライアントサイドのプロジェクトで、何百万行ものSQLと何千人ものユーザーを扱うプロジェクトを、トランザクションを一つも追加せずに作ったよ。 :/

明示的なトランザクションがない場合、すべての挿入や更新はそれぞれ独自のトランザクション(自動コミット)になるよ。やることによっては、もっと必要ないかもしれないけど、これがトランザクションとして実行されることを知っておくのは大事だね。

SELECT FOR UPDATEは使ったことある?それとも依存データを更新する必要がなかっただけ?複雑な操作がストアドファンクションやプロシージャで実装されてるなら、トランザクションは暗黙的に行われるよ。データが単純で、循環参照がない一対多のCRUDだけなら、トランザクションなしでもできるし、テーブルのリレーションシップだけで整合性が保てるよ。

いいね。だけど、君のユースケースやスキーマ、クエリパターンについて何も言わなかったから、これが何を意味するのか全然わからないよ。いくつかのユースケースは明示的なトランザクションなしで簡単にできるし、何も失ってない。だけど、他の場合(たいていは高い同時書き込みや、複数のテーブルで同じデータに対する書き込み+読み込みで不変条件を強制する必要がある場合)では、トランザクションがかなり重要だよ。だから、状況によるね。

これってまだトランザクションだよね!OLTPのワークロードでは、明示的なBEGIN / COMMITなしで一つのクエリだけのトランザクションが大部分を占めることも珍しくないよ。これをオートコミットトランザクションとか、暗黙のトランザクションって呼ぶんだ。

このトピックに興味がある人には、「Designing Data Intensive Applications」って本を超おすすめするよ。https://www.goodreads.com/book/show/23463279-designing-data-.... いろんな隔離レベルだけじゃなくて、伝統的なACIDの定義における曖昧さについても触れてるんだ。第2版が出るのも近いと思うよ。

シリアライズ可能な読み取りに関するセクションで、TFAがaccountsbalanceを間違えてる。

うん、概念を誤解してたのか、記事にエラーがあったのかよくわからなかった。

ありがとう、直った!

ファントムリードっていうのは、トランザクションが同じSELECTを何度も実行するけど、2回目には違う結果が返ってくることを指すんだ > SQL標準では、リピート可能なリードレベルはファントムリードを許可してるけど、Postgresではまだできないんだ。これはちょっと悪い表現で、リピート可能なリードが異なる値を示すかもしれないって誤解を招く可能性があるよ。行の値は同じだけど、新しい行が2回目の結果セットに追加されるかもしれない。新しい行が重要なのは、以前に読まれた行が変更されたり削除されたりすることはないからだ — そうじゃないと、その行については2回目に繰り返しができないからね。