在庫引当とは、注文や出庫要求に対して在庫の数量を割り当て、ほかの注文に売れない状態で押さえる処理です。棚に100個あっても、40個が別の注文に引き当て済みなら、新しい注文に約束できるのは60個しかありません。この記事では、受注引当と出荷引当の違い、有効在庫と引当可能数の計算、引当明細テーブルの状態遷移、キャンセルや決済期限切れでの引当解除、欠品時に入荷分をどの注文へ回すかの配分ロジックを、PostgreSQL 18系のSQLで示します。
まとめ:在庫引当の実装で先に決める引当の時点・解除条件・配分順
在庫引当の設計で決めることは3つに絞れます。1つ目は引き当てる時点で、注文確定で数量だけ押さえる受注引当と、出荷直前に棚とロットまで決める出荷引当のどちらを使うか、両方を2段階で使うかです。2つ目は引当を外す条件で、キャンセル・数量変更・決済待ちの期限切れのそれぞれで、どの数を戻すかを決めます。3つ目は欠品時の配分順で、入荷した分を先着順・納期順・顧客ランク順のどれで回すか、分納を許すかを注文ごとに持たせます。
実装面では、在庫残高表の引当量だけでなく、注文明細ごとの引当明細テーブルを状態つきで持つのが要点です。残高表の数字だけでは、どの注文の引当を外せばよいかを後から特定できません。納期回答をする業務、複数の販売チャネルで同じ在庫を売る業務では受注引当を入れ、即日出荷で受注から出荷までが数時間の業務では出荷引当だけで足ります。
在庫引当の定義と、実在庫・有効在庫・引当可能数の3つの数の違い
引当を実装する前に、画面に出す在庫の数を3種類に分けておきます。この区別が曖昧なまま作ると、同じ「在庫数」という欄が担当者によって別の意味で読まれます。
在庫引当の台帳上の操作と、棚の数と注文に約束できる数が分かれる理由
引当で減るのは在庫台帳の上の残高で、棚の現物は動きません。引き当てた時点では商品はまだ棚にあり、ピッキングして出荷した時点で初めて実在庫が減ります。受注から出荷まで2日かかる業務なら、その2日間は「棚にはあるが、もう売れない」在庫が存在します。
このずれがあるため、在庫の数は「棚にある数」と「新しい注文に約束できる数」の2系統に分かれます。JIS Z 8141(生産管理用語)での引当と有効在庫の定義、在庫管理システム全体の機能の中で引当がどこに位置するかは、在庫管理システムの入出庫管理と引当の仕組みを解説した記事で整理しています。
有効在庫と引当可能数の計算式と、受注時に入荷予定を足すかの判断基準
実装で使う式は次の3本です。実在庫は棚にある数、引当可能数は今すぐ約束できる数、有効在庫は発注判断に使う数で、入荷予定(発注残)を含めるかどうかが違います。
| 数 | 計算式 | 使う場面 |
|---|---|---|
| 実在庫 | 入庫累計-出庫累計 | 棚卸・出荷指示 |
| 引当可能数 | 実在庫-引当量 | 受注時の在庫確認 |
| 有効在庫 | 実在庫-引当量+発注残 | 発注点との比較 |
受注画面に有効在庫を出すと、まだ届いていない入荷予定まで約束してしまいます。即納の可否には引当可能数を使い、入荷予定分は「何日に何個まで回答できるか」という納期回答の欄に分けて出すのが安全です。発注点や安全在庫に有効在庫をどう使うかは、需要予測を在庫管理につなぐ発注点と安全在庫の設計が扱っています。
ShopifyのAPIに見る在庫状態の分け方とon_handの内訳
市販のECプラットフォームも同じ分け方をしています。Shopifyの開発者向け文書Manage inventory quantities and statesでは、拠点にある現物の総数on_handが、available・committed・reserved・damaged・safety_stock・quality_controlの合計と定義されています。入荷途中のincomingはこの合計に入りません。
注文で押さえた数にあたるcommittedは、注文の作成と出荷に応じてShopify側が動かし、GraphQL Admin APIからは調整も移動もできない、と同じ文書に書かれています。自社で作る場合もこの考え方を写すと事故が減ります。引当量を画面から直接書き換えられる設計にせず、注文・キャンセル・出荷のイベント経由でのみ増減させる設計です。
受注引当と出荷引当の違い、引当の時点で変わる業務と在庫の見え方
「在庫引当」と呼ばれる処理には、実務では性格の違う2つの段階が混ざっています。どちらを指しているかを先にそろえないと、要件定義の会話がかみ合いません。
受注引当:注文確定の時点で品目と数量だけを押さえる論理在庫の引当
受注引当は、注文を確定した瞬間に品目と数量だけを押さえる処理です。どの棚のどのロットを出すかはまだ決めません。在庫は「A-100が30個」という論理的な数として押さえられ、引当可能数が30減ります。
受注引当の役割は約束の確定です。ECサイトで「在庫あり」と表示して注文を受けたのに、出荷時に在庫が無いという事態を防ぎます。複数の注文が同時に同じ品目を取り合う場面では、空き数の確認と引当量の加算を1つのSQL文で行わないと、37個しかないのに60個を約束する事故が起きます。この取り合いをUPDATE文1本で防ぐ書き方は在庫管理システムの記事の「同時に入った注文で在庫がマイナスにならない引当」で実行例つきで解説しているため、ここでは繰り返しません。
出荷引当:出荷直前に棚番とロットまで決める実在庫の割当手順と判断基準
出荷引当は、出荷指示を出す直前に、どの倉庫のどの棚番から、どのロットを何個出すかまで決める処理です。WMS(倉庫管理システム)側の仕事になることが多く、ピッキングリストはこの結果から作られます。引当の前提になる棚番ごとの在庫の持ち方は、ロケーション管理の棚番マスタと棚別在庫のデータモデルをSQLで実装した記事で解説しています。
賞味期限やロットを扱う品目では、出荷引当の時点で期限の近いものから割り当てるFEFOなどのルールが効きます。受注時点でロットまで決めてしまうと、出荷日までに先に期限の来るロットが入荷した場合に逆転が起きるためです。ロット単位の引当ルールは販売管理システムのロット管理とFEFO引当の判断軸で詳しく扱っています。
2段階引当の流れと、OdooのReservation Methodにある3つの契機
規模のある卸や通販では、受注引当で数量を約束し、出荷引当で棚とロットを決める2段階にするのが標準的な形です。オープンソースERPのOdooも引当の契機を設定で選ばせており、Odoo 19.0の公式文書「Reservation methods」では、Inventory ‣ Configuration ‣ Operations TypesのReservation Methodで次の3つから選びます。
- At Confirmation:受注を確定した時点で、在庫がすでにある場合だけ引き当てる
- Manually:在庫の確認と引当を担当者が手作業で行う
- Before scheduled date:予定出荷日の何日前から引き当てるかを指定する(既定値は0日)
3つ目の方式は、受注から出荷まで数週間ある取り寄せ品や予約販売で効きます。注文確定の時点で押さえると、先の注文が在庫を長く抱え込み、今日出せる注文に回らなくなるからです。出荷予定日の数日前まで引当を遅らせる設計は、自社開発でも引当バッチの抽出条件に予定日を足すだけで実現できます。
引当明細テーブルのデータモデル:残高表と状態つき明細の二本立て
引当の数を残高表の1列だけで持つ設計は、作るのは簡単ですが運用で破綻します。注文がキャンセルされたとき、どの数を戻せばよいかを残高表は覚えていないからです。
在庫残高テーブルと引当明細テーブルの役割分担と実装に必要な最小限の列
残高表は品目ごとの実在庫と引当量の合計を持ち、受注時の在庫確認を速くするための集計です。引当明細は「どの注文明細に、どの品目を、何個、どの状態で」押さえたかを1行ずつ持つ記録で、解除と監査の根拠になります。PostgreSQLで書くと次のようになります。
CREATE TABLE stock_balance (
item_code text PRIMARY KEY,
on_hand integer NOT NULL DEFAULT 0, -- 実在庫
allocated integer NOT NULL DEFAULT 0, -- 引当量の合計
CHECK (allocated >= 0 AND allocated <= on_hand)
);
CREATE TABLE allocation (
allocation_id bigserial PRIMARY KEY,
order_line_id bigint NOT NULL, -- 受注明細
item_code text NOT NULL REFERENCES stock_balance,
location_code text, -- 受注引当ではNULL、出荷引当で確定
lot_no text,
qty integer NOT NULL CHECK (qty > 0),
status text NOT NULL
CHECK (status IN ('仮引当','本引当','出荷済','解除')),
expires_at timestamptz, -- 仮引当の期限
created_at timestamptz NOT NULL DEFAULT now(),
released_at timestamptz,
release_reason text
);
CREATE INDEX ON allocation (order_line_id) WHERE status IN ('仮引当','本引当');
location_codeとlot_noをNULL可にしているのが2段階引当のための工夫です。受注引当の行は品目と数量だけで作り、出荷引当で値を埋める対象はこの2列です。残高表のallocatedは、状態が仮引当と本引当の行のqtyの合計と常に一致させます。月次でこの2つを突き合わせるSQLを用意しておくと、ずれた時点で原因の注文を特定できます。
仮引当・本引当・出荷済・解除の4状態と業務イベント別の遷移条件
状態は4つで足ります。仮引当はカート投入や決済待ちのように期限つきで押さえた状態、本引当は注文確定後の状態、出荷済は現物が出て実在庫が減った状態、解除は引当を外した状態です。
| 遷移 | 契機 | 残高表の動き |
|---|---|---|
| なし→仮引当 | 決済開始 | 引当量+ |
| 仮引当→本引当 | 決済完了 | 変化なし |
| 本引当→出荷済 | 出荷確定 | 実在庫-・引当量- |
| 仮・本→解除 | 取消・期限切れ | 引当量- |
出荷済と解除は終端で、そこから戻る遷移は作りません。出荷後の返品は引当の取り消しではなく入庫として扱い、在庫の履歴に新しい行を足します。終端からの戻りを許すと、残高表と明細の合計が一致しなくなる経路が生まれます。
引当の登録と解除を同じトランザクションで残高表へ反映するSQL
登録は、残高表の加算と明細の挿入を1文にまとめます。PostgreSQLの書き込み可能なCTEを使うと、空き数が足りたときだけ明細が入り、足りないときは0行が返ります。
-- 受注明細5001にA-100を30個引き当てる(足りなければ0行が返る)
WITH upd AS (
UPDATE stock_balance
SET allocated = allocated + 30
WHERE item_code = 'A-100'
AND on_hand - allocated >= 30
RETURNING item_code
)
INSERT INTO allocation (order_line_id, item_code, qty, status)
SELECT 5001, item_code, 30, '本引当' FROM upd
RETURNING allocation_id;
-- 受注明細5001の引当を解除して残高表へ戻す
WITH rel AS (
UPDATE allocation
SET status = '解除', released_at = now(), release_reason = '受注取消'
WHERE order_line_id = 5001
AND status IN ('仮引当','本引当')
RETURNING item_code, qty
)
UPDATE stock_balance s
SET allocated = s.allocated - r.qty
FROM (SELECT item_code, sum(qty) AS qty FROM rel GROUP BY item_code) r
WHERE s.item_code = r.item_code;
解除側のWHERE status IN ('仮引当','本引当')が二重解除の防止になっています。キャンセル操作が2回届いても、2回目は対象行が0件なので残高表は動きません。PostgreSQLの既定の分離レベルはRead Committedで、公式文書の13.2 Transaction Isolationによれば、同時に更新された行に対するUPDATEは相手の確定を待ってから、更新後の行でWHERE句を評価し直します。この再評価があるため、上の2本は同時実行されても数が狂いません。
引当解除の実装:取消・数量変更・決済期限切れで在庫を戻す処理
引当の不具合は、登録よりも解除で起きることが多くあります。押さえっぱなしの引当は在庫を眠らせ、実際には売れる商品が「在庫なし」と表示される原因になります。
受注の取消と数量の減少で、注文明細単位に在庫へ戻す数量を決める手順
全量の取消なら、前節の解除SQLで注文明細の引当をすべて外せば済みます。数量変更では、在庫へ戻す差分を計算しなければなりません。30個の注文が20個に減った場合、30個の引当行を解除して20個で引き当て直すのではなく、差分の10個だけを戻すほうが安全です。解除と再引当の間にほかの注文が割り込むと、20個が確保できなくなる恐れがあるためです。
差分を戻すには、引当行のqtyを減らし、同じトランザクションで残高表のallocatedも10減らします。ロットや棚番まで決まった出荷引当の行が複数ある場合は、出荷予定日の遅い行から減らすと、ピッキング中の作業を止めずに済みます。
決済待ちの仮引当に期限を持たせ、自動で解除するバッチの実装手順
ECの決済待ちやカートの取り置きは、仮引当にexpires_atを入れ、期限を過ぎた行をバッチで解除します。バッチを複数台で並列に動かす場合は、同じ行を2台が同時に処理しないようFOR UPDATE SKIP LOCKEDを使います。
-- 期限切れの仮引当を最大500行ずつ解除する(複数ワーカーで並列実行可)
WITH expired AS (
SELECT allocation_id
FROM allocation
WHERE status = '仮引当' AND expires_at < now()
ORDER BY allocation_id
LIMIT 500
FOR UPDATE SKIP LOCKED
), rel AS (
UPDATE allocation a
SET status = '解除', released_at = now(), release_reason = '期限切れ'
FROM expired e
WHERE a.allocation_id = e.allocation_id
RETURNING a.item_code, a.qty
)
UPDATE stock_balance s
SET allocated = s.allocated - r.qty
FROM (SELECT item_code, sum(qty) AS qty FROM rel GROUP BY item_code) r
WHERE s.item_code = r.item_code;
PostgreSQLのSELECT文の公式文書(The Locking Clause)は、SKIP LOCKEDはロック中の行を読み飛ばすため一貫しない見え方になり汎用の処理には向かないが、キューのようなテーブルを複数の処理が取り合う場面では使える、と説明しています。期限切れの解除はまさにキュー型の処理なので、この用途に合います。
決済完了の処理は、WHERE status = '仮引当'を条件に本引当へ更新します。バッチが先に解除していれば0行更新になるので、そのときに必要なのは、引き当て直しを試み、在庫が無ければ決済を取り消す分岐です。この分岐を作らずに本番へ出すと、決済済みなのに引当が無い注文が生まれます。
欠品時の優先順位:入荷した在庫をどの注文へ先に回すかの配分ロジック
引当可能数が足りずに引当できなかった注文は、受注残(バックオーダー)として入荷を待ちます。入荷した50個を、待っている4件の注文のどれに回すかを決めるのが再引当の配分ロジックです。
欠品時の先着順・納期順・顧客ランク順の比較と、標準にすべき配分順
| 並び順 | 向く業務 | 弱点 |
|---|---|---|
| 受注日時の先着順 | EC・一般消費者向け | 急ぎの注文が後回し |
| 納期の早い順 | 卸・BtoBの納期回答 | 納期の甘い入力に弱い |
| 顧客ランク順 | 大口取引先の優先契約 | 小口の注文が滞留 |
BtoBの販売管理では、納期の早い順を第1キー、受注日時を第2キーにするのが標準的な形です。顧客ランクは並び順に混ぜず、大口取引先向けに数量を先に確保する「枠」として別に持つと、ランクの低い注文が無期限に後回しになる事態を避けられます。一般消費者向けのECは先着順一択です。注文した順に届かないと問い合わせが増え、説明のしようがありません。
欠品時に分納を許す注文と、全量そろうまで待つ注文を分ける引当判定
受注残には「入った分だけ先に送ってよい」注文と「全量そろってから出してほしい」注文が混ざります。工事の資材や店舗の新装用の什器は、半分だけ届いても使えないため後者になりがちです。この区別を受注明細の列(例:partial_ok)で持たないと、配分ロジックが全量待ちの注文に半端な数を割り当て、その数がほかの注文に回らずに眠ります。
判定の規則は単純です。分納可の注文には入荷数の範囲で割り当て、全量待ちの注文は残りが注文数に届かなければ飛ばして、その分を次の注文へ回します。
入荷時の再引当を、ウィンドウ関数の累積和で一括配分するSQL
分納可の注文だけなら、並び順に累積した注文数と入荷数を比べるだけで、全件の配分量を1本のSQLで出せます。次は入荷50個を納期順・受注日時順に配分する例です。
WITH ranked AS (
SELECT order_line_id, qty_short,
sum(qty_short) OVER (ORDER BY due_date, ordered_at, order_line_id)
- qty_short AS before_qty -- 自分より前の注文の合計
FROM backorder
WHERE item_code = 'A-100'
)
SELECT order_line_id, qty_short,
greatest(least(qty_short, 50 - before_qty), 0) AS alloc_qty
FROM ranked
ORDER BY before_qty;
受注残4件(5102:30個・10月22日納期、5103:15個・10月22日納期、5101:20個・10月25日納期、5104:10個・10月28日納期)で計算すると、配分は5102に30個、5103に15個、5101に5個、5104に0個です。この結果は、同じSQLをSQLite 3.50.4でgreatestとleastをmaxとminに置き換えて検算しました。5104は4件の中で最も早く受注していますが、納期が最も遅いため納期順では最後に回ります。先着順なら5104が最初に10個を取るので、並び順の違いがそのまま出荷先の違いになります。
累積和の方式は全量待ちの注文を扱えません。入荷が25個だと、全量待ちの5102に25個が割り当てられ、30個に届かないまま25個が眠ります。全量待ちの注文が混ざる品目は、アプリ側で注文を並び順に1件ずつ見て、足りない全量待ちを飛ばす処理にします。同じ25個なら、5102を飛ばして5103に15個、5101に10個となります(同じデータで検算済み)。
複数明細の注文で起きるデッドロックと、品目コード順にロックを取る対策
1件の注文に複数の品目が入っていると、引当のたびに残高表の複数行をロックします。注文XがA-100→B-200の順に、注文YがB-200→A-100の順に引き当てると、互いに相手の確定を待って止まります。
PostgreSQLの公式文書13.3.4 Deadlocksは、このような循環待ちを自動で検出していずれかのトランザクションを中断すると説明したうえで、どちらが中断されるかは予測できず頼るべきではないと書いています。そして最善の防御は、データベースを使うすべての処理が複数の対象に一貫した順序でロックを取ることだとしています。
引当に当てはめると、注文明細を画面の並び順ではなく品目コードの昇順に並べ替えてから、1品目ずつ引き当てます。先にSELECT … FROM stock_balance WHERE item_code = ANY(…) ORDER BY item_code FOR UPDATEで対象行をまとめてロックしてから更新する書き方でも、得られる効果は同じです。それでもデッドロックで中断された場合に備え、アプリ側で引当のトランザクションを丸ごと再実行する処理を入れておきます。原因と解消の一般論はデッドロックの原因と解消方法の記事、行ロックの取り方の違いは楽観ロックと悲観ロックの使い分けの記事が詳しく扱っています。
受注引当を入れるべき業務と、出荷引当だけで足りる業務の判断基準
2段階の引当は作り込みが増えるぶん、要らない業務に入れると保守の負担だけが残ります。ここでは、どちらを選ぶかを条件で言い切ります。
受注引当を入れるべき業務の判断基準:納期回答・複数チャネル・取り置き
次のどれか1つに当てはまるなら、受注引当を入れます。取引先から納期を聞かれて回答する業務、ECモールと自社サイトと店舗で同じ在庫を売る業務、受注から出荷まで1日を超える取り寄せや予約販売の業務です。いずれも、注文時点で約束した数を出荷まで守る仕組みが無いと、二重販売か回答のやり直しが起きます。
この条件に当てはまり、パッケージの引当機能では配分順や分納の扱いが業務に合わない場合は、個別開発やカスタマイズの検討に入る段階です。受注・引当・出荷・請求を1本の流れで作る販売管理システム開発では、引当の時点と解除条件を要件定義で先に決めてから設計に入ります。
出荷引当だけで足りる業務条件と、受注引当の解除漏れで失敗する形
受注から出荷までが当日中で、販売チャネルが1つしかない業務では、受注引当は不要です。出荷指示の時点で棚の在庫を割り当てれば足り、注文時点の在庫確認は引当可能数の表示だけで済みます。従業員数十人規模の卸で、電話とFAXの受注を当日出荷している会社がこの典型です。
失敗するのは、受注引当を入れたのに解除の運用を決めていない形です。見積段階で仮に押さえた引当や、失注した案件の引当が残り続け、倉庫に現物があるのに受注画面では「在庫なし」になります。受注引当を入れるなら、仮引当に期限を持たせる設計と、期限切れを自動で外すバッチを同時に作ります。片方だけでは入れないほうがましです。
よくある質問
在庫引当の実装や運用について、検索の多い質問に答えます。
在庫引当と引当金は同じ意味ですか?
別の概念です。在庫引当は、注文に対して在庫の数量を割り当てて押さえる在庫管理上の処理で、金額は動きません。引当金は、将来の支出や損失に備えて決算で費用を見積もり計上する会計上の処理で、賞与引当金や退職給付引当金などがあります。販売管理システムの画面で「引当」とあれば在庫引当、会計システムの画面なら引当金の計上を指すのが通例です。
受注引当と出荷引当はどちらを先に行いますか?
2段階で使う場合は受注引当が先です。注文確定の時点で品目と数量だけを押さえ、出荷指示の直前に出荷引当で棚番とロットを決めます。受注引当で押さえた数量は、出荷引当のときにその範囲内で具体的な現物に割り当て直されるので、引当量の合計は増えません。出荷引当だけを使う業務では受注時に何も押さえず、出荷指示の時点で初めて引き当てます。
有効在庫と引当可能数は何が違いますか?
入荷予定を含めるかどうかが違います。引当可能数は実在庫から引当量を引いた数で、今すぐ約束できる数です。有効在庫はそこに発注残(入荷予定)を足した数で、発注点と比べて発注するかを決めるときに使います。受注画面で即納できるかを判断する欄には引当可能数を出し、有効在庫は発注担当者の画面に出すと、届いていない入荷予定を約束してしまう事故を防げます。
Excelで在庫引当を管理することはできますか?
受注が1日に数件で、入力する担当者が1人なら管理できます。品目ごとに実在庫と引当量の列を持ち、注文ごとに引当量を足し、出荷とキャンセルで引けば計算は成り立ちます。破綻するのは複数人が同じファイルを同時に更新する場合で、同じ在庫を2人が別の注文に約束する取り合いをExcelは防げません。どの注文の引当かを行単位で残さないと、キャンセル時にどの数を戻すかも追えなくなります。
引当したのに在庫が合わなくなる原因は何ですか?
多いのは3つです。1つ目は引当の解除漏れで、キャンセルや失注の注文の引当が残り続けるパターンです。2つ目は残高表だけを更新して引当明細を持たない設計で、合計がずれたときに原因の注文を特定できません。3つ目は空き数の確認と引当の加算を別々のSQLで行う実装で、同時に入った注文が同じ在庫を取り合います。月に1回、引当明細の合計と残高表の引当量を突き合わせる確認を入れると、ずれが小さいうちに見つかります。
関連記事
- 販売管理システムとは?機能・Excel管理との違いと中小企業向けの選び方:引当を含む受注から請求までの流れと、システムを選ぶ判断の全体像
- 在庫管理システムとは?入出庫管理と引当の仕組み・機能・種類・費用と選び方:入出庫履歴の台帳設計と、同時注文で在庫がマイナスにならない引当のUPDATE例
- WMSとは?倉庫管理システムの機能・在庫管理との違い・導入判断を解説:出荷引当で棚番とロットを決めるWMS側の機能
- トランザクションとは?分離レベルの違い・ACID・commit/rollbackを実装目線で解説【2026年版】:引当のSQLを同じトランザクションにまとめる前提知識
- EC在庫管理システムとは?多店舗の在庫連携・受注同期の機能と費用・選び方を解説:複数チャネルで同じ在庫を売るときの引当と在庫同期