ガイド

累計を計算する — SUM() OVER (ORDER BY ...)

日次売上の累計や残高推移など、積み上げ計算を出したいとき。

累計を計算する — SUM() OVER (ORDER BY ...) diagram

問題 — 累計が欲しい

「日ごとの売上を積み上げた累計」「月初からの累計」「口座残高の推移」といった累計(running total)は非常によくある要件です。

昔は自己結合や相関サブクエリで書いていたこの処理も、ウィンドウ関数のある現代なら一発。SUM() OVER (ORDER BY ...) が基本形です。

基本形

ORDER BY を付けた SUM() OVER() が累計の基本。デフォルトのウィンドウフレームは RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW(先頭から現在行まで)なので、省略でも累計になります。

sql
SELECT
  sale_date,
  amount,
  SUM(amount) OVER (ORDER BY sale_date) AS running_total
FROM daily_sales
ORDER BY sale_date;

PARTITION BY でリセットする

「商品ごとに累計」「月初で累計をリセット」のようにグループ単位で累計をやり直すには PARTITION BY を足すだけ。月単位リセットなら、月を PARTITION BY DATE_TRUNC('month', sale_date) のように指定します。

sql
-- 商品ごと、月単位でリセットする累計
SELECT
  product_id,
  sale_date,
  amount,
  SUM(amount) OVER (
    PARTITION BY product_id, DATE_TRUNC('month', sale_date)
    ORDER BY sale_date
  ) AS mtd_total
FROM daily_sales;

ウィンドウフレーム — ROWS vs RANGE

フレーム指定は ROWSRANGE の2種類があり、挙動が異なります:

  • ROWS: 純粋に行番号ベースで「前何行〜現在行」
  • RANGE: ORDER BYベースで、同じ値の行は同じ位置扱い(ピア)

日付ごとの累計で同じ日付が複数行ある場合、デフォルトの RANGE では同一日の全行が同一累計値になります。1行ずつ積みたいなら明示的に ROWS を使う。移動平均(過去N行の平均)なども ROWS が自然。

sql
-- 過去7日間の移動平均 (ROWS を明示)
SELECT
  sale_date, amount,
  AVG(amount) OVER (
    ORDER BY sale_date
    ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
  ) AS avg_7d
FROM daily_sales;

注意点

ORDER BY を忘れると累計にならないSUM() OVER (PARTITION BY ...) だけだとパーティション全体の合計(全行同じ値)になります。これを知らずに書いてバグらせるのは初心者あるある。

MySQL は 8.0 からウィンドウ関数対応(それ以前は変数トリックで代替)。SQLite は 3.25 から。古い環境でコードを動かす場合は要確認。

関連トピック