在庫は1個なのに、2人が買えた。SQLにエラーがなくても起きるバグの正体
一人で試すと正しく動く。それなのに、二人が同時に押すと在庫が壊れる。処理の間にもう一人が入る瞬間を、同じ一個の在庫で追ってみましょう。

この記事の目次
一人で買えた。それだけでは確かめられない
残り一個の限定品。Aさんが買えた。Bさんも買えた。画面にはどちらも注文完了が出ているのに、倉庫には一個しかない。SQLのつづりが正しくても、こういうバグは起こります。
SQLは、データベースへ出す指示の文です。今回の原因は、二人が同じ途中の状態を読めること。「在庫があるか確認する」と「在庫を減らす」が別々なら、その隙間へもう一人が入れる。まずはこの二つの処理を、時間の順に並べてみましょう。
一人ずつのテストでは、その隙間へ誰も入ってきません。何を売ったかより、同時に処理されたときにも「売った個数が在庫を超えない」という約束を守れるかが、今回の問いです。
二人とも「1」を読んだ後、何が起きる?
在庫qtyを1で始めます。AがSELECTで1を読む。その直後にBも1を読む。二人とも「買える」と判断して、順にqtyを1ずつ減らす。条件のないUPDATEなら、最後はマイナス1です。読み取った時点では二人とも正しくても、二回目に減らす時点では条件が変わっているんですね。
下の図は、この一個だけを追います。危ない側では、確認と変更が別の操作。条件を含めた側では、減らすUPDATE自身へ「qty > 0」を入れます。一件変更できた場合だけ購入成功とし、変更がゼロ件なら売り切れとして扱う。成功の判定まで変えるところが大切です。
これは実際の研究でも問題になる分野です。Warszawski・Bailis氏らのACIDRainは、四言語の12のECアプリを調べ、並行処理に関係する22の重大な問題を報告しました。文が正しいかだけでなく、複数の操作がどんな順番で重なるかを試す理由になります。[4]
「BEGINとCOMMITで囲めば大丈夫」と覚えるだけでも、隙間が残ります。Berenson氏らの研究は、分離レベルの定義と許される異常を批判的に整理しました。Adya氏らも、弱い隔離条件をどう記述するかを扱っています。トランザクションがあることと、読み取った条件が最後まで保たれることは、同じ保証ではないんです。[1][2]
「買える」の定義も、コードの外へ残さないようにします。更新がゼロ件だったのに画面が注文完了を出せば、在庫を守ったDBと、利用者へ約束したアプリが食い違う。処理の成功は、例外が出なかったかだけでなく、業務上の条件を満たしたかで決める必要があるわけです。
なお、二人とも読んだ古い値からqtyを0へ設定する別の実装では、最後の数値が0のままでも二件売れることがあります。在庫が負数かどうかだけを探しても、それは見つからない。今回の図の「1ずつ減らして−1」とは別の更新方法ですが、どちらも売った件数と在庫の関係を確認したい理由になります。
つまり、守りたい約束を先に書き、その約束に必要な競合の扱いを選びます。今回なら、在庫を減らす条件と結果の判定を一つの更新へ寄せる。一方、複数の品目、支払い、注文レコードが一緒に関わるなら、この一文だけでは足りないので、全体のトランザクションや再試行も設計します。
同じ一個の在庫を、二人の順番で追う
| 順番 | Aの処理 | Bの処理 | 在庫 |
|---|---|---|---|
| 開始 | ― | ― | 1 |
| 1 | 1を読み、買えると判断 | ― | 1 |
| 2 | ― | 1を読み、買えると判断 | 1 |
| 3 | 条件なしで1を減らす | ― | 0 |
| 4 | ― | 条件なしで1を減らす | -1 |
二人が読んだ値は同じ1。でも、二回減らすと在庫は−1。
強い保証には、待つ場面もある
競合した二人を正しい順へ並べるには、一方が待ったり、やり直したりする場面が出ます。読み取り専用の画面と、残り一個を売る処理では、必要な保証も違うでしょう。すべてへ同じ強い設定を付ける前に、何を守る操作かを分けます。
Bailis氏らのHighly Available Transactionsは、高い可用性の下でどの保証を提供できるかを整理しています。弱い保証がいつも悪い、という単純な結論にはなりません。何を優先し、何を許すかによって、成立する仕組みが変わる。その選択を、在庫の約束と結びつけるわけです。[3]
PostgreSQLのRead Committedでは、条件付きUPDATEが競合相手の更新を待った場合、更新された行に対して条件を評価し直す動作が説明されています。今回の単一行の例で条件をUPDATEに含める理由は、ここにあります。別のDBや分離レベルへ移すときは、同じ動作と決めつけず仕様を確認しましょう。[5]
練習用DBで、変更された行を確かめる
自分の練習用PostgreSQLがある場合は、下の一時テーブルを同じ接続で作り、条件付きUPDATEを二度実行します。一回目はqtyが0の一行、二回目は戻り値がゼロ行になるはずです。これは条件と成功判定を読む練習で、二接続を使った競合の再現とは分けています。
「エラーが出なかった」だけで両方を成功にすると、せっかくの条件が画面へ伝わりません。RETURNINGで返った行の有無、または変更件数を読み、ゼロ件なら売り切れへ進む。この分岐まで含めて、画面の結果を確かめます。
DBがない方は図を一手ずつ進め、AとBが同じ1を読める瞬間を見つけてください。仕組みが分かったら、次はSQLのWHEREや更新件数を学ぶ。実行できる環境を持つ前にも、問いを具体的にすることはできます。
CREATE TEMP TABLE demo_stock (id integer PRIMARY KEY, qty integer NOT NULL);
INSERT INTO demo_stock VALUES (1, 1);
UPDATE demo_stock SET qty = qty - 1
WHERE id = 1 AND qty > 0 RETURNING qty;
-- もう一度、同じ UPDATE を実行する。戻る行はある? 研究の補足と、よくある疑問
二回のUPDATEは、実際の同時実行を証明している?
コードは同じ接続で条件と変更件数を確認する練習です。同時実行時のロックや再評価を確かめるには、二接続と実行順を管理した別の検証が必要です。図はその順番を説明する模型です。[5]
出典と確認した範囲(5資料)
出典と、読み取れる範囲
原著の公開要旨・取得できた本文部分・研究レビューと、仕様を確認する公式資料を照合しています。対象と比較条件が違う結果を一つの効果へ合算せず、手元の操作例は編集上の説明例として区別します。確認範囲は資料ごとに示しています。
対象・方法:SQL-92の分離レベルの定義と複数の実装・現象を分析。
主な結果:名前と限定された異常現象だけでは振る舞いを十分に特徴づけられないことを示す。
限界:1995年の理論整理。具体的な製品・版の挙動は現行マニュアルと実験で確認する。
確認した範囲
Sciteの本文抜粋と書誌を確認。索引は別版へのnew edition通知も返すが、撤回として扱わない。 確認日:2026-10-29
Sciteにnew editionの関連通知(10.1145/568271.223785)あり。取得した通知は撤回・懸念通知ではありません。版差の全面照合は未実施。
対象・方法:分散データベースの分離を一般化して定義する理論研究。
主な結果:処理の順序や依存関係から異常を捉える枠組み。
限界:理論の定義を、任意製品の設定名と同一視しない。
確認した範囲
Sciteの書誌を確認。原著本文の全文取得は未実施。本文で本研究固有の効果量や実測値は引用しない。 確認日:2026-10-29
取得できた書誌で撤回・懸念通知は確認されていません。出版社の履歴を網羅した監査ではありません。
対象・方法:PVLDB 7(3)、2014年会議に採択された可用性と分離保証の理論・実験研究。
主な結果:弱い保証にも提供可能な性質があり、可用性との関係は保証ごとに異なると整理。
限界:強い保証だけを常に選ぶ一般論にはしない。本文の2013年著作表示と会議年を区別。
確認した範囲
著者公開PDFの冒頭・関連部分を確認。全文精読・追試・統計の再計算は未実施。 確認日:2026-10-29
取得できた書誌で撤回・懸念通知は確認されていません。出版社の履歴を網羅した監査ではありません。
対象・方法:4言語の12セルフホスト型ECアプリへ解析を適用。
主な結果:22件の重大な並行実行の問題を確認。
限界:選んだ12アプリに対する結果。全ECサイトや任意のDBの脆弱性率ではない。
確認した範囲
著者公開PDFの冒頭・関連部分を確認。全文精読・追試・統計の再計算は未実施。 確認日:2026-10-29
取得できた書誌で撤回・懸念通知は確認されていません。出版社の履歴を網羅した監査ではありません。
対象・方法:PostgreSQLの分離レベルと並行動作の仕様。
主な結果:Read Committedのコマンド単位の可視性と、競合時の挙動等を説明。
限界:OracleやSQLiteへ無変更で当てはめない。論文数に含めない。
確認した範囲
2026-10-29に公式の公開説明を確認。全製品での追試ではない。 確認日:2026-10-29
公式仕様・操作資料。学術文献の撤回索引とは別に扱います。
この記事は複数文献を照合した編集記事です。既存研究を照合し、結果と条件を紹介しています。研究が同じ参加者を含む可能性があるため、文献間の人数を足していません。本文の行動提案・未来のシナリオは編集上の整理です。


