累計を計算する — SUM() OVER (ORDER BY ...)
日次売上の累計や残高推移など、積み上げ計算を出したいとき。
問題 — 累計が欲しい
「日ごとの売上を積み上げた累計」「月初からの累計」「口座残高の推移」といった累計(running total)は非常によくある要件です。
昔は自己結合や相関サブクエリで書いていたこの処理も、ウィンドウ関数のある現代なら一発。SUM() OVER (ORDER BY ...) が基本形です。
基本形
ORDER BY を付けた SUM() OVER() が累計の基本。デフォルトのウィンドウフレームは RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW(先頭から現在行まで)なので、省略でも累計になります。
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) のように指定します。
-- 商品ごと、月単位でリセットする累計
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
フレーム指定は ROWS と RANGE の2種類があり、挙動が異なります:
- ROWS: 純粋に行番号ベースで「前何行〜現在行」
- RANGE:
ORDER BYの値ベースで、同じ値の行は同じ位置扱い(ピア)
日付ごとの累計で同じ日付が複数行ある場合、デフォルトの RANGE では同一日の全行が同一累計値になります。1行ずつ積みたいなら明示的に ROWS を使う。移動平均(過去N行の平均)なども ROWS が自然。
-- 過去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 から。古い環境でコードを動かす場合は要確認。
