Loading

ROUTE

ルートゼロの
アクティビティ

SQLの累積合計をSUM OVERで計算する方法

2

売上データを日付順に並べて、「その時点までの合計」を行ごとに表示したい。`GROUP BY`で合計を出すと明細行がまとまってしまい、思った形にならないことがあります。 明細行を残して累積合計を付けるなら、`SUM(金額) OVER (ORDER BY 並び順 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)`を使います。 チーム別に計算したければ`PARTITION BY`を追加します。この記事ではPostgreSQLを中心に、同じ日付のデータが複数ある場合の注意点まで、実行できるSQLで確認します。

まずはSUM OVERで累積合計を出す

例として、日付・チーム・売上金額を持つ`sales`テーブルを使います。金額は説明用の架空データです。

CREATE TABLE sales (
    sale_id  INTEGER PRIMARY KEY,
    sold_on  DATE NOT NULL,
    team     VARCHAR(20) NOT NULL,
    amount   INTEGER NOT NULL
);

INSERT INTO sales (sale_id, sold_on, team, amount) VALUES
(1, '2026-01-05', '営業A', 100),
(2, '2026-01-05', '営業A', 200),
(3, '2026-01-06', '営業A', 150),
(4, '2026-01-05', '営業B',  90),
(5, '2026-01-06', '営業B',  60);

チームの区別をせず、全体で1行ずつ積み上げる場合は次のように書きます。

SELECT
    sale_id, sold_on, team, amount,
    SUM(amount) OVER (
        ORDER BY sold_on, sale_id
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS running_total
FROM sales
ORDER BY sold_on, sale_id;

結果は次のとおりです。

sale_id 日付 チーム 金額 累積合計
1 2026-01-05 営業A 100 100
2 2026-01-05 営業A 200 300
4 2026-01-05 営業B 90 390
3 2026-01-06 営業A 150 540
5 2026-01-06 営業B 60 600

`OVER`は、どの行を集計対象にするかを指定する仕組みです。`ORDER BY sold_on, sale_id`で計算順を決め、`ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW`で「先頭行から現在行まで」を合計します。`sale_id`まで並び順に含めているのは、同じ日付の行の順序を確定するためです。

なお、`OVER`内の`ORDER BY`は計算順、最後の`ORDER BY`は表示順を指定します。結果の見せ方も固定したい場合は両方を書きます。


GROUP BYとの違いは「明細行を残せるか」

通常の集計では、次のように`GROUP BY`を使います。

SELECT team, SUM(amount) AS total_amount
FROM sales
GROUP BY team
ORDER BY team;

結果は「営業A=450」「営業B=150」の2行です。元の5行はチームごとにまとめられます。一方、`SUM(…) OVER (…)`なら元の明細5行を保持したまま、各行に計算結果を加えられます。

欲しい結果 適した書き方
チームごとの合計を1行ずつ `GROUP BY team`
各売上明細を残した累積合計 `SUM(amount) OVER (ORDER BY …)`
明細を残したチーム別累積合計 `SUM(amount) OVER (PARTITION BY team ORDER BY …)`
日別合計を作ってから累積 日別`GROUP BY`の結果に`SUM(…) OVER`

(GROUP BYの基本やWHERE・HAVINGの違いについては『GROUP BY使い方完全解説』をご参照ください)


PARTITION BYでチームごとに累積をリセットする

全社合計ではなく、チームごとに売上を積み上げたいなら`PARTITION BY team`を指定します。

SELECT
    sale_id, sold_on, team, amount,
    SUM(amount) OVER (
        PARTITION BY team
        ORDER BY sold_on, sale_id
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS team_running_total
FROM sales
ORDER BY team, sold_on, sale_id;
チーム sale_id 金額 チーム内累積
営業A 1 100 100
営業A 2 200 300
営業A 3 150 450
営業B 4 90 90
営業B 5 60 150

`PARTITION BY`は集計の区切りです。営業Aの累積は営業Bへ持ち越されません。月別、店舗別、担当者別のように「区切りごとに累積を出す」場面で応用できます。


同じ日付がある場合はROWSと並び順を確認

累積合計が意図と違うときは、`ORDER BY`の重複値が原因かもしれません。PostgreSQLでは、ウィンドウの`ORDER BY`を指定してフレームを省略すると、既定のフレームは同順位の行(peer)まで含みます。

(PostgreSQLでの既定フレームと同順位の扱いについては『PostgreSQL公式ドキュメントのウィンドウ関数の説明』をご参照ください)

例えば`ORDER BY sold_on`だけだと、2026-01-05の3行は同じ日付です。先頭の行でも、その日付の売上合計390まで計算対象になり得ます。「明細を上から1行ずつ足す」結果が必要なら、`ROWS`を明示し、`sale_id`のような一意の列も並び順に加えると意図を明確にできます。

SUM(amount) OVER (
    ORDER BY sold_on, sale_id
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
)

`ROWS`だけ指定しても、同じキーの内部順が決まっていなければ結果の対応関係が安定しません。並び順とフレーム指定はセットで確認してください。

日別の合計を作ってから累積する場合

「明細1件ずつ」ではなく「日付ごと」の累積が欲しいときは、先に日別合計を作ります。

WITH daily_sales AS (
    SELECT
        sold_on,
        SUM(amount) AS daily_amount
    FROM sales
    GROUP BY sold_on
)
SELECT
    sold_on,
    daily_amount,
    SUM(daily_amount) OVER (
        ORDER BY sold_on
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS running_total
FROM daily_sales
ORDER BY sold_on;

この例では1月5日の日別合計が390、1月6日が210なので、日別累積は390、600となります。`GROUP BY`とウィンドウ関数は競合するものではなく、集計する単位によって組み合わせて使うものです。

累積が合わないときの確認ポイント

  • チームや担当者をまたいで合計される:`PARTITION BY`が必要か確認する。
  • 同じ日付の行がまとめて加算される:デフォルトのウィンドウフレームと`ROWS`指定を確認する。
  • 明細の表示順と累積順が合わない:`OVER`内と最終結果の`ORDER BY`を確認する。
  • 期間を絞ると初期値が変わる:`WHERE`で除外された行はウィンドウ計算の対象外。全期間の累積を計算してから期間を切り出すなら、サブクエリで計算と表示のフィルタを分ける。
  • 日別・月別の1行が欲しい:先に`GROUP BY`でその単位へ集約してから累積する。

大きなテーブルでは、実行計画・ソート・インデックスの効果はデータ量やDBMSで変わります。特定の書き方が常に高速とは決めつけず、実データと実行計画で判断します。

まとめ:何を1行として表示したいかから決める

`SUM(…) OVER`を使えば、明細を残したまま累積合計を計算できます。`GROUP BY`は集約後の1行が欲しいとき、`PARTITION BY`はチームなどで計算を区切りたいときに使います。

まずはサンプルの5行で`GROUP BY`との結果行数の違いを確認し、同じ日付を含むデータで`ROWS`の有無を比べてみてください。実務のSQLへ応用するときは、何を1行とするか、どの列で順序を確定するかを先に決めると迷いにくくなります。

ルートゼロの採用情報を見る

RANKINGranking-icon

すべてを開示している会社です

ポジティブな面もネガティブな面も、包み隠さず誠実にお答えします。
気になることがあれば、面談の際にすべてオープンにお話します。