売上データを日付順に並べて、「その時点までの合計」を行ごとに表示したい。`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行とするか、どの列で順序を確定するかを先に決めると迷いにくくなります。