存貨成本先進先出法(FIFO),
可以用 SQL 在資料倉儲算嗎?

存貨成本的先進先出法(FIFO)可以用 SQL 的 window function 在資料倉儲裡計算,不必把資料搬進應用程式逐筆處理。 我們處理過的一個案例,客戶的工程師用 500 多行程式還沒完成,改用 SQL 約 50 行就完成。

為什麼放在資料倉儲算

放在資料倉儲算,是因為資料量一大,把資料從資料庫搬到程式裡的時間,會遠多於計算本身的時間。 把運算放到資料所在的地方,也就是「搬移程式到資料端」(move code to data),可以省掉大量 IO。

另一個好處是除錯。每一步的中間結果都可以寫成一個 view 直接查詢,像在 Excel 裡逐欄檢查一樣,容易找出哪一步算錯。

作法:用累計數量對應累計成本

FIFO 的關鍵,是把每筆出庫換算成「累計出庫量」,再看它落在哪一批進貨裡:

  1. 進貨:依時間算出每批的累計數量與累計成本。
  2. 出庫:依時間算出累計出庫量。
  3. 用累計出庫量找到對應的進貨批次,按該批的單價內插出「累計出庫成本」。
  4. 本筆出庫成本=本筆的累計出庫成本-上一筆的累計出庫成本。一筆出庫即使跨了兩批進貨,成本也正確。

以一個品項為例,依時間先後:

異動數量FIFO 成本說明
進貨10單價 100第一批
進貨10單價 120第二批
出庫44004 個取自第一批
出庫6600剛好用完第一批
出庫56005 個取自第二批
進貨10單價 150第三批
出庫81,050跨兩批:5 個 × 120 + 3 個 × 150

以下 SQL 改寫自《從試算表到資料平台》第十章的範例,在 DuckDB 測試過,算出的就是上表的結果。stock_moves 每列是一筆進貨(in)或出庫(out):

-- stock_moves: one row per receipt ('in') or issue ('out') of an item
WITH receipts AS (            -- running totals of quantity and cost received
  SELECT item,
         sum(qty)             OVER w                  AS cum_in_qty,
         coalesce(sum(qty)    OVER prev_w, 0)         AS prev_in_qty,
         sum(qty * unit_cost) OVER w                  AS cum_in_cost,
         coalesce(sum(qty * unit_cost) OVER prev_w, 0) AS prev_in_cost
  FROM stock_moves
  WHERE direction = 'in'
  WINDOW w      AS (PARTITION BY item ORDER BY moved_at),
         prev_w AS (PARTITION BY item ORDER BY moved_at
                    ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING)
),
issues AS (                   -- running total of quantity issued
  SELECT move_id, item, moved_at, qty,
         sum(qty) OVER (PARTITION BY item ORDER BY moved_at) AS cum_out_qty
  FROM stock_moves
  WHERE direction = 'out'
),
issue_cost AS (               -- cumulative FIFO cost at each issue's running total
  SELECT i.move_id, i.item, i.moved_at, i.qty,
         r.prev_in_cost + (i.cum_out_qty - r.prev_in_qty)
           / (r.cum_in_qty - r.prev_in_qty) * (r.cum_in_cost - r.prev_in_cost) AS cum_out_cost
  FROM issues i
  JOIN receipts r
    ON i.item = r.item
   AND i.cum_out_qty >  r.prev_in_qty    -- half-open range: an issue ending exactly
   AND i.cum_out_qty <= r.cum_in_qty     -- on a batch boundary matches one batch only
)
SELECT move_id, item, moved_at, qty,
       cum_out_cost - coalesce(lag(cum_out_cost) OVER (PARTITION BY item ORDER BY moved_at), 0)
         AS fifo_cost
FROM issue_cost
ORDER BY item, moved_at;

使用時要注意兩點:同一品項的 moved_at 不能重複,否則先後順序不確定;出庫量超過累計進貨量的那幾筆,不會出現在結果裡。

Excel 裡的計算,搬進資料倉儲的一般作法

FIFO 是一個例子。其他原本在 Excel 裡做的計算,搬進資料倉儲時大致是同一套作法:

延伸閱讀

想討論貴公司的計算怎麼搬進資料倉儲,歡迎直接來信。

來信談談你的資料問題