開発

在庫管理システムを自作する:Excel・kintone・SQLの境界と同時出庫の排他制御

在庫管理システムを自作する:Excel・kintone・SQLの境界と同時出庫の排他制御

在庫管理システムを自作するとき、画面や帳票より先に詰まるのは「2人が同じ商品を同時に出庫したとき、在庫数がずれないか」です。この記事では、Excel・kintone・SQLデータベースの3つの手段を、同時更新をどう止められるかで切り分けます。そのうえで、PostgreSQL 18系で在庫残高表と入出庫履歴表を作り、条件付きのUPDATEで同時出庫を1件だけ通すSQL、2つのセッションで確かめる手順、kintoneで同じことをするrevision付きの更新リクエストまでをコードで示します。最後に示すのは、自作を続ける条件と受託開発へ切り替える判断の目安です。

まとめ:在庫管理の自作は、在庫数を条件付きで減らす1行から作る

自作で崩れる在庫は、ほぼ「読んでから書くまでの間に、別の人が書いた」ことで起きます。画面で在庫5を確認し、3を引いて2を書き戻す作りは、同時に2人が操作すると両方が2を書き、6個出したのに在庫は2になります。防ぎ方は手段ごとに決まっていて、SQLなら「在庫数が出庫数以上のときだけ減らす」UPDATE、kintoneならレコードのrevision照合です。Excelには、この照合を書く場所がありません。

結論として、担当者1人が入力する規模ならExcelで足ります。複数人が入出庫を入れるならkintoneのrevision更新かSQLの条件付きUPDATEが必須で、EC・POS・倉庫など複数の経路から在庫が動くなら、SQLで残高表と履歴表を分けて作ります。受注に在庫を押さえる引当まで要る場合は、その設計を在庫引当の実装(受注引当・出荷引当・欠品時の配分をSQLで書く方法)に分けて読み進めてください。

自作の手段を選ぶ前に、Excel・kintone・SQLの同時更新の扱いを比べる

同じ「在庫表」でも、手段によって同時更新の止め方がまったく違います。機能一覧ではなく、この1点で比べると選択を誤りません。

3つの手段で、同時出庫を止める仕組みの有無と上限を表で比べる

手段 同時更新を止める仕組み 入出庫の履歴 向く規模
Excel(共同編集) 無し(セル単位で後から書いた値が残る) 手で行を足す運用 入力者1人・品目数百まで
kintone revision照合(不一致なら更新しない) 出庫アプリにレコードで残る 入力者数人〜数十人
SQLデータベース 行ロックと条件付きUPDATE・CHECK制約 履歴表に自動で残せる 複数経路・外部連携あり

表の「向く規模」は目安で、決め手は2列目です。入力者が2人以上になった時点で、Excelは同時更新を止める手段を持たないため候補から外れます。市販のアプリを買う選択肢との比較は、在庫管理アプリの選び方(無料・有料・エクセルとの違い)で扱っています。

Excelの共同編集で在庫数が合わなくなる、読んでから書くまでの空白

Microsoftは、旧来の「共有ブック」を共同編集に置き換えた機能と位置づけ、共有ブック機能についての公式の説明では、表の作成やデータの入力規則、シートの保護などが共有ブックでは使えないと案内しています。現在の共同編集なら複数人が同じブックを開けますが、どちらも「在庫数が出庫数以上のときだけ書き込みを受け付ける」という条件を持てません。

典型的な崩れ方は次のとおりです。AさんとBさんが同じ商品の在庫5を見て、それぞれ3を出庫し、セルに2を入力します。結果は2。実際の棚は-1です。マクロで減算しても、2人が別々のブックの写しで動かす限り同じことが起きます。Excelで続けるなら、入力者を1人に絞るのが唯一の対策になります。

kintoneのrevisionと、ドメイン単位で100件の同時実行上限

kintoneのレコードには更新のたびに増えるrevision(リビジョン番号)があります。レコード更新APIの仕様では、期待するrevisionを渡すと、実際の番号と一致しない場合はエラーとなり更新されません。-1を渡すか省略すると照合しないため、在庫の更新で省略するとExcelと同じ崩れ方をします。

注意したいのは上限です。サイボウズのkintoneの性能に関する解説(2025年6月23日更新)によると、REST APIの同時実行は1ドメインにつき100件までで、超えるとそのアプリだけでなくドメイン全体のリクエストがエラーになります。ECの受注同期のように在庫を自動で何度も更新する仕組みを足すと、手入力の画面まで止まる。この上限が、kintoneでの自作を続けるかの分かれ目になります。外部システムとの連携方式はkintoneのデータ連携の方式とAPI制限の設計で比べています。

検証環境の準備と、在庫残高表と入出庫履歴表を分けるテーブル設計

ここからはSQLで作る場合の実装です。PostgreSQL 18系(2026年10月時点の最新は18.6)をDockerで起動し、テーブルを作ります。

DockerでPostgreSQL 18を起動し、psqlで接続する検証用のコマンド

docker run -d --name zaiko-db \
  -e POSTGRES_USER=app -e POSTGRES_PASSWORD=app -e POSTGRES_DB=zaiko \
  -p 5432:5432 postgres:18

docker exec -it zaiko-db psql -U app -d zaiko -c "SELECT version();"

2行目で「PostgreSQL 18.x」と表示されれば準備は完了です。以降のSQLはschema.sqlなどのファイルに保存し、docker exec -i zaiko-db psql -U app -d zaiko < schema.sqlで流し込みます。

品目・保管場所・在庫残高・入出庫履歴の4表を作るDDLとCHECK制約

CREATE TABLE item (
  id    bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  code  text NOT NULL UNIQUE,
  name  text NOT NULL
);

CREATE TABLE location (
  id    bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  code  text NOT NULL UNIQUE
);

-- 今いくつあるか(1品目×1保管場所で1行)
CREATE TABLE stock_balance (
  item_id     bigint NOT NULL REFERENCES item (id),
  location_id bigint NOT NULL REFERENCES location (id),
  on_hand     integer NOT NULL DEFAULT 0 CHECK (on_hand >= 0),
  updated_at  timestamptz NOT NULL DEFAULT now(),
  PRIMARY KEY (item_id, location_id)
);

-- なぜ増減したか(追記だけで、更新も削除もしない)
CREATE TABLE stock_movement (
  id          bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  request_id  text NOT NULL UNIQUE,
  item_id     bigint NOT NULL,
  location_id bigint NOT NULL,
  qty_delta   integer NOT NULL CHECK (qty_delta <> 0),
  reason      text NOT NULL CHECK (reason IN ('receive', 'ship', 'adjust', 'move')),
  created_at  timestamptz NOT NULL DEFAULT now(),
  FOREIGN KEY (item_id, location_id) REFERENCES stock_balance (item_id, location_id)
);

INSERT INTO item (code, name) VALUES ('A-001', '検証用の商品');
INSERT INTO location (code) VALUES ('MAIN');
INSERT INTO stock_balance (item_id, location_id, on_hand) VALUES (1, 1, 5);

在庫数の列に付けたCHECK (on_hand >= 0)は最後の防波堤です。PostgreSQLの制約の説明どおり、条件に反する値を書こうとした文はエラーで止まるので、アプリ側の判定に漏れがあってもマイナス在庫は保存されません。保管場所を棚番まで細かく持つ場合のコード設計はロケーション管理の実装を参照してください。

残高表だけで作ると棚卸の差異の原因を追えなくなる、履歴表を足す理由

在庫数だけを上書きする作りは、最初は楽です。ところが棚卸で帳簿5・実数3となったとき、いつ・どの出庫で2つずれたかを追えません。そこで残高表(今の数)と履歴表(増減の記録)を分け、同じトランザクションで両方を書きます。残高表は履歴表の合計と常に一致するので、ずれを疑ったらSUM(qty_delta)で検算できます。

履歴表のrequest_idに付けた一意制約は、二重登録を防ぐためのものです。通信が切れて画面が出庫ボタンを再送しても、同じrequest_idは2回入らないので、在庫も2回は減りません。

同時出庫で在庫数を崩さない、条件付きUPDATEと冪等キーのSQL

自作の在庫管理で一番効くのは、この章の1文です。在庫を読んでアプリで計算する手順をやめ、データベースに「減らせるなら減らす」を1回で頼みます。

在庫数が足りるときだけ減らし、同じ文で出庫履歴を書く1文のSQL

WITH upd AS (
  UPDATE stock_balance
     SET on_hand = on_hand - 3,
         updated_at = now()
   WHERE item_id = 1
     AND location_id = 1
     AND on_hand >= 3
  RETURNING item_id, location_id
)
INSERT INTO stock_movement (request_id, item_id, location_id, qty_delta, reason)
SELECT 'ship-20261027-0001', item_id, location_id, -3, 'ship'
  FROM upd
RETURNING id;

戻りが1行なら出庫成功、0行なら在庫不足です。アプリは0行のときに「在庫が足りません」を返せばよく、SELECTで在庫を先に読む必要はありません。同じrequest_idで再実行すると一意制約の違反になり、UPDATEも含めて文全体が取り消されるため、在庫は減りません。

Read CommittedのUPDATE待機とWHERE条件の再評価の仕組み

この1文で同時出庫が防げるのは、PostgreSQLの既定の分離レベルによるものです。トランザクション分離の公式ドキュメントでは、Read Committedで別のトランザクションが更新中の行に当たったUPDATEは、相手のコミットかロールバックを待ちます。相手がコミットした場合は、更新後の行に対してWHERE条件を評価し直し、条件に合うときだけ更新する仕組みです。

つまり在庫5に対して3個の出庫が同時に2件来ると、先の1件が在庫を2にした後で、後の1件は「2 >= 3」を評価し直して0行で終わります。行ロックの公式の説明にあるとおり、行ロックは同じ行に書く処理だけを止め、在庫の照会は止めません。SELECT … FOR UPDATEで先にロックする方法との違いは楽観ロックと悲観ロックの使い分けで比べています。

2つのpsqlセッションから同時に出庫し、1件だけ通ることを確かめる手順

docker exec zaiko-db psql -U app -d zaiko \
  -c "UPDATE stock_balance SET on_hand = 5 WHERE item_id = 1 AND location_id = 1;"

for n in 1 2; do
  docker exec zaiko-db psql -U app -d zaiko \
    -c "BEGIN;" \
    -c "WITH upd AS (UPDATE stock_balance SET on_hand = on_hand - 3 WHERE item_id = 1 AND location_id = 1 AND on_hand >= 3 RETURNING item_id, location_id) INSERT INTO stock_movement (request_id, item_id, location_id, qty_delta, reason) SELECT 'test-$n-' || now()::text, item_id, location_id, -3, 'ship' FROM upd;" \
    -c "SELECT pg_sleep(2);" \
    -c "COMMIT;" &
done
wait

docker exec zaiko-db psql -U app -d zaiko \
  -c "SELECT on_hand FROM stock_balance WHERE item_id = 1 AND location_id = 1;"

先に行を更新したセッションは、2秒の待機中も行ロックを持ち続けます。後のセッションのUPDATEはその間待たされ、コミット後に条件を評価し直すため、出力は片方がINSERT 0 1、もう片方がINSERT 0 0になり、最後の在庫数は2になるはずです。WHERE句からAND on_hand >= 3を消して同じ手順を流すと、CHECK制約の違反で後のセッションが止まります。制約が無ければ在庫は-1で保存されていたことになり、2つの防御の役割の違いが確かめられます。

複数品目の出庫で起きるデッドロックを、品目コード順の更新で避ける

1回の出庫で複数の品目を減らすと、セッション1が品目A→B、セッション2が品目B→Aの順で更新したときにお互いを待ち、デッドロックになります。PostgreSQLは検知して片方を取り消すため、利用者から見ると出庫処理の失敗です。品目IDの昇順など、全員が同じ順番で更新するだけで避けられます。原因と検知の仕組みはデッドロックの原因と解消方法で詳しく扱っています。

kintoneで自作する場合の在庫更新、revisionを渡すリクエストと再試行

kintoneで作るなら、在庫アプリ(品目ごとの在庫数)と出庫アプリ(出庫の記録)を分けます。SQLの残高表と履歴表と同じ分け方です。

在庫レコードのrevisionを照合し、出庫登録と在庫減算を1回で送る例

curl -X POST "https://example.cybozu.com/k/v1/bulkRequest.json" \
  -H "X-Cybozu-API-Token: ${KINTONE_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "requests": [
      {
        "method": "PUT",
        "api": "/k/v1/record.json",
        "payload": {
          "app": 10,
          "id": 1,
          "revision": 7,
          "record": { "在庫数": { "value": "2" } }
        }
      },
      {
        "method": "POST",
        "api": "/k/v1/record.json",
        "payload": {
          "app": 11,
          "record": {
            "商品コード": { "value": "A-001" },
            "出庫数": { "value": "3" }
          }
        }
      }
    ]
  }'

在庫アプリ(app 10)のレコードを取得した時点のrevisionが7で、在庫数5から3を引いた2を書く例です。複数のAPIを一度に実行するAPIの仕様では、1回に最大20件まで指定でき、いずれかが失敗すると以降は実行されず、すべての処理がロールバックされます。revisionが合わずに在庫の更新が失敗すれば、出庫レコードも残りません。

revisionの不一致エラーを受けたら、取り直して計算し直す再試行の順序

エラーが返ったときは、在庫レコードを取り直し、最新の在庫数とrevisionで計算し直して送ります。在庫数が出庫数を下回っていれば、再送せず在庫不足として返します。再試行は2〜3回で打ち切り、それでも通らなければ利用者にやり直しを促すのが現実的です。サイボウズのkintoneで安全に在庫管理を行うテクニック(2024年9月12日更新)も、取得時のrevisionを更新時に渡し、在庫数がマイナスになる出庫をエラーにする同じ組み立てでサンプルを示しています。

SQLの条件付きUPDATEと違い、kintoneは「読んで計算して書く」手順そのものは残ります。同じ品目への出庫が集中するほど不一致と再試行が増えるため、出荷の多い繁忙日に入力が詰まり始めたら、次の章の切り替え目安に当たります。

自作を続けるか受託開発に切り替えるかを、在庫が動く経路の数で決める基準

自作で作れるかどうかより、作った後に在庫が崩れたとき直せるかで判断します。目安は、在庫が動く経路の数です。

自作で進めてよい条件と、作り始める前に止めるべき在庫管理の業務条件

自作で進めてよいのは、在庫を動かすのが社内の入力画面だけで、1日の出庫が数百件までの場合です。この条件なら、kintoneのrevision更新か、この記事のSQL程度の規模で足ります。

反対に、次のどれかに当たるなら作り始める前に止めます。

  • ECモール・自社EC・実店舗のPOSなど、2つ以上の経路が同じ在庫を減らす
  • 受注の時点で在庫を押さえ、入荷待ちの注文に入荷分を配る(引当)
  • ロット・使用期限・シリアル番号を出庫単位で追う必要がある
  • 会計や販売管理へ、月末に在庫金額を渡す

どれも「1行のUPDATE」では済まず、同期の遅れ・取消・返品の戻しを経路ごとに設計することになります。在庫システムの種類と機能の全体像は在庫管理システムとは(入出庫管理と引当の仕組み)、市販製品のタイプ別の比較は在庫管理システム比較にまとめています。

EC連携や引当が加わった段階で、受託開発へ切り替える目安と準備

自作の在庫表にEC連携を後から足すと、受注取込の重複や取消の戻し忘れで在庫が少しずつずれ、棚卸のたびに手で合わせる運用に落ちがちです。経路が2つを超えたら、残高表・履歴表・引当表を最初から分けた設計に作り直すほうが、手直しを続けるより早く安定します。

一創の在庫管理システム開発では、既存のExcelやkintoneの在庫データを移しながら、複数経路の在庫同期と引当を含めて作り直すご相談を受けています。相談の前に、在庫が動く経路の一覧と、1日の出庫件数、棚卸で差異が出た品目の例を用意しておくと、どこまで自作の資産を残せるかを具体的に判断できます。

よくある質問

在庫管理システムの自作について、手段の選び方と実装でよく出る疑問に答えます。

在庫管理システムはExcelのマクロ(VBA)だけで自作できますか?

入力者が1人なら作れます。品目マスタ・入出庫の記録シート・在庫数の集計シートを分け、在庫数は記録シートの合計から計算する形にすれば、上書きでずれる事故は減ります。ただし、複数人が同時に入力するとマクロでも在庫数の照合はできません。2人目の入力者が加わる時点で、kintoneかデータベースへ移る準備を始めてください。

MySQLでも同じ条件付きUPDATEで同時出庫を防げますか?

防げます。MySQLのInnoDBでも、UPDATEは対象の行にロックを取り、待たされた側はロック解放後の最新の値でWHERE条件を判定するため、「在庫数が出庫数以上のときだけ減らす」書き方は同じように働きます。影響を受けた行数が0なら、在庫不足として扱うのがこの処理の判定方法です。MySQL 8.0のリファレンスマニュアルでは、8.0.16より前の版はCHECK句を解析するだけで無視するとされているため、古い版では在庫数の列を符号なし整数にするなどの代わりの防御を置きます。

在庫管理システムの自作にノーコードツールや生成AIは使えますか?

画面やCRUDの土台づくりには使えます。生成AIに頼む場合も、在庫数を「読んで計算して書く」コードが出てくることが多いので、在庫の更新部分はこの記事の条件付きUPDATEやrevision照合の形になっているかを必ず確かめてください。崩れるのは画面ではなく、同時に動いたときの在庫数です。2セッションの確認手順は、生成されたコードの検証にもそのまま使えます。

バーコードやQRコードの読み取りも自作できますか?

自作での読み取りは可能です。多くのハンディスキャナはキーボードとして文字を入力するので、入出庫画面の入力欄にフォーカスを置けば、商品コードがそのまま入力される仕組みです。スマートフォンのカメラで読む方法もありますが、読み取り速度と照明の影響を受けるため、出庫件数が多い現場では専用スキャナのほうが速く安定します。読み取った後の在庫の更新は、この記事と同じ条件付きの書き方にします。

棚卸で帳簿と実在庫が合わないとき、自作のシステムでどう直しますか?

在庫数を直接書き換えず、差分を「棚卸調整」として履歴表に1行追加し、同じトランザクションで残高を合わせます。この記事のDDLならreasonをadjustにした行です。こうすると、いつ・いくつ・なぜ合わせたかが残り、差異の多い品目や保管場所を後から集計できます。上書きで直すと、次に同じずれが起きたときに原因をたどる手がかりが消えます。

関連記事

お気に入りに入れた記事の一覧

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.23 テックブログ Renta!はいつ再開?不正アクセスでメールアドレス等が流出したおそれ・再開時のパスワード再設定と運営側の対応
  2. 2026.07.09 テックブログ Muse Imageとは?Metaの画像生成AIの使い方・API料金(1枚0.01ドル)と日本での提供状況【2026年10月】
  3. 2026.10.23 テックブログ Splunk 脆弱性CVE-2026-76299:影響版の確認と10.2.8などへの更新手順
  4. 2026.10.22 テックブログ Tomcatのバージョン一覧とサポート期限【2026年10月】最新版11.0.27、9.0系は2027年3月末終了・9.1系は2030年末まで
  5. 2026.10.21 テックブログ ユミルリンク不正アクセス第二報で延べ5,162万件:Cuenote FC利用企業と受信者の確認点

RELATED POSTS 関連記事

目次