月次帳票で「前月比」と「累計」を出す。これ、SQL でどう書いてました??
カーソルで1行ずつ回して前の行を覚えておく。同じテーブルを自分自身に結合して1ヶ月ずらす。俺も昔はそっち側でした。動くんだけど、長い。そして遅い。
どうも、ヒロポンです。
今回は SQL Server の窓関数を、月次帳票の実務で使う3パターンで解説します。LAG で前月比、SUM() OVER で累計、LEAD で翌月比較。構文をただ並べるんじゃなくて、「どこで区切って、どう並べるか」という OVER 句の考え方から積み上げていきます。ここが腹落ちすると、窓関数って一気に手に馴染むんですよね。
前月比・累計・順位みたいな「行をまたいだ計算」を、カーソルや自己結合で書いてた業務SE 向けです。
はじめに: 行をまたいで計算したい、という課題
普通の集計(SUM / GROUP BY)は、行をグループにまとめて1行に畳みます。でも帳票でやりたいのはそっちじゃない。各行を残したまま、他の行を参照した値を横に足すほうです。
「この行の売上と、前の月の売上を比べたい」「ここまでの累計を各行に出したい」。この「行を畳まずに、行をまたいで計算する」やつ。これを解決するのが窓関数(Window Function)です。
GROUP BY は縦に畳む。窓関数は横に列を足す。このイメージの差が腹落ちすると、いい感じに一気に見えてきます。ここが出発点。
窓関数以前: カーソルと自己結合で書いていた
窓関数がなかった頃(や、知らなかった頃)、前月比はこう書いてました。
-- 自己結合で1ヶ月前を引っ張る(古いやり方)
SELECT t1.month, t1.sales,
t1.sales - t2.sales AS diff
FROM Monthly t1
LEFT JOIN Monthly t2 ON t2.month = DATEADD(MONTH, -1, t1.month);
累計に至っては、カーソルで1行ずつ回して変数に足し込む。そういう処理を書いてた人、多いはず。動くんですよ。ただ、自己結合は行数が増えると重くなる。カーソルは1行ずつなので遅い。しかもコードが長い。
窓関数は、これを1つの OVER 句に置き換えます。まずはその OVER 句の考え方から。
窓関数の基本: OVER 句の PARTITION BY と ORDER BY
窓関数の心臓は OVER 句です。ここで「窓(ウィンドウ)=計算に使う行の範囲」を決める。決めることは2つだけ。
PARTITION BY: どこで区切るか(グループの境目)。部門ごと、商品ごと、みたいなORDER BY: 窓の中でどう並べるか。前月比や累計は「時間順」が要る

PARTITION BY を書かなければ全行が1つの窓。書けばグループごとに窓が分かれてリセットされる。ORDER BY は「前」「次」「ここまで」を決めるための並び順。この2つで窓が決まる。全部の窓関数に共通する土台がこれです。
ここまでで、窓関数は「区切って、並べて、その上で計算する」ものだと掴めれば OK。では3パターン、順にいきます。
パターン1: LAG で前月比を出す
LAG(列) は、窓の中で1つ前の行の値を返します。ORDER BY month で月順に並べれば、「1つ前の行 = 前月」になる。
SELECT
month,
sales,
LAG(sales) OVER (ORDER BY month) AS prev_sales,
sales - LAG(sales) OVER (ORDER BY month) AS diff
FROM Monthly
ORDER BY month;
実行結果(prev_sales に前月・diff に前月差・先頭行は NULL):

LAG(sales) が前月の売上、sales - LAG(sales) が前月差。あの自己結合が丸ごと1行に消えました!! 前月比(割合)にしたいなら sales * 1.0 / LAG(sales) OVER (ORDER BY month) です。
部門ごとに前月比を出すなら OVER (PARTITION BY dept ORDER BY month)。部門の境目で LAG がリセットされるので、隣の部門の値を誤って引くこともない。これが PARTITION BY の効きどころ。
(ここまで: LAG = 前の行、ORDER BY で「前」が決まる、PARTITION BY で区切る。この3点。)
パターン2: SUM() OVER で累計を出す(フレームに注意)
累計は SUM(列) OVER (ORDER BY ...)。ただ、ここに窓関数で一番踏む落とし穴が待ってます。
まず正しい書き方から。
SELECT
month,
sales,
SUM(sales) OVER (
ORDER BY month
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_total
FROM Monthly
ORDER BY month;
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW が「先頭からこの行まで」というフレーム(範囲)の指定。これで行ごとに積み上がる累計になります。
問題は、このフレームを省くとどうなるか。SUM(sales) OVER (ORDER BY month) とだけ書くと、既定のフレームは ROWS じゃなくて RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW になる。
ORDER BY の値が全部ユニークなら、RANGE でも ROWS でも結果は同じ。でも、同じ月が複数行ある(重複がある)と話が変わります。RANGE は「同じ ORDER BY 値の行を全部まとめて」扱う。だから重複月のところで累計が一気にジャンプする。
行単位で1行ずつ積み上げたいなら、必ず ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW を明示。これが累計の鉄則です。「なんか累計の数字が飛んでる??」と思ったら、まずフレームを疑ってください。
実機で再現(同じ月 2026-03-01 が2行ある時、ROWS 明示版と RANGE 既定版で累計が変わる):

上の対比、重複月の行で ROWS 版は 270、RANGE 既定版は 300。RANGE は「同じ月の行を全部まとめて」足すので、重複があるとこう変わる。
(ここまで: 累計は SUM() OVER、ただしフレームを ROWS で明示しないと重複月で累計がずれる。)
パターン3: LEAD で翌月と比べる
LEAD(列) は LAG の逆。窓の中で1つ後の行の値を返します。ORDER BY month なら「翌月」ですね。
SELECT
month,
sales,
LEAD(sales) OVER (ORDER BY month) AS next_sales,
LEAD(sales) OVER (ORDER BY month) - sales AS next_diff
FROM Monthly
ORDER BY month;
「翌月に向けてどう変化するか」を各行に出したい時に使います。LAG と LEAD はセットで覚えると、前後どっちとも比較できるので楽。第2引数でずらす行数(LAG(sales, 3) で3ヶ月前)も、値がない時の既定値(LAG(sales, 1, 0))も指定できます。
3つの窓関数の役割を並べるとこうです。

適用判断: 窓関数 vs カーソル・自己結合
「行をまたぐ計算」は、まず窓関数を第一候補にしていいです。
- 前月比・前年比・順位・累計 → 窓関数(
LAG/LEAD/ROW_NUMBER/SUM() OVER) - 1行ずつ手続き的に複雑な状態を持つ処理 → カーソルが要ることもある(が稀)
自己結合は行数が増えると結合コストで重くなる。カーソルは1行ずつで遅い。窓関数はテーブルを基本1回スキャンして窓の中で計算するので、短く書けて速いことが多い。帳票みたいな「並べて横に足す」処理とは相性がいい。
俺の現場ではこう使ってる
俺が月次の売上帳票を触ってた時、前月比と累計を自己結合とカーソルで書いた SQL が、数百行あって誰も触りたがらないやつになってました。
これを LAG と SUM() OVER に書き換えたら、数百行が数十行になって、実行時間も目に見えて縮んだ。マジで別物です!! 何より、後から見た人が「何を出してるか」読める。OVER (PARTITION BY dept ORDER BY month) の1行で「部門ごと・月順」が伝わるので、コメントもいらない。いい感じに自己説明的になります。
窓関数は「知ってるかどうか」で帳票 SQL の見た目と速度がガラッと変わる領域です。カーソルで消耗してるなら、まず LAG と SUM() OVER から入るのがいい感じにハマると思います。
まとめ
SQL Server の窓関数、帳票で使う3パターン。
LAG→ 1つ前の行(前月比)。ORDER BYで「前」を決めるSUM() OVER→ 累計。ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROWを明示(省くと重複月でずれる)LEAD→ 1つ後の行(翌月比較)- 土台:
OVER句のPARTITION BY(区切り)とORDER BY(並び)で窓が決まる
「行をまたぐ計算」をカーソルや自己結合で書いていたなら、窓関数一本で短く・速くなります。OVER 句の考え方さえ掴めば、前月比も累計も順位も同じ発想で書ける。帳票 SQL の保守が、いい感じにラクになります。
よくある質問
窓関数はどのバージョンの SQL Server から使えますか?
LAG / LEAD / SUM() OVER のフレーム指定は SQL Server 2012 以降で使えます。2016 現場でも問題なく動きます。互換性レベルが極端に古い場合だけ確認してください。行番号の ROW_NUMBER は 2005 からで、こっちの落とし穴は SQL Server ROW_NUMBER の落とし穴 にまとめてます。
PARTITION BY と GROUP BY は何が違いますか?
GROUP BY は行をグループごとに1行へ畳みます。PARTITION BY は行を畳まず、グループごとに窓を区切るだけ。元の行数はそのまま残ります。「各明細行を残したまま、グループ内の集計を横に足したい」時が PARTITION BY、「グループの合計だけ欲しい」時が GROUP BY です。
窓関数を使うと実行計画はどうなりますか?
多くの場合、ORDER BY に沿った Sort と Window Spool 系の演算子が入ります。適切なインデックスがあれば Sort を省けて速くなる。自己結合より軽いことが多いですが、大量データでは実行計画の確認を。見積りと実測のズレは SQL Server 実行計画の見積りと実測 を参考にしてください。
累計の数字が飛ぶんですが??
フレーム指定が原因のことが多いです。SUM() OVER (ORDER BY month) のようにフレームを省くと既定が RANGE になり、同じ月が複数行あるとその行すべてに合計が入って飛びます。ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW を明示すると1行ずつの累計になります。
次に読むべき記事
窓関数みたいな「知ってると SQL が一段レベルアップする道具」を1つずつ増やしていくと、任される仕事の幅が変わってきます。帳票 SQL 周りで合わせて読むと効く記事を置いておきます。





以上!
カーソルで累計書いてた時代を思い出して「うわ」ってなった人、どんどんシェア待ってるぜ!!
執筆者
バイブス父さん — 業務 SE 7 年(正社員 2 / フリーランス 5)。現職は SEO 直轄部の AI アドバイザー兼 PL、副業で中小 SIer の CTO。SIer 正社員から独立し、複数のフリーランスエージェント経由で現場を渡り歩いた経験ベースで「業務 SE 視点」の技術 + キャリア記事を書いています。
🐦 X: @hiro_progra0524(日々の現場メモ更新中)
📝 About Me で経歴詳細を見る



