Article

在庫管理システムを Excel から移行する手順

高田晃太郎
在庫管理システムを Excel から移行する手順

研究報告では、業務用スプレッドシートの約94%に重大な誤りが含まれるとされています¹。供給網関連の調査でも、店舗や倉庫の在庫精度が60〜70%台に留まるケースが示され²、数式の破綻や手動入力・転記の遅延が主因になりやすいことが指摘されています³。公開事例や現場の声を俯瞰すると、Excel集計ベースの在庫は棚卸後に反映されるまで1〜3日程度のラグが生じがちで、結果として欠品や過剰発注の温床になりやすい状況が見受けられます。Excelは“ドキュメント”としては優秀ですが、在庫という“状態”を分散同時更新で正しく扱うには、ACIDなトランザクション(原子性・一貫性・分離性・耐久性)、監査可能なイベント履歴(変更の来歴を追える記録)、権限と入力検証、そして低遅延な入出庫反映が欠かせません。ここでは、Excelから在庫管理システムへ移行する実装手順を、データモデル、ETL(抽出・変換・ロード)、API、検証、カットオーバー、運用指標まで一気通貫で示します。加えて、Excel依存から統合在庫システムへ移行することはデータ整合性や監査性の面で有効だとする報告もあります⁵。キーワードとしては、在庫管理システム移行、Excel在庫管理の課題、イベントソーシング、PostgreSQL、FastAPI、Kafka、WMS/ERP連携、リアルタイム在庫可視化などの観点が重要です。

ROIから逆算する設計とロードマップ

移行の起点は手段ではなく目的です。Excelからの移行はシステム刷新自体が目的ではなく、在庫差異の縮小、在庫回転の改善、欠品と滞留の同時低減、棚卸と監査の生産性向上というビジネス成果を狙うべきです。経験的知見として、仕入から出荷までの取引を在庫イベントとして正規化し、在庫残高を導出値(イベントの集計結果)に切り替えると、在庫回転率の改善を見込みやすくなります。欠品率は安全在庫の動的計算とリードタイム分布の取り込みで低下しやすく、これらは在庫政策として広く研究されています⁶。サイクルカウント(営業を止めずに行う定期棚卸)は年次棚卸に比べて運用への負荷が小さく、精度向上にも有効とされます⁴。さらに、Excelから統合システムへ移行することでデータの一貫性や意思決定の迅速化が期待できるという調査もあります⁵。これらは業態やSKU特性に依存しますが、共通する要諦は在庫を“残高テーブル”で直接更新しないことです。すべての増減を**不変のイベント(入庫・出庫・調整)**として記録し、残高はビューや集計で計算、あるいはイベントソーシング(履歴から現在値を投影する設計)で投影します。これにより、差異の原因追跡、権限に応じた可視化、遡及修正の安全性が飛躍的に高まります。ロードマップはデータモデル確定とスモールスコープの二重運用から始め、イベント履歴が十分に溜まったところで残高の権威ソースを切り替え、最終的にExcelを参照専用へと移行する流れが理にかないます。

最小限で始めて確実に拡張する

失敗は往々にして要件過多から始まります。多拠点・多単位・ロット/シリアル・有効期限・返品・キッティングなど全要素を初期から取り込むのではなく、SKU(在庫管理単位)とロケーション(保管場所)、取引イベントの三点を中核に据え、出荷・入荷・棚卸調整の三分類から着手します。これらを賄うデータモデルを先に固定し、追加の派生要件はイベント属性の拡張で吸収するのが安全です。Excelを残したまま短期の二重運用を実施し、差異の原因を分類してモデルか運用のどちらで解決すべきかを切り分けることで、カットオーバー時の意外な欠品や異常在庫の発生を抑えられます。

ベンチマークとSLOを先に決める

在庫はリアルタイム性の要求が高い領域です。受入から可用化までの遅延、ピッキング反映までの整合、移動の多重更新などが起こり得ます。SLO(サービスレベル目標)としての一例として、入庫イベントの95パーセンタイル反映レイテンシを2秒以内、在庫照会のp95(95%がこの時間以内)応答を150ミリ秒以内、イベント重複許容率を0.01%以下、在庫差異の月次率を0.5%以下といった値を置くと、設計の優先順位が明確になります。SLOに基づき、DBインデックス戦略や書き込みパスの冪等性(同じ要求を繰り返しても結果が変わらない設計)、キューのバックプレッシャー(過負荷時に送出側を抑制する仕組み)設定を後述の実装に反映していきます。関連する可観測性の枠組みも参考になります。

Excelからの脱却:データモデルとETL

移行の骨格はスキーマ設計です。残高の正規化、イベントの一意性、参照整合性、権限と監査を担保できるかで難易度が決まります。まずはコアとなるSKU、ロケーション、在庫イベント、在庫残高の四テーブルを導入し、イベントを真実の単一ソースに据えます。以下はPostgreSQLでの基本定義例です(マテリアライズドビューは集計結果を物理化し高速参照を可能にする仕組みです)。

-- 1) マスタ: SKU とロケーション
CREATE TABLE sku (
  sku_id SERIAL PRIMARY KEY,
  sku_code TEXT UNIQUE NOT NULL,
  name TEXT NOT NULL,
  uom TEXT NOT NULL CHECK (uom IN ('EA','CS','KG','L')),
  is_active BOOLEAN NOT NULL DEFAULT TRUE
);

CREATE TABLE location (
  location_id SERIAL PRIMARY KEY,
  location_code TEXT UNIQUE NOT NULL,
  type TEXT NOT NULL CHECK (type IN ('WH','STORE','STAGING')),
  is_active BOOLEAN NOT NULL DEFAULT TRUE
);

-- 2) 不変のイベント: 入出庫と調整
CREATE TABLE inventory_event (
  event_id UUID PRIMARY KEY,
  occurred_at TIMESTAMPTZ NOT NULL,
  sku_id INT NOT NULL REFERENCES sku(sku_id),
  location_id INT NOT NULL REFERENCES location(location_id),
  qty NUMERIC(18,3) NOT NULL,
  kind TEXT NOT NULL CHECK (kind IN ('RECEIPT','ISSUE','ADJUSTMENT')),
  source_doc TEXT,
  actor TEXT NOT NULL,
  UNIQUE (source_doc, kind) -- 取引単位の冪等化
);

-- 3) 残高は導出。集計投影 or マテビュー
CREATE MATERIALIZED VIEW inventory_balance AS
SELECT sku_id, location_id,
       SUM(qty) AS on_hand
  FROM inventory_event
 GROUP BY sku_id, location_id;

CREATE INDEX idx_event_skuloc_time ON inventory_event(sku_id, location_id, occurred_at);

Excelのデータを取り込む際は、まずSKUコード、ロケーションコード、数量、イベント種別、発生日を標準化して、外部キーと検証に通します。品質担保にはデータバリデーションをコード化するのが近道です。PythonとPandas、さらにGreat ExpectationsやPanderaを併用すると、Excelのレイアウト差や型不整合、負数禁止などを機械的に弾けます(ETLパイプラインでの自動検証が要点です)。

# 4) Pandas + pandera での事前検証と整形
import pandas as pd
import pandera as pa
from pandera import Column, DataFrameSchema, Check

schema = DataFrameSchema({
    "sku_code": Column(str, Check.str_length(1, 64)),
    "location_code": Column(str, Check.str_length(1, 64)),
    "qty": Column(float, Check(lambda x: abs(x) < 1e9)),
    "kind": Column(str, Check.isin(["RECEIPT","ISSUE","ADJUSTMENT"])),
    "occurred_at": Column(pa.DateTime)
})

df = pd.read_excel("inventory.xlsx").rename(columns=str.lower)
df = schema.validate(df).assign(qty=lambda d: d["qty"].round(3))

検証に通ったデータは、参照マスタと結合して外部キーを解決し、冪等化キーを付与してイベントとしてロードします。イベントは不変なので、ダブルポストを避けるためのユニーク制約が重要です(同一イベントを重複登録しても結果が変わらないことが肝心です)。

# 5) 参照解決とUPSERT(冪等)
import uuid
import psycopg2

conn = psycopg2.connect(dsn="postgres://...")
cur = conn.cursor()

for row in df.itertuples():
    cur.execute("SELECT sku_id FROM sku WHERE sku_code=%s", (row.sku_code,))
    sku_id = cur.fetchone()[0]
    cur.execute("SELECT location_id FROM location WHERE location_code=%s", (row.location_code,))
    loc_id = cur.fetchone()[0]
    event_id = str(uuid.uuid5(uuid.NAMESPACE_URL, f"{row.kind}:{row.sku_code}:{row.location_code}:{row.occurred_at}:{row.qty}"))
    cur.execute(
        """
        INSERT INTO inventory_event(event_id, occurred_at, sku_id, location_id, qty, kind, source_doc, actor)
        VALUES (%s,%s,%s,%s,%s,%s,%s,%s)
        ON CONFLICT (event_id) DO NOTHING
        """,
        (event_id, row.occurred_at, sku_id, loc_id, row.qty, row.kind, "excel-import", "migration")
    )
conn.commit()

ロード後は、Excel側の期末残と新システムの残高を突合し、差異をイベント単位で可視化します。差異はイベント欠落、符号逆、ロケーションミス、SKUマッピングの四類型に大別されることが多く、修正もイベントで行います。直接残高を手で直すのは禁物です(監査性が失われます)。

-- 6) 期末残の突合(SKU/ロケーション単位)
WITH sys AS (
  SELECT s.sku_code, l.location_code, COALESCE(b.on_hand,0) AS on_hand
    FROM inventory_balance b
    JOIN sku s USING(sku_id)
    JOIN location l USING(location_id)
), xls AS (
  SELECT sku_code, location_code, SUM(qty) AS on_hand
    FROM staging_excel
   GROUP BY 1,2
)
SELECT COALESCE(sys.sku_code,xls.sku_code) AS sku_code,
       COALESCE(sys.location_code,xls.location_code) AS location_code,
       COALESCE(sys.on_hand,0) AS sys_on_hand,
       COALESCE(xls.on_hand,0) AS xls_on_hand,
       COALESCE(sys.on_hand,0) - COALESCE(xls.on_hand,0) AS diff
  FROM sys FULL OUTER JOIN xls USING(sku_code, location_code)
 ORDER BY sku_code, location_code;

APIと整合性:二重更新のない書き込み経路

運用移行の肝は、ユーザがExcelに書くのをやめても困らないAPIとUIの提供です。バックエンドは冪等な書き込みを備え、二重送信や再試行で重複が生じても一意キーで吸収します(Idempotency-Keyなどの活用)。FastAPIを例に、在庫調整APIを示します。また、出荷・入荷の実務がERPやWMSにまたがる場合は、システム間統合とイベント駆動で在庫を一元反映できる構成が有効です³。

# 7) FastAPI: 在庫調整の冪等なPOST
from fastapi import FastAPI, Header, HTTPException
from pydantic import BaseModel
import uuid, asyncpg, datetime

app = FastAPI()

class Adjustment(BaseModel):
    sku_code: str
    location_code: str
    qty: float
    occurred_at: datetime.datetime

@app.post("/inventory/adjustments")
async def create_adjustment(adj: Adjustment, Idempotency_Key: str | None = Header(default=None)):
    if not Idempotency_Key:
        raise HTTPException(status_code=400, detail="Missing Idempotency-Key")
    event_id = str(uuid.uuid5(uuid.NAMESPACE_URL, Idempotency_Key))
    conn = await asyncpg.connect(dsn="postgres://...")
    async with conn.transaction():
        sku_id = await conn.fetchval("SELECT sku_id FROM sku WHERE sku_code=$1", adj.sku_code)
        loc_id = await conn.fetchval("SELECT location_id FROM location WHERE location_code=$1", adj.location_code)
        try:
            await conn.execute(
                """
                INSERT INTO inventory_event(event_id, occurred_at, sku_id, location_id, qty, kind, source_doc, actor)
                VALUES ($1,$2,$3,$4,$5,'ADJUSTMENT','api', 'user')
                """,
                event_id, adj.occurred_at, sku_id, loc_id, adj.qty
            )
        except Exception as e:
            if "duplicate key" in str(e):
                return {"status":"ok","id": event_id, "duplicate": True}
            raise
    await conn.close()
    return {"status":"ok","id": event_id}

出荷や入荷の業務連携がERPやWMSにまたがる場合は、イベント駆動が有効です。KafkaやPulsarに在庫イベントスキーマを定義し、出荷指示、ピッキング完了、出荷確定といった業務イベントを在庫イベントに変換して取り込みます。イベントの一意性は外部システムのドキュメント番号と組み合わせると安定します(重複防止に有効です)。

// 8) 在庫イベントのスキーマ(JSON Schemaの一例)
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "InventoryEvent",
  "type": "object",
  "properties": {
    "id": {"type": "string", "format": "uuid"},
    "occurred_at": {"type": "string", "format": "date-time"},
    "sku_code": {"type": "string"},
    "location_code": {"type": "string"},
    "qty": {"type": "number"},
    "kind": {"type": "string", "enum": ["RECEIPT","ISSUE","ADJUSTMENT"]},
    "source_doc": {"type": "string"}
  },
  "required": ["id","occurred_at","sku_code","location_code","qty","kind"]
}
# 9) Kafka Producer(冪等なイベント送信)
from kafka import KafkaProducer
import json, uuid, datetime

producer = KafkaProducer(
    bootstrap_servers=["kafka:9092"],
    value_serializer=lambda v: json.dumps(v).encode("utf-8"),
    acks="all", enable_idempotence=True
)

def send_issue(sku_code, location_code, qty, source_doc):
    eid = str(uuid.uuid5(uuid.NAMESPACE_URL, f"ISSUE:{source_doc}:{sku_code}:{qty}"))
    evt = {
        "id": eid,
        "occurred_at": datetime.datetime.utcnow().isoformat() + "Z",
        "sku_code": sku_code,
        "location_code": location_code,
        "qty": -abs(qty),
        "kind": "ISSUE",
        "source_doc": source_doc
    }
    producer.send("inventory-events", value=evt, key=eid.encode())
    producer.flush()

読み取り系はホットパスを意識して最適化します。残高照会ではSKUとロケーションで適切にクラスタリングし、イベント集計を毎回行うのは避けます。マテリアライズドビューをトリガやスケジュールでリフレッシュするか、イベント到着時に残高を別テーブルでインクリメンタル更新する方式が現実的です。高負荷下でのp95レイテンシ150ミリ秒を守るには、インデックスの選定とI/O局所性が支配的になります。

-- 10) 高頻度照会に効くカバリングインデックス
CREATE INDEX idx_balance_cover ON inventory_balance(sku_id, location_id) INCLUDE (on_hand);

-- 在庫履歴の最新N件を素早く取る
CREATE INDEX idx_event_loc_time ON inventory_event(location_id, occurred_at DESC);

カットオーバー戦術と失敗しない検証

検証は移行プロジェクトの生命線です。Excelと新システムの二重運用期間を最短2週間程度確保し、日次の差異をイベント単位で分類して恒常的なバグと偶発的運用ミスを切り分けます。API経路に限定してイベントを生成できる状態を確立してから、Excelの編集権限を読み取り専用に変更し、期末時点での固定化を宣言します。カットオーバー当日は入出庫のフリーズ時間を短く抑え、バッチの残ジョブがない状態で在庫残高の権威ソースを切り替えます。ロールバックはイベントテーブルのオフセットに基づいて可能にし、直前のスナップショットを保持しておきます(復旧手順を事前演習しておくと安心です)。

差異率はSKU×ロケーションの組合せで母数が大きく、直感に反して数件のケアレスミスが全体の印象を左右します。ダッシュボードで差異の分布を可視化し、トップ寄与のSKUや拠点に対して集中的に是正を掛けるのが効果的です。グラフの設計は、時系列の差異率、イベント遅延の分布、APIエラー比率、そしてストレージのVACUUM/ANALYZE頻度を監視対象に含めると、運用の先手が打てます。

-- 11) 差異率のトラッキング(p95を監視)
WITH diff AS (
  SELECT date_trunc('day', occurred_at) AS d,
         ABS(SUM(qty)) FILTER (WHERE kind = 'ADJUSTMENT') AS adjust_abs,
         ABS(SUM(qty)) FILTER (WHERE kind IN ('RECEIPT','ISSUE')) AS flow_abs
    FROM inventory_event
   GROUP BY 1
)
SELECT d,
       CASE WHEN flow_abs = 0 THEN 0 ELSE adjust_abs / flow_abs END AS diff_rate
  FROM diff
 ORDER BY d;

Read/WriteのSLOを守るためのマイグレーション計画も事前に検証します。例えば、100並列でのイベント書き込みに対し、NVMeストレージのPostgreSQLなどでは数千events/sec規模のスループットとサブ数秒のp95レイテンシが得られる構成が報告されることもあります(構成・データ量・クエリ特性に依存)。イベントの肥大化は古いイベントのアーカイブとテーブルパーティショニング(テーブルを期間やキーで分割)で緩和できます。日付パーティションを切り、ホットパーティションのみインデックスを厚くする運用が現実的です。

-- 12) RANGEパーティショニングでホット/コールドを分離
CREATE TABLE inventory_event_y2025 PARTITION OF inventory_event
  FOR VALUES FROM ('2025-01-01') TO ('2026-01-01');

セキュリティと監査の実装ポイント

在庫は財務影響が大きく、監査要件を軽視できません。変更不能なイベントと責任追跡のため、イベントの署名や監査ログを導入します。さらに、更新系APIには役割ベースの権限を設け、調整は二人承認に限定するなど運用統制を技術で支えます。監査証跡はアプリ層のログだけでなく、DBのテーブル水準でも保持します(アプリ側の権限とDB側の制約を二重化すると堅牢です)。

-- 13) 監査ログ(軽量版)
CREATE TABLE audit_log (
  id BIGSERIAL PRIMARY KEY,
  occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  actor TEXT NOT NULL,
  action TEXT NOT NULL,
  entity TEXT NOT NULL,
  payload JSONB NOT NULL
);

CREATE OR REPLACE FUNCTION log_inventory_insert() RETURNS trigger AS $$
BEGIN
  INSERT INTO audit_log(actor, action, entity, payload)
  VALUES (NEW.actor, 'INSERT_EVENT', 'inventory_event', to_jsonb(NEW));
  RETURN NEW;
END; $$ LANGUAGE plpgsql;

CREATE TRIGGER trg_event_audit
AFTER INSERT ON inventory_event
FOR EACH ROW EXECUTE FUNCTION log_inventory_insert();

運用定着と継続改善:Excelを“見るだけ”にする

移行のゴールはExcelを捨てることではなく、Excelを安全なビューワへ変えることです。新システムを権威ソースとして固定した後は、BIやスプレッドシート連携を通じて只読のデータセットを提供し、フィールドの判断材料は最新在庫に一本化します。これにより、最終行の数式が壊れていたといった事故は自然消滅します。加えて、サイクルカウントを週次で回す仕組みを整え、差異が出やすいSKUや拠点に優先度を割り当てます。差異イベントの傾向や季節要因を分析し、安全在庫と発注点を動的に調整する仕組みを入れると、Excel時代の余剰緩衝を縮めながら欠品リスクを抑えることができます⁶,⁴。

技術負債を返済する最後のひと押しとして、在庫計算をイベントソーシング的に整理し、マテリアライズドビューの更新をイベントドリブン化すると、トラブル時の回復力が増します。ビューが壊れてもイベントから再投影すれば復旧でき、突合の説明責任も果たせます。現場の体感は、在庫照会が速い、差異の原因がすぐ見える、そして棚卸が楽になった、の三点に集約されます。これが定着すれば、もはやExcelに戻る理由はありません。応用として、製造の仕掛やキッティング、ロットトレースにも同じイベントモデルを拡張できます。デジタルツインの観点は出発点になります。

まとめ:小さく始め、イベントで終わらせる

Excelからの移行で最も重要なのは、機能の多さではなく正しさの一貫性です。SKU、ロケーション、イベントという核をまず固め、Excelは読み取りに退いてもらう。それだけで、差異率の低下、回転率の改善、棚卸時間の半減に手が届きます。SLOを先に置いて設計を逆算し、冪等なAPI、検証済みのETL、説明可能な差異管理という三点を揃えれば、二重運用の期間も短く、カットオーバーの不安も小さくできます。あなたの現場では、どのSKUや拠点が差異の大半を生んでいるでしょうか。今日、その一つをイベントとして正しく記録するところから始めてみてください。次の一歩は、在庫を“数式”ではなく“事実の流れ”として扱うことです。そこにExcel時代を越える再現性と速度が宿ります。

参考文献

  1. Newswise. Study Finds 94% of Business Spreadsheets Have Critical Errors. https://www.newswise.com/articles/study-finds-94-of-business-spreadsheets-have-critical-errors#:~:text=Newswise%20%E2%80%94%20A%20recent%20study,for%20better%20quality%20assurance%20practices
  2. SciELO Chile. Inventory record inaccuracy in retail environments. https://www.scielo.cl/scielo.php?pid=S0718-18762017000200006&script=sci_arttext#:~:text=,complexity%20of%20the%20store%20environment
  3. XPLUS Global. Why an ERP system is better than using Excel for inventory management. https://xplusglobal.com/blog/why-an-erp-system-is-better-than-using-excel-for-inventory-management/#:~:text=2,integrated%20with%20any%20other%20software
  4. NetSuite. Using Inventory Control Software for Cycle Counting. https://www.netsuite.com.hk/portal/hk/resource/articles/inventory-management/using-inventory-control-software-for-cycle-counting.shtml#:~:text=Because%20it%20doesn%E2%80%99t%20force%20companies,Other%20benefits%20include
  5. Wolters Kluwer. Modern solutions vs. Excel for inventory management. https://www.wolterskluwer.com/en/expert-insights/modern-solutions-vs-excel-for-inventory-management#:~:text=According%20to%20a%20Gartner%20survey%2C,switching%20from%20Excel%20for%20inventory
  6. Redalyc. Supply chain inventory policies and collaboration effects. https://www.redalyc.org/journal/3967/396773998017/html/#:~:text=One%20of%20the%20policies%20to,%282006%29%2C%20Barratt%20et%20al