コンテンツにスキップ

トランザクション・分離レベルの振り返り・解説

トピック入口と開始文へ戻る。以下の数値例・図は概念説明で、本人の実測ではありません。

ChatGPT開始文
https://github.com/MFQWKMR4/tech2026 のCHATGPT.md、AGENTS.mdと次を読んでください。
- src/content/docs/database/transactions.mdx
- reviews/database-transactions.yaml
- sessions/2026-09-17-database-transactions-01.yaml
- public/diagrams/database-transactions/isolation.svg
- public/diagrams/database-transactions/write-skew.svg
- src/content/docs/practice/reflections/2026-09-17-database-transactions-01.md
database-transactions のlearnとして、気になる節を一つ確認してから、説明→疑問→具体例→理解確認を一つずつ進めてください。
指定がなければneeds_revisitから始め、独力の回答とヒント・解説後を区別してください。
参照できたファイルとrevisionを示し、読めないものや未確認の事実を補完しないでください。
図の内容を読むことと描画の確認、教材検証と私の実績を区別してください。
開始時刻を確認できれば記録し、40分付近で区切りを提案してください。
終了時は既存テンプレートのYAML+Session narrativeで[Session Sync]をMFQWKMR4/tech2026へ作成し、URLを返してください。
作成できなければ未作成と明示してコピー可能な本文を返してください。

最終レビュー:2026-09-17 · 説明した(Explained)。ヒント後の理解と自力の説明を区別して記録しています。

次回、自力で確かめたいこと

  • 2026-09-17 (2026-09-17-database-transactions-01): READ COMMITTED / REPEATABLE READ / SERIALIZABLEを、同じ具体シナリオで『何が見えるか』『いつ待つか』『いつabortするか』『retry要否』まで再確認する。今回REPEATABLE READとSERIALIZABLEは説明を受けながら理解した部分が多い。
  • 2026-09-17 (2026-09-17-database-transactions-01): FOR UPDATE / NOWAIT / SKIP LOCKED / optimistic locking / 条件付きUPDATEを、競合率・latency・connection pool・retryコストの観点で使い分ける練習を別条件で再確認する。
  • 2026-09-17 (2026-09-17-database-transactions-01): write skewとSERIALIZABLEのread/write dependency検出を、別の複数行不変条件の例で自力説明できるか確認する。
  • 2026-09-17 (2026-09-17-database-transactions-01): 高競合時に条件付きUPDATEを第一候補にできるケースと、明示ロック・queueing・data model変更が必要なケースの境界を整理する。
  • 2026-09-17 (2026-09-17-database-transactions-01): deadlock(複数行を異なる順序でロックするケース)は終了時に未出題のまま残したため、次回扱う候補とする。
  • 2026-09-17 (2026-09-17-database-transactions-01): 楽観ロックのUPDATEも未commitの競合更新を待ち得る。commit済みversion不一致の0件と、競合writerの待機を別条件で確認する(Issueの待機不要という説明は一般化しない)。

2026-09-17のセッション

2026-09-17 00:21 EDT開始。途中で睡眠・日をまたぐ中断を挟み、2026-09-19と2026-09-21に再開して終了。残り1個の在庫購入を題材に、transaction境界、Atomicity、行ロック、SELECT FOR UPDATE、NOWAIT/SKIP LOCKED、条件付きUPDATE、optimistic/pessimistic locking、READ COMMITTED/REPEATABLE READ/SERIALIZABLE、write skew、高競合時のtrade-offまで確認した。外部決済APIはtransaction境界の確認として一部扱ったが、本人からDB transaction/lockを深めたい旨がありDBへ戻した。説明前に示せた内容と、説明・誘導後に理解した内容を区別する。

初案は在庫更新と購入記録を同じtransactionへ。競合制御は解法説明へ移行。Atomicity、writer待機・通常SELECT、RCの再読を説明し、分離レベル・手段のtrade-offは再確認が必要。

問い・回答の流れをIssueで読む

回答で示せたこと(ヒントの有無を含む)
  • 2026-09-17 (2026-09-17-database-transactions-01): 在庫減算と購入レコード作成を同一transactionにし、片方失敗時に両方rollbackする必要性を自発的に説明した。回答:『同じトランザクションに入れればロールバックをして在庫の数は1のままだが、入れてなければ在庫は減っているのにパーチェイスは発生してない』『これは普通にトランザクションに入れるべき』。ヒント: なし(直前に障害条件のみ提示)。
  • 2026-09-17 (2026-09-17-database-transactions-01): ACIDのAtomicityを在庫減算と購入記録のセット成功/失敗に結び付けた。回答:『それはアトミスティかな。まあ在庫の減らすのと購入の記録はセットで行われるべき』。ヒント: なし。
  • 2026-09-17 (2026-09-17-database-transactions-01): Consistencyを業務上の不変条件として捉え、stockが0未満にならないことや在庫減算と購入記録の不整合を例示した。回答:『同時に更新しても在庫がゼロより下回ることはない』『在庫を減らしたのに購入レコードが作成できてないとかいう不整合を防ぐ』。ヒント: 問いで『業務上のルール』という観点提示あり。
  • 2026-09-17 (2026-09-17-database-transactions-01): DurabilityをCOMMIT後の永続化として説明した。回答:『コミットが成功した後にDBサーバーが落ちても、それはもう書き込まれていいから永続化が完了している』。ヒント: COMMIT後の障害条件提示あり。
  • 2026-09-17 (2026-09-17-database-transactions-01): SELECT FOR UPDATEでAが対象行をロックしているとき、BのUPDATEは待たされると自発的に説明した。回答:『Aが先にトランザクションを貼ってるんでしょ。だったら、排他制御入ってるからBは待たされる』。ヒント: なし。
  • 2026-09-17 (2026-09-17-database-transactions-01): FOR UPDATE中でも通常SELECTは読めること、未commitの0ではなくcommit済みの1を見ることを説明した。回答:『これは読めるんじゃないか。リードオンリーな処理なのでロックを取る必要がない』『1でしょう。だってまだコミットしてないから』。ヒント: なし。 通常SELECTは競合する行ロックを取らないという範囲。table lock不要という一般化はしない。
  • 2026-09-17 (2026-09-17-database-transactions-01): READ COMMITTEDで同一transaction内の2回目SELECTが、Aのcommit後に0を見得ると回答した。回答:『普通にロックとかする必要がないから、0が見えるんじゃないか』。ヒント: なし。
  • 2026-09-17 (2026-09-17-database-transactions-01): SKIP LOCKEDの用途について、候補が複数あるワーカー処理向きで、在庫1個では単にスキップされるだけと説明した。回答:『向いてるのは複数ワーカーの方じゃないか。在庫1個の方でやったらただスキップされて終わるだけ』。ヒント: SKIP LOCKEDの挙動説明後の確認。
  • 2026-09-17 (2026-09-17-database-transactions-01): optimistic lockingの既にコミット済みのversion不一致ならUPDATE 0件になると説明した(待機不要という一般化は採用しない)。回答:『更新されてるとしたらバージョン5でヒットしないから、待たなくていいし失敗もしなくていい。ただ更新量がゼロ件でしたみたいなことになりますよね』。ヒント: version列を使うSQL例を提示。
  • 2026-09-17 (2026-09-17-database-transactions-01): 条件付きUPDATEの同時実行では片方だけ成功すると考え、DB内部で競合が直列化される点を説明した。回答:『同時に実行したとしても片方だけ成功すると思います。それはもう内部で、内部では直列化してるだろうから』。ヒント: 直前に条件付きUPDATEの意味を説明済み。
  • 2026-09-17 (2026-09-17-database-transactions-01): 高競合SERIALIZABLEでserialization failure→retry→再競合の負の連鎖を自発的に認識した。回答:『リトライが増えるとさらに競合が増えるっていうことで』『負の連鎖が入りそう』。ヒント: 高競合時の影響を問うたのみ。
  • 2026-09-17 (2026-09-17-database-transactions-01): write skew回避のデータモデル案として、複数行に分散した『最低1人当直』条件を共有カウンタ行へ寄せる案を出した。回答:『当直の人数っていうのをレコードにすればいいのかな』。ヒント: 『競合transactionが必ず同じ1行を取り合うようにデータモデルを変える』という観点提示あり。
自力では説明しきれなかったこと
  • 2026-09-17 (2026-09-17-database-transactions-01): 最初の在庫1個の問いでは、transactionに在庫更新と購入レコード作成を含める案は出たが、同時実行時の競合制御(FOR UPDATE、条件付きUPDATE、isolation level等)は自発的には出なかった。回答:『購入レコードとその在庫の状態みたいなのを更新する処理をトランザクションにしますかね』。ヒント: その後に競合条件を追加して学習。
  • 2026-09-17 (2026-09-17-database-transactions-01): FOR UPDATEという具体的なSQL/ロック手段は最初は思い出せず、『わからない』と回答した。ヒント: その後SELECT ... FOR UPDATEを説明。
  • 2026-09-17 (2026-09-17-database-transactions-01): NOWAITの名称と用途は未習得で、名称は『聞いたことない』と回答。ヒント: その後NOWAITを説明。
  • 2026-09-17 (2026-09-17-database-transactions-01): REPEATABLE READという分離レベルは開始時点で未整理で、『repeatable read? なんだそれ』と回答。ヒント: その後snapshot固定と更新競合を説明。
  • 2026-09-17 (2026-09-17-database-transactions-01): SERIALIZABLEでもserialization failureによりtransaction全体のretryが必要な点は自発的には説明できなかった。回答:『リトライってそもそもいらないんじゃね』。ヒント: その後abort+retryを説明。
理由・設計を詰めたいこと
  • 2026-09-17 (2026-09-17-database-transactions-01): Isolationの説明は『二人の処理が混ざらない』『同じリソースなら直列化』という直感はあったが、分離レベルごとに許容する現象が異なる点までは自発的に整理できていなかった。ヒント: READ COMMITTED/REPEATABLE READ/SERIALIZABLEへ接続して説明。
  • 2026-09-17 (2026-09-17-database-transactions-01): 共有カウンタ1行と各行+SERIALIZABLEのtrade-offでは、hot rowの認識はあったが、待機・abort/retry・実装単純性・競合頻度の比較はまだ明確に言語化しきれなかった。回答:『トレードオフまではね、結構思いつかない』。ヒント: その後比較を整理。
  • 2026-09-17 (2026-09-17-database-transactions-01): optimistic/pessimistic lockingの適用先について、当初『競合がほとんど起きない通常の場合はSELECT FOR UPDATE』『最後の1個を大量ユーザーが取り合う場合は楽観ロック』という方向に傾いた。一般的な指針としては低競合でoptimistic、高競合ではpessimisticを検討しやすいことを説明した。ヒント: あり(説明後に整理)。 同期補足: 競合率だけで正誤を決めず、待機とretry等の比較が未整理な点を残す。
修正が必要な説明
  • 2026-09-17 (2026-09-17-database-transactions-01): SELECT FOR UPDATEについて、高競合時の比較で『ロックが取れなかった時点で失敗となりそう』と回答したが、通常のFOR UPDATEは即失敗ではなく待機する。ヒント: 回答後にNOWAITとの違いを説明。

9/17開始・9/21終了の面接振り返りに初案・支援の順序・役割別の評価をまとめています。途中に解法説明を含むセッションで、独力テストだけの結果ではありません。ここは仕組みを読む教材です。対象はPostgreSQL 18。挙動を他DBへそのまま当てはめないでください。

Default — 成功条件と競合の条件を分ける

Section titled “Default — 成功条件と競合の条件を分ける”

演習条件はstock=1、同じ商品をA/Bが購入、購入レコードは一購入一行。在庫減算と購入記録を一つのtransactionにすることで、途中失敗時に両方を取り消せる。これがAtomicityの役割。一方、通常SELECTで双方が1を読み、それぞれアプリ内で0を計算して書くだけでは、READ COMMITTEDで両方が購入を記録し得る。transaction境界だけで、アプリが古い値に基づいて判断する問題は消えない。

単純な一商品の減算なら、同じtransaction内で次の条件付きUPDATEを使い、RETURNINGが1行返ったときだけ購入を記録する。0行はこの例では売り切れまたは商品不存在なので、購入INSERTへ進まない。購入INSERTが失敗すればtransactionをrollbackする。実装では商品不存在と売り切れをどう返すかも決める。

BEGIN;
UPDATE inventory SET stock = stock - 1
WHERE product_id = 42 AND stock > 0
RETURNING product_id, stock;
-- アプリで結果を確認。1行のときだけ購入INSERT、0行なら購入せず終了。
-- INSERT失敗時はROLLBACK。両方成功時だけCOMMIT。

これは説明用SQLで、今回のDB実測ではない。stockのCHECK制約は負数防止を補助するが、購入記録との対応や外部決済まで自動保証しない。外部APIはDBのrollback対象外なので、冪等性と決済状態を別途設計する。

Why — 同じ行では誰が何を待つか

Section titled “Why — 同じ行では誰が何を待つか”

次の図は、Bが最初にstock=1を読んだ後、Aが1→0を更新してまだcommitしていない時点を比較する。全体の処理時間や待機時間を表す尺度ではない。

stock=1を読んだBがAのcommit前後に読む値と、条件付きUPDATEの結果を分離レベルで比較する概念図

図を拡大

Bの分離レベル Aが未commitの間の通常SELECT Aのcommit後の通常SELECT Bの条件付きUPDATE
READ COMMITTED(既定) commit済みの1 次のstatementでは0 Aの更新中なら待つ。commit後、更新された行でstock条件を再評価し0行
REPEATABLE READ Bのsnapshotの1 同じsnapshotの1 snapshot後にAが同じ行を更新commitしたためserialization failure。全transactionを再試行
SERIALIZABLE この例ではRRと同じ この例ではRRと同じ 同じ行の競合に加えて、別行にまたがるread/write依存も監視する

RRのsnapshotは単にBEGINした時点ではなく、最初のtransaction制御以外のstatementを基準にする。自分のtransactionの更新は自分で見える。Aがrollbackした場合は上表のcommit後とは結果が変わる。通常SELECTは行ロックと競合しないが、tableのACCESS SHAREは取るためDDL等との待機まで否定しない。PostgreSQL 18:分離レベルロック

Trade-off — 何を保護し、待機をどこへ置くか

Section titled “Trade-off — 何を保護し、待機をどこへ置くか”
方法 向く判断 支払うコスト・例外
条件付きUPDATE stockが正なら減らす等、条件を更新へ埋め込める UPDATEも行ロックを取り競合時は待つ。短い文でもcommitまで保持し得る
SELECT FOR UPDATE 読んだ行を基に複数処理を判断し、その行の変更を排他したい 通常は待機。往復と保持時間が増え、pool接続を占有。外部API待ちを含めない
NOWAIT 待てないときは失敗として呼出し元に返せる 行ロック取得不可なら即エラー。無制限retryは負荷を増やす
SKIP LOCKED 複数候補のjobを複数workerが分担する ロック中の候補が結果から欠ける。在庫1個では「売り切れ」と同義でなく、一般的な整合性確認に不向き
versionによる楽観ロック 読んでから考える時間が長く、衝突が少ない version不一致で0行なら再読・再判断。UPDATE自体はロック待機し得る
SERIALIZABLE + retry 複数行・検索条件にまたがる不変条件 読取り依存の監視とabort/retry。すべての関連書込みが同じ整合性方針に従う必要

「楽観だからロックを取らず待たない」は誤り。例えばAがversion=5を6へ未commit更新中なら、Bのversion=5付きUPDATEはRCではAを待ち、Aのcommit後に条件を再確認して0行になり得る。既にcommit済みの不一致を見て0行になる場合と区別する。これはIssue #24の説明を同期時に補足した点で、本人が既に再確認できたとは扱わない。

高競合では悲観ロック一択とも、楽観ロック一択とも言えない。保持時間・再試行可能性・許容latency・競合区間を短縮できるかを並べる。低競合のversion検査は無駄な排他を減らせるが、高競合で全員が計算をやり直すなら損が増える。SELECTのlocking句

Exception — 別行を更新しても壊れる当直ルール

Section titled “Exception — 別行を更新しても壊れる当直ルール”

演習条件はA/Bの2人がON、「最低1人ON」を守る。各transactionが相手のONを読んで、自分だけOFFへ更新する。

同じsnapshotではAもBも相手がONと読み、別行更新で両方OFFになり得るwrite skew

図を拡大

RRでは更新対象が別行なので、双方commitして0人になり得る。一人ずつ直列なら後から実行した側は相手のOFFを読んで退勤を取りやめるはずで、同じ結果を説明する直列順序がない。Serializableの依存監視は、このような異常につながる実行をabortさせる。どちらが、どのstatementで失敗するかは固定しない。predicate lockは「全読取りを排他してwriterを待たせる」ロックではない。Serializable

代案 不変条件の守り方 代償
テーブルの排他的な書込み制御 読取り判断の前に、他の更新と競合する適切なtable lockを取得 無関係な当直グループまで直列化しやすい。単に「table lock」という名称だけではモード不足
当直グループごとの共有カウンタ行 同じtransactionで人数が1より多いときだけ減らし、当人をOFFにする 集約行がhot row。個人状態と人数の二重管理を常に整合させる
全対象行を共通順序でロック ルールに関係する全行を読み、共通手順で判断・更新 対象の漏れや新規行の追加も考える。自分の行だけでは不十分
各医師行 + SERIALIZABLE 読取り条件と更新の依存から異常を防ぐ abortしたtransaction全体の再試行が必要。関連する処理全体の方針をそろえる

Production — 正しさに加え、待ちと再試行を制限する

Section titled “Production — 正しさに加え、待ちと再試行を制限する”

演習でpoolが50なら、1000リクエスト全部が同時にDB接続を保持するわけではない。最大50接続の中で行ロック待ちが増え、残りはpoolや入口で待つ。他APIとpoolを共用すれば影響が波及する。短いtransaction・入口の同時実行制限・待機期限を組み合わせ、売り切れ後に同じ仕事を続けない設計も検討する。

serialization failure(SQLSTATE 40001)は、最後のUPDATEだけでなく読取りと判断を含むtransaction全体をrollbackして再実行する。上限・backoff・jitter・リクエスト期限を設け、retryで外部副作用を重複させない。deadlockも全体の再実行やロック順序見直しが必要だが、今回の本人への問いは未出題だった。再試行の扱い

Troubleshooting — 0件と失敗と遅延を混ぜない

Section titled “Troubleshooting — 0件と失敗と遅延を混ぜない”

まずaffected rows=0の業務結果、SQLSTATE、pool取得待ち、DB内のlock wait、transaction時間を分ける。pg_stat_activityのwait_event_type/wait_event、pg_blocking_pids(pid)pg_locksで誰が何を保持しているか確認する。CPUが低いだけで正常とはしない。強制終了を最初の手段にせず、長時間transactionや接続解放漏れを確かめる。

補強元:2026-09-17-database-transactions-01 / Issue #24の4依頼。対象PostgreSQL 18、一次資料確認2026-09-21。図とSQLは概念説明、二接続による再実験・本人の独力再テストは未実施。