- トランザクション は、データベースで複数の操作を 1 つの 原子的な単位 として実行するための仕組みで、読み取り・書き込み・更新・削除を含む
- MySQL と Postgres は
begin; と commit; でトランザクションを制御し、失敗やエラー時には rollback; で変更を取り消す
- 両データベースとも 一貫した読み取り(consistent read) を保証するが、Postgres は 多版行管理(MVCC) を、MySQL は undo log を使用する
- 分離レベル(isolation level) はトランザクション間のデータ干渉を制御し、Serializable から Read Uncommitted までの 4 段階に分かれる
- Postgres と MySQL は 同時書き込みの競合 を異なる方法で処理し、Postgres は楽観的検証を、MySQL は 行レベルロック(row-level locking) を使用する
トランザクションの基本概念
- トランザクションは、データベースで複数の SQL 操作を 1 つの 原子的実行単位 にまとめる仕組み
begin; で開始し、commit; で終了し、その間に複数のクエリを実行できる
commit; の時点で、すべての変更が一度に適用される
- 予期しない障害(電源断、ディスク障害など)や意図的な取り消し時には
rollback; で変更を巻き戻す
- Postgres は WAL(Write-Ahead Log) により復旧を支援する
- トランザクション中に変更されたデータは 分離 され、他のセッションからは見えない
rollback; 時にはすべての変更が取り消され、データベースは元の状態に復元される
一貫した読み取り(Consistent Reads)
- トランザクションは、実行中に外部の変更の影響を受けない 一貫したデータビュー を維持する必要がある
- MySQL と Postgres は
REPEATABLE READ モード以上でこれをサポートするが、実装方式は異なる
- Postgres: 多版行管理(MVCC) により各行のバージョンを管理
- MySQL: undo log を使って過去のバージョンを再構成
Postgres の多版行管理
- 行が更新されるたびに新しいバージョンが生成され、古いバージョンには
xmax、新しいバージョンには xmin としてトランザクション ID が記録される
- トランザクションがコミットされる前は、他のセッションは変更内容を見ることができない
- コミット後は、新しいバージョンがデータベース全体に反映される
rollback; 時には変更が破棄され、元のデータが維持される
- 古い行バージョンは
VACUUM FULL コマンドで整理され、ストレージ領域が回収される
MySQL の Undo Log
- MySQL は行を直接上書きするが、undo log に以前の値を記録することで、必要に応じて復元できる
- 各行は
xid(直近の更新トランザクション ID) と ptr(undo log ポインタ) をメタデータとして持つ
- 複数のトランザクションが同時に実行される場合、undo log によって各トランザクションが必要なバージョンを選択的に参照する
- 同一行に複数の undo log 記録が存在することがあり、トランザクション ID を基準に適切なバージョンを選択する
分離レベル(Isolation Levels)
- トランザクション間のデータ干渉を制御する設定で、Serializable → Repeatable Read → Read Committed → Read Uncommitted の順に緩和される
- Serializable: すべてのトランザクションが順次実行されたかのように動作する
- Repeatable Read: 同じクエリを再実行したとき結果は同一だが、phantom read は起こりうる
- Read Committed: すでにコミットされた他トランザクションの変更を読める
- Read Uncommitted: dirty read を許容する、最も低い保護レベルだが性能は高い
同時書き込み(Concurrent Writes)
- 2 つのトランザクションが同じ行を同時に変更するときの処理方式は、データベースごとに異なる
MySQL: 行レベルロック(Row-level Locking)
- 共有ロック(S lock) は複数のトランザクションによる同時読み取りを可能にする
- 排他ロック(X lock) は 1 つのトランザクションだけが行を変更できる
SERIALIZABLE モードでは、すべての更新時に X lock を取得する必要があり、競合時には デッドロック(deadlock) が発生する可能性がある
- MySQL はデッドロックを検知し、片方のトランザクションを終了させる
Postgres: Serializable Snapshot Isolation
- Postgres は predicate lock を使用して、行集合単位でアクセスを追跡する
- 例:
WHERE id BETWEEN 10 AND 20 条件に対するロック
- 実際のアクセスを遮断せず、競合を検知して違反時にトランザクションを終了する
- 楽観的競合解決(optimistic conflict resolution) によりデッドロックを回避する
- MySQL と同様に、競合時には 1 つのトランザクションが終了し、アプリケーションはリトライロジックを実装する必要がある
結論
- トランザクションはデータベースの中核をなす要素であり、原子性・一貫性・分離性・永続性(ACID) を保証する
- Postgres と MySQL は、同じ目標を異なる内部構造で達成している
- 4 つの分離レベルとトランザクションの動作原理を理解すれば、データベースをより安定的に運用できる
1件のコメント
Hacker Newsの意見
この記事は少し物足りなく感じた
SQL標準で定義された**現象(phenomena)**を中心に分離レベルを説明するより、直列化可能性(serializability)の概念から始める方が直感的だと思う
直列化可能性はスレッドセーフ性を一般化したものと見なせて、これが失われると実行順序によって結果が変わるバグが生じる
データベースのさまざまな分離レベルはこの保証を緩めた形にすぎず、利用者が別の方法で保証を確保する必要がある
現象は非直列的な状況を可視化するための道具にすぎず、直列化可能性と直接結びつくものではない
たとえばKubernetesクラスターも、うまく設計されたコントローラーを使えば直列化可能に動作しうる
トランザクション、分離レベル、MVCCを、複数DB間の比較まで含めて一度に扱うのは非常に大きな作業だった
技術的な深さと読みやすさ、そして記事の長さのあいだでバランスを取ろうとした
もっと記法や引用があるとよいという意見
ただし不要なのに使うと、トランザクション間の調整コストが大きくなり、並行性とスループットが下がる
トランザクションは**copy-on-writeファイルシステム(btrfs, zfs)**のスナップショットのように考えることもできるが、Gitブランチでたとえる方が直感的だと思う
BEGINはブランチ作成、UPDATEはコミット、ROLLBACKはブランチ削除、COMMITは
git mergeと同じ衝突が起きるとDBは行単位でマージを試み、失敗した場合は設定に応じてロールバックするか強制マージする
READ UNCOMMITTEDは高速なマージを、SERIALIZABLEは正確性を優先する
この比喩は、トランザクションの概念を「なるほど!」と理解する助けになるかもしれない
多くの人が驚く点は、PostgresとMySQLがデフォルトで直列化可能モードではなく read-committedだということ
性能差は「少し」どころではなく、実際にははるかに大きい
read-committedを使うなら**ロック(lock)**管理に気を配る必要があり、UNIQUE制約も競合状態を防ぐのに必要になる
それでも、直列化可能モードの性能低下や再試行の問題を受け入れるくらいなら、このやり方の方を好む
参考: PostgreSQL公式ドキュメント
MySQLドキュメント、MariaDBドキュメント を参照
MyISAMはもうほとんど使われない
アプリケーション側でこれを検知し、再試行戦略を持つ必要がある
直列化可能モードは教科書では格好よく見えるが、実際にはほとんど使われない
最近の多くのデータベースツールは、ACIDよりリアルタイム更新の共有を優先している
たとえばAirtableはフィールドを修正すると同僚の画面にすぐ反映されるが、トランザクションがないためデータ不整合のリスクがある
関連内容は VisualDBのブログ記事 を参照
PlanetScaleブログを読むのは本当に楽しい
可視化にどんなツールを使ったのか気になる
可視化はjs + gsap(https://gsap.com)で作成した
このテーマに興味があるなら、**『Designing Data-Intensive Applications』**を強く勧める
さまざまな分離レベルだけでなく、ACID定義の曖昧さまで扱っている
第2版がもうすぐ出ると聞いた
PostgresのようなMVCCシステムのトランザクションは、copy-on-writeファイルシステムのスナップショットに似ている
BEGIN時点でデータのスナップショットを作り、UPDATEは自分専用のコピーにだけ反映される
ROLLBACK時にはコピーは破棄され、COMMIT時には新しいスナップショットが正式版になる
この比喩は、トランザクション概念を明確に理解するきっかけになるかもしれない
P.S. Gitブランチの比喩も使える
SELECTのあとにUPDATEするような場合、あるスレッドがブロックされることがある
今日、MySQLでこれを単一クエリに変えられるか試してみる予定
以前はバックエンドの面接でトランザクションをよく聞かれた
みんな使ったことはあるが、理解の深さは経歴によって違う
分離レベルを全部暗記していなくても、それぞれ異なる動作をすると知っているだけで好奇心やシステム理解度が見える
「phantom read」の説明は誤解を招くかもしれない
repeatable readでは既存行の値は変わらないが、新しい行が追加されることはある
既存行が変更・削除されることはないので、その点を明確にすべきだ
「xmin/xmaxとは関係ない」という文は不完全に感じる
コミット時の可視化がテーブルヘッダーを指しているのも奇妙だ
実際にはxmax/xminがコミット可否を判断する中核メカニズムではないのか?
サブトランザクションまで考えるとさらに複雑になる
それでも、可視化と説明は全体として楽しく読めた
分離レベルを理解するうえで重要なのに、まるで節が欠けているように思えた