回 5-3 道 5 データベースと分散
トランザクション は、 データベース への複数の変更を「全部やるか、何もしないか」の 1 まとまりにして扱う仕組みです。A から 300 円 を引いて B に足す振込が途中で止まると、まとめていなければ合計 1000 円 が 700 円 に減ります。控え (ログ) を先に書き、コミットの印で確定する形なら、どこで止まっても 1000 円 のままに戻せます。
振込が途中で止まっても壊れない理由と、2 人 が同時に書くと更新が消える理由を、コミットと分離レベルで切り分けられるようになります。
文 ゆめさくコード編集部 公開 2026年10月27日 更新 2026年10月27日 出典を確かめた日 2026年10月27日
振込が 途中で 止まったら、 お金は どこへ行くのか トランザクション は、 データベース への複数の変更を「全部やるか、何もしないか」の 1 まとまりにする仕組みです。この語はビジネスの取引や、日々の取引の記録 (トランザクションデータ) を指すこともありますが、この回で扱うのはデータベースの意味です。
A さんの口座から B さんの口座へ 300 円 を移すとき、データベースでは「A から 300 円 を引く」と「B に 300 円 を足す」の 2 つ を行います。1 つ 目の直後に電源が落ちたら、A からは 300 円 が消え、B には届いていません。お金がどこかへ消えたことになります。
トランザクションは、2 つ の変更を 1 つ の封筒に入れ、封をした (確定した) ときだけ両方を残します。この回では、お金が消える様子を Python で起こし、控え (ログ) を先に書けば消えないことを確かめます。
トランザクションは どう動くのか トランザクション は、始まりの印から確定の印までの変更をひとかたまりで扱います。確定をコミット、取り消しを ロールバック と呼びます。
BEGIN から COMMIT までが 1 つ SQL ( データベース に命令する言葉) では、BEGIN で始め、COMMIT で確定します。途中でやめるときは COMMIT の代わりに ROLLBACK を出すと、それまでの変更がすべて取り消されます。
振込を 1 つにまとめる
BEGIN;
UPDATE accounts
SET balance = balance - 300
WHERE id = 'A';
UPDATE accounts
SET balance = balance + 300
WHERE id = 'B';
COMMIT;PostgreSQL の公式の手引きは、トランザクションの要点を「複数の手順を、全部か無かの 1 つ の操作に束ねること」と書いています。途中の状態はほかから見えず、途中で失敗すれば、どの手順もデータベースに残りません。
BEGIN を書かない文も、1 文ずつが小さなトランザクションとして動き、成功すれば確定します。これを自動コミットと呼びます。1 文ずつは守られますが、2 文の間で止まると 1 文目だけが残ります。
控えを先に書く 止まっても壊れないのは、データベースが本体を書き換える前に、何をするかの控え (ログ) を先に残すからです。
PostgreSQL はこの控えを WAL (Write-Ahead Logging、先に書く記録) と呼びます。公式文書は、データの ファイル を書き換えてよいのは、その変更を書いた WAL の記録がディスクに書かれたあとだけだと定めています。止まったあとは、控えを読み直して次のように整えます。
止まった時点 起き直したとき コミットの印より前 途中までの 変更を 無かったことにする コミットの印より後 全部の 変更が 残る 形にそろえる
この頁の Python は、取り消し用の「直す前の値」も控えに書く、いちばん単純な形にしています。PostgreSQL は書き換える前の行を残しておき、コミットの印が無いトランザクションが作った行を見せないことで、同じ「無かったこと」を実現します。
ACID の 4 つ トランザクションが守る性質は、頭文字を取って ACID と呼ばれます。
性質 守ること 振込で言うと A 原子性 (Atomicity) 全部 やるか、 何もしないか 引いたのに 足さない、が残らない C 一貫性 (Consistency) 決めた 決まりを破らない 残高が 0 円未満にならない I 分離性 (Isolation) 途中の 状態を ほかから見せない 引いた 直後の 残高を 他人が読まない D 永続性 (Durability) 確定したら 停電でも消えない COMMIT の あとの 700 円は残る
A と D を支えるのが前の節の控えです。I は、同時に動くトランザクションどうしをどこまで切り離すかで、あとの節の分離レベルがそれです。
動かして確かめる 1 本 目は、 トランザクション を使わずに振込を書きます。A は 1000 円 、B は 0 円 から始め、300 円 を移します。止まる側は、A から引いた直後に例外 (停電の代わり) を投げます。
動かす まとめずに振り込む
実行すると、次の通りに出ます
{'A': 700, 'B': 300} 合計 1000
止まった 停電
{'A': 700, 'B': 0} 合計 700止まらなければ合計は 1000 円 です。引いた直後に止まると、A は 700 円 、B は 0 円 で、合計は 700 円 になりました。300 円 がどこにも無い状態が残っています。
2 本 目は、控えを先に書く形です。振込を、A の控え、A を書く、B の控え、B を書く、コミットの印の 5 手に分け、0 手から 5 手までのそれぞれで止めます。起き直したら、控えにコミットの印があれば新しい値でそろえ (やり直し)、無ければ控えを後ろから読んで古い値に戻します (取り消し)。
動かす 控えを先に書いて止める
実行すると、次の通りに出ます
手 直後 -> 戻した後
0 1000+0 -> 1000+0 取り消し
1 1000+0 -> 1000+0 取り消し
2 700+0 -> 1000+0 取り消し
3 700+0 -> 1000+0 取り消し
4 700+300 -> 1000+0 取り消し
5 700+300 -> 700+300 やり直し2 手や 3 手で止まった直後は、1 本 目と同じ 700+0 です。それでも控えに A の古い値 1000 があるので、起き直すと 1000+0 に戻せました。
4 手の行では、A も B も書き終えて 700+300 なのに、コミットの印が無いので取り消されました。確定を決めるのは「本体を書き終えたか」ではなく「コミットの印が控えに残ったか」です。5 手まで進んだときだけ 700+300 が残りました。
どの行も、戻した後の合計は 1000 円 です。止まる位置がどこでも、全部やったか何もしなかったかのどちらかになります。これが、残高の移し替えが途中で止まっても壊れない理由です。
上の図は注文の明細と合計を 2 文で直す例で、落ちる位置を動かすと、自動コミットのときだけ両者がずれます。
同時に 書くと、 なぜ更新が 消えるのか トランザクション で包んでも、2 人 が同じ残高を同時に読んで書くと、片方の更新が消えることがあります。これを lost update (更新の消失) と呼びます。
3 本 目は、A の 1000 円 から、田中さんが 300 円 、鈴木さんが 200 円 をほぼ同時に引き出す場面です。2 人 とも先に残高を読み、手元で引き算をしてから書きます。
動かす 2 人が同じ残高を読む
実行すると、次の通りに出ます
2 人とも読んだ 1000 1000
田中が書いた 700
鈴木が書いた 800
正しくは 500正しくは 500 円 のはずが、800 円 になりました。鈴木さんが古い 1000 円 から計算した値で上書きし、田中さんの 300 円 が消えています。2 人 の書き込みは 1 つ ずつなら正しく確定しているので、原子性と永続性ではこれを防げません。防ぐのは分離性の側です。
分離レベルで 何が防げるか 分離レベルは、同時に動くトランザクションどうしを、どこまで切り離すかの度合いです。 SQL の標準は 4 段 を決めていて、PostgreSQL の公式文書は、段ごとに起こり得る現象を次の表にしています (表 13.1 を訳した物)。
分離レベル ダーティリード 反復不能読み取り ファントム 直列化の異常 READ UNCOMMITTED 標準は 許すが PostgreSQL では起きない 起こり得る 起こり得る 起こり得る READ COMMITTED 起きない 起こり得る 起こり得る 起こり得る REPEATABLE READ 起きない 起きない 標準は 許すが PostgreSQL では起きない 起こり得る SERIALIZABLE 起きない 起きない 起きない 起きない
ダーティリードは、まだコミットしていない他人の変更を読むこと 反復不能読み取りは、同じ行を 2 回 読むと、間に他人がコミットした変更で値が変わっていること ファントムは、同じ条件で 2 回 探すと、当たる行の集まりが変わっていること 直列化の異常は、結果が 1 つ ずつ順に流したどの順番とも合わないこと PostgreSQL の既定は READ COMMITTED で、READ UNCOMMITTED を頼んでも中では READ COMMITTED と同じに動きます。
lost update はこの表の列にありません。PostgreSQL では、書き方と分離レベルで次のように分かれます。
書き方と 分離レベル 2 人目の書き込み 更新 READ COMMITTED で、 読んだ 値から 計算して書く そのまま 上書きする 消える READ COMMITTED で、balance = balance - 200 と 1 文で書く 1 人目の コミットを 待ち、 直った あとの 行で 計算し直す 消えない REPEATABLE READ か SERIALIZABLE 先を 越された と分かると失敗する 消えない (やり直しが要る)
公式文書によると、REPEATABLE READ では、自分が始まったあとに他人が書き換えた行を書き換えられず、could not serialize access due to concurrent update の失敗が返ります。アプリはトランザクションを始めからやり直し、新しい値を読み直して計算します。
4 本 目は、この「先を越されたら失敗し、読み直してやり直す」形をまねます。行に版の番号 (書かれた回数) を持たせ、読んだときの版と今の版が違えば書き込みを断ります。
動かす 版を確かめてやり直す
実行すると、次の通りに出ます
田中 1000 -> 700
鈴木 1000 -> 800
確かめない 800
田中 1000 -> 700
鈴木 衝突
鈴木 700 -> 500
確かめる 500版を確かめないと、3 本 目と同じく 800 円 です。確かめると、鈴木さんは 1 度「衝突」で断られ、700 円 を読み直して 500 円 を書きました。
崩れる前提は「読んでから書くまで値は変わらない」です。また段を上げると失敗がふつうに起きるので、やり直しを書いていないアプリは守れません。
トランザクションは 仕事で どう 確かめるのか 仕事では、どこまでを 1 つ の トランザクション にしたかと、同時に来たときの動きを、試験とコードの読み合わせで確かめます。
2 つ 以上の値を一緒に直す処理 (振込、注文と在庫) が、1 つ の BEGIN から COMMIT の中にあるかを見る。道具が自動で BEGIN を出すこともあるので、道具の文書も確かめる 試験で、2 文の間にわざと例外を投げ、合計が始める前と同じかを見る (1 本 目と 2 本 目と同じ確かめ方) 「読んでから書く」所を探し、1 文で書くか、分離レベルを上げて失敗したらやり直すコードにする トランザクションを開いたまま、人の操作や外の API を待たない PostgreSQL では、古い行を片付ける掃除 (VACUUM) が裏で動いています。これが追いつかないと何が起きるかが、次の実例です。
関わる実例 トランザクション ID の 周回で 書き込みが 止まった Sentry (2015年7月20日) エラーを集めるサービス Sentry は、2015年 7月 20日 の月曜日、米国の仕事の時間のほとんどで止まりました。2015年 7月 23日 に David Cramer が公開した事後報告によると、原因は PostgreSQL の トランザクション ID (XID) でした。
PostgreSQL は、途中の状態をほかから見せない仕組みを、トランザクションごとの番号で作っています。行には作ったトランザクションの番号が付き、自分より新しい番号の行は「未来の行」として見せません。公式文書によると、この番号は 32 ビットで、40 億を超えるトランザクションを流すと 1 周します。比べは周回を前提にした計算で、どの番号から見ても 20 億が古い側、20 億が新しい側です。そこで掃除が、十分に古い行に「凍結」の印を付けて回ります。
掃除が追いつかないと、周回まで残り 4000 万で警告が出て、残り 300 万を切ると新しい番号を配らなくなります。データが消えるのを防ぐため、書き込みを断る守りです。
Sentry は書き込みがとても多く、自動の掃除の設定を何度も見直していましたが、この守りが働いて書き込みが止まりました。自動の掃除を待って 3 時間 近くを費やしても書き込みは戻らず、調べると 1 つ の巨大な表だけ掃除が終わっていませんでした。使い道の限られた表だったので空にする (TRUNCATE) と決め、5 分 後に全体が戻ったと書いています。その後、掃除を早く始める設定 (autovacuum_freeze_max_age を 500000000) と、掃除の担当を 6 つ にする設定に変えました。
崩れた前提は「トランザクションの番号はいくらでも使える」です。分離性の土台の番号は有限で、掃除が追いつくことが前提でした。
出典は Transaction ID Wraparound in Postgres (Sentry Blog) と Routine Vacuuming (PostgreSQL 公式文書) です。
よくある問い BEGIN を 書かないと、 トランザクションは 無いのですか あります。PostgreSQL は BEGIN の無い文を 1 文ずつ小さな トランザクション として包み、成功すれば確定します (自動コミット)。ただし 2 文の間で止まると 1 文目だけが残るので、まとめたい変更は BEGIN と COMMIT で囲みます。
トランザクションの 一部だけを 取り消せますか セーブポイントを使えばできます。SAVEPOINT で印を置き、ROLLBACK TO でその印まで戻すと、印より前の変更を残したまま後ろだけを取り消せます。外側を COMMIT するまで、どの変更もほかの接続には見えません。
トランザクションデータや トランザクションファイルとは何ですか 注文や入出金のように、取引ごとに増えていく記録を指す言葉で、この回のトランザクションとは別の意味です。商品や顧客の一覧のようにあまり変わらない情報はマスターデータと呼んで分けます。トランザクションデータを書き込むときに、この回のトランザクションを使う関係です。
@Transactional とは何ですか Java の Spring などで、 関数 に付けると、その中の処理を 1 つ のトランザクションで包む印です。自分で BEGIN と COMMIT を書く代わりに、道具が関数の始まりと終わりで出します。
確認クイズ 答えだと 思う 選択肢を 押すと、 その場で 正誤と 理由が 開きます。 4 問すべてに 正しく 答えると、 この 回は 修了で、 石が 緑に 変わります (この 端末の ブラウザに 残ります)。 間違えた 問いは、 あとから 解き直せます。
1 問目
トランザクション の途中で止まったとき、変更を始める前の状態に戻すことを何と呼びますか。
ア コミット イ セーブポイント ウ ロールバック エ 自動コミット 2 問目
このコードを動かすと何が出ますか。
2 人が読んでから書く
bal = 500
a = bal # 1 人目が読む
b = bal # 2 人目が読む
bal = a + 100
bal = b - 50
print(bal)3 問目
この頁の 2 本 目のコードで、4 手目 (A も B も書き終え、コミットの印はまだ) で止まったとき、起き直した後の残高はどうなりましたか。
ア A は 700 円、 B は 300 円のまま残る イ A は 700 円、 B は 0 円になる ウ A は 1000 円、 B は 300 円になる エ A は 1000 円、 B は 0 円に戻る 4 問目
2015年 7月 の Sentry の障害で、PostgreSQL が書き込みを受け付けなくなった直接の理由はどれですか。
ア トランザクション ID が 周回の 点に 近づき、 データが 消えるのを 防ぐ 守りが 働いたから イ 2 人が 同じ行を 同時に 書き換え、 更新が 消えたから ウ 分離レベルを SERIALIZABLE に して、 すべての 書き込みが 失敗したから エ ディスクの 空きが 無くなり、 WAL を 書けなくなったから 説明課題 A の口座から B の口座へ 300 円 を移す途中で電源が落ちても、お金が消えない理由を、「控え (ログ)」「コミットの印」「取り消し」という言葉を使って、3〜4 文で説明してください。書いたら、下の模範解答を開いて比べてください。
あなたの説明
模範解答 模範解答を開く 自分の 説明を 書いてから 開いて、 比べてください。
振込は「A から引く」と「B に足す」の 2 つ の変更で、1 つ 目だけで止まると合計 1000 円 が 700 円 に減ってしまう。 トランザクション では、本体を書き換える前に変更の控え (ログ) をディスクに書き、2 つ の変更のあとにコミットの印を控えに残す。起き直したとき、コミットの印があれば両方の変更が残る形にそろえ、無ければ控えをもとに途中の変更の取り消しを行う。そのため止まった位置がどこでも、全部やったか何もしなかったかのどちらかになり、合計は 1000 円 のまま変わらない。
注文と 在庫、 明細と 合計のように、2 つ 以上の 値を 一緒に 直す処理は 仕事の あちこちに あります。 どこまでを 1 つ の トランザクションに するか、 同時に 来たら 何が 起きるかを 言えると、 設計の 見直しや 障害の 調べで 役に 立ちます。
もっと深く この 回の 続きを、 ゆめさくの コースの 章や 道具で 深められます。 頁の 中の 確認クイズに 答えた あと、 手を 動かす量を 増や したい ときに 使ってください。
一次資料 どれも 2026年 10月 27日 に開いて、題と中身を確かめました。