毎月、先月の資料を開いて前月の売上を書き写し、電卓で前月比を出している。前年比も出したいが、1年前のファイルを探すのが手間。SQLで出してみたら、数字がどこか合わない。
最初に確認したいのは、月別の集計表が「1か月1行」で、抜けている月がないかです。そこがそろっていれば、前月比も前年比も、LAG関数1本で出せます。
この記事でわかること
- LAG関数で前月比・前年比を出すSQLの書き方
- 月の抜け・店舗の混ざりなど、ずれる3つの原因
- 比較をSQLにすると業務の何が良くなるか
結論:前月比は「LAGで前月の値を横に並べてから割る」の2段で出す
前月比は、LAGで前の月の売上を同じ行に並べ、今月の売上をそれで割れば出せます。
LAGは、前の行の値を取り出す関数です。ウィンドウ関数と呼ばれる仲間の1つです。前の行をひょいと見に行けるので、地味に好きな関数です。
ウィンドウ関数は、行をまとめずに、前後の行を参照して計算します。GROUP BYと違い、元の行はそのまま残ります。
書き方の骨組みは、次のとおりです。
LAG(売上の列, 何行前か)
OVER (
PARTITION BY 分ける列
ORDER BY 並べる列
)
- 何行前か:省略すると1。12なら12行前
- PARTITION BY:店舗別などに分けるときに使う
- ORDER BY:何の順で「前」を決めるか
前の行がないとき(最初の月など)は、NULL(値なし)が返ります。
注意したいのは、LAGが取るのは「前の月」ではなく「並べたときの前の行」だということです。月が抜けていたり、店舗が混ざっていたりすると、別の月の値を取ります。エラーは出ません。
この記事のSQLは、PostgreSQLとMySQL 8.0以降を想定しています。ほかのDBで使う場合は、そのDBの資料でLAGの書き方を確かめてください。
基本の書き方:月別の売上表から、前月の売上と前月比を出す
前月比のSQLは「LAGで前月の売上を出す」「100.0を掛けてから割る」の2つで書けます。
以下は説明のための架空の例です。月別の売上を1か月1行にまとめた表を使います(単位は万円。sales は整数の列)。
入力データ(monthly_sales)
| ym | sales |
|---|---|
| 2026-07 | 400 |
| 2026-08 | 500 |
| 2026-09 | 450 |
ym は「2026-07」のように、月を2桁にそろえた文字列です。桁がそろっていれば、文字の並び順が月の順番と一致します。
まず、前月の売上を横に並べます。
SELECT
ym,
sales,
LAG(sales) OVER (ORDER BY ym) AS prev_sales
FROM monthly_sales;
実行結果
| ym | sales | prev_sales |
|---|---|---|
| 2026-07 | 400 | NULL |
| 2026-08 | 500 | 400 |
| 2026-09 | 450 | 500 |
なぜこうなるか:ORDER BY ym で月の順に並べ、1行前の sales を取り出しています。
次に、前月比を計算します。
SELECT
ym,
sales,
ROUND(
sales * 100.0
/ NULLIF(LAG(sales) OVER (ORDER BY ym), 0),
1
) AS mom_pct
FROM monthly_sales;
実行結果
| ym | sales | mom_pct |
|---|---|---|
| 2026-07 | 400 | NULL |
| 2026-08 | 500 | 125.0 |
| 2026-09 | 450 | 90.0 |
割り算の書き方には、2つの理由があります。
100.0を先に掛ける:PostgreSQLでは、整数どうしの割り算は小数点以下が切り捨てられます(5/2は2)。先に100.0を掛けると、小数のまま計算されます。MySQLの / は、整数どうしでも小数を返します。
NULLIFで0を除く:NULLIF(値, 0) は、値が0のときにNULLを返します。0で割ると、PostgreSQLではエラー、MySQLではNULLになります。NULLIFを通すと、どちらでもNULLになります。
「前月比」は、125%のように比率で言う職場と、+25%のように増減で言う職場があります。増減で出すなら、mom_pct から100を引きます。どちらで出すかは、資料を見る人と先に合わせておきます。
前年比:LAG(sales, 12)は「12行前」。月が全部そろっていることが前提
前年同月の売上はLAG(sales, 12)で取れますが、それは月が1つも抜けずに並んでいるときだけです。
SELECT
ym,
sales,
LAG(sales, 12) OVER (ORDER BY ym) AS prev_year_sales
FROM monthly_sales;
2つ目の引数の12は「12行前」という意味です。最初の12か月は12行前がないので、NULLです。
前年比も、前月比と同じ形で計算します。NULLIF の中を LAG(sales, 12) OVER (ORDER BY ym) に替えるだけです。
裏を返すと、途中で1か月でも行が抜けると、12行前は「13か月前」になります。エラーは出ず、前年比らしい数字が出ます。確かめ方と直し方は、次の「詰まりやすい3つのポイント」で扱います。
初学者が詰まりやすい3つのポイント:LAGは「前の月」ではなく「前の行」を取る
3つのポイントはどれも、「前の行」が「比べたい前の月」になっていないときに起きます。
前月比・前年比がおかしいときは、次の順に確かめます。
| 確認すること | 確かめ方 | ずれると |
|---|---|---|
| 月が抜けていないか | 前の行の月を並べる | 前々月と比べる |
| 店舗が混ざっていないか | 1か月の行数を数える | 別の店舗と比べる |
| 絞り込みの順番 | WHEREの位置を見る | 前月がNULLになる |
1. 前の月の行がなくても、LAGは前の月を取ると思う
問題と原因:売上がなかった月や、取り込みが漏れた月は、月別の表に行ができません。LAGは抜けた月を飛ばして、さらに前の月の値を取ります。
架空の例で、8月の行がない表を見てみます。
| ym | sales |
|---|---|
| 2026-07 | 400 |
| 2026-09 | 450 |
| 2026-10 | 480 |
確認方法:前の行の月を、一緒に出します。
SELECT
ym,
LAG(ym) OVER (ORDER BY ym) AS prev_ym,
LAG(sales) OVER (ORDER BY ym) AS prev_sales
FROM monthly_sales;
| ym | prev_ym | prev_sales |
|---|---|---|
| 2026-07 | NULL | NULL |
| 2026-09 | 2026-07 | 400 |
| 2026-10 | 2026-09 | 450 |
9月の「前の行」は7月です。このまま計算すると、9月の前月比は112.5%(450÷400)になります。
どう直すか:抜けのない「月の一覧」を先に用意し、そこに売上をLEFT JOIN(左側の行を全部残す結合)します。行のない月も、一覧の側に残ります。
行がない月を、確かめずに0で埋めない。「売上がなかった」なら0で正しいです。「データの取り込み漏れ」なら、0ではありません。抜けた月を見つけたら、先に元データを確かめます。
次のSQLは、8月が「売上なし」だと確かめた後の例です。
WITH filled AS (
SELECT
m.ym,
COALESCE(s.sales, 0) AS sales
FROM months AS m
LEFT JOIN monthly_sales AS s
ON s.ym = m.ym
)
SELECT
ym,
sales,
LAG(sales) OVER (ORDER BY ym) AS prev_sales
FROM filled;
months は、2026-07から2026-10までの4か月を1行ずつ持つ表です(架空)。COALESCE(s.sales, 0) は、売上がNULLの月を0にします。取り込み漏れだった場合は、元データを直してから集計し直します。
| ym | sales | prev_sales |
|---|---|---|
| 2026-07 | 400 | NULL |
| 2026-08 | 0 | 400 |
| 2026-09 | 450 | 0 |
| 2026-10 | 480 | 450 |
9月の前の行が、8月になりました。このまま前月比のSQLに入れると、9月は前月が0なので、NULLIFによりNULLになります。比べられないことが、そのまま見える形になります。
月の一覧は、Excelで作った月の列を取り込むなどして用意します。PostgreSQLなら、generate_series関数で日付の連番を作ることもできます。
2. 店舗の順に並べれば、店舗ごとの前月と比べられると思う
問題と原因:店舗別の前月比を1つのSQLで出すとします。ORDER BYで店舗順に並べても、店舗の境目はLAGに伝わりません。次の店舗の最初の月が、前の店舗の最後の月と比べられます。
架空の店舗別の表(store_sales)です。
| store | ym | sales |
|---|---|---|
| A | 2026-08 | 500 |
| A | 2026-09 | 450 |
| B | 2026-08 | 300 |
SELECT
store,
ym,
LAG(sales) OVER (ORDER BY store, ym) AS prev_sales
FROM store_sales;
| store | ym | prev_sales |
|---|---|---|
| A | 2026-08 | NULL |
| A | 2026-09 | 500 |
| B | 2026-08 | 450 |
店舗Bの8月が、店舗Aの9月の450と比べられています。
確認方法:月別の表で、同じ月が何行あるかを数えます。1か月に2行以上あるなら、店舗などが混ざっています。
どう直すか:PARTITION BY で、比べる単位を分けます。
LAG(sales) OVER (
PARTITION BY store
ORDER BY ym
) AS prev_sales
PARTITION BYを書くと、店舗ごとに「前の行」を探します。店舗Bの8月は店舗Bの最初の行なので、NULLになります。
3. WHEREで対象の月に絞っても、前月比は出せると思う
問題と原因:「9月の前月比だけ出したい」とWHEREで9月に絞ると、前月比がNULLになります。ウィンドウ関数は、WHEREで絞り込んだ後の行で計算されるからです。8月の行は、LAGが見る前に消えています。
SELECT
ym,
sales,
LAG(sales) OVER (ORDER BY ym) AS prev_sales
FROM monthly_sales
WHERE ym = '2026-09';
| ym | sales | prev_sales |
|---|---|---|
| 2026-09 | 450 | NULL |
期間で絞った場合も、絞った最初の月の前月比がNULLになります。
どう直すか:先にWITH句の中で全部の月にLAGを計算し、外側で9月に絞ります。WITH句は、SQLの中で一時的な表に名前をつける書き方です。
WITH calc AS (
SELECT
ym,
sales,
LAG(sales) OVER (ORDER BY ym) AS prev_sales
FROM monthly_sales
)
SELECT ym, sales, prev_sales
FROM calc
WHERE ym = '2026-09';
| ym | sales | prev_sales |
|---|---|---|
| 2026-09 | 450 | 500 |
それでも合わないとき:LAGの前に、月別の集計表そのものを確かめる
LAGの書き方が正しくても、元の月別の集計がずれていれば、前月比もずれます。
次の順に確かめます。
- 月の合計が、既存の月次レポートと一致するか
- 月を区切る日付が、資料と同じ列か
- JOINで行が増えていないか
2つ目は、注文日と売上の計上日のどちらで区切るか、という違いです。同じ「9月の売上」でも、区切る日付が違えば数字が変わります。
日時の列から「2026-09」の形を作る関数は、DB製品で違います。PostgreSQLならTO_CHAR、MySQLならDATE_FORMATなどを使います。
集計の件数・合計がずれる原因の探し方は、次の記事にまとめています。
業務への着地:毎月の比較が、同じSQLを実行し直すだけになる
前月比をSQLにすると、前月の数字を探して書き写す作業がなくなります。
架空の例で、Before/Afterを比べると次のとおりです。
| 作業 | Before(手作業) | After(SQL) |
|---|---|---|
| 前月・前年の数字 | 過去の資料から書き写す | LAGで前の行から取る |
| 計算 | 1か月ずつ電卓で割る | 全部の月をまとめて出す |
| 店舗が増えたとき | 店舗の数だけくり返す | PARTITION BYで分ける |
| 確認 | 書き写しを目で見比べる | 月の抜けと合計を見る |
良くなるのは、計算の速さだけではありません。書き写しがなくなるので、書き写しのミスを探す見比べもなくなります。確認は「月が抜けていないか」「月の合計が合っているか」の2つに絞れます。
SQLには、何と何をどう比べたかがそのまま残ります。担当が変わっても、同じデータに同じSQLを実行すれば、同じ数字が出ます。
前月比を載せている月次資料そのものを見直すときは、次の記事の手順が使えます。
まとめ
SQLで前月比・前年比を出すときは、次の5つを確認します。
- 月別の表が1か月1行で、月の桁がそろっているか
- 前の行の月も出して、抜けた月がないか
- 店舗別に比べるなら、PARTITION BYで分けたか
- 対象月への絞り込みは、LAGを計算した後か
- 100.0を掛けてから割り、NULLIFで0を除いたか
前年比はLAG(sales, 12)で出せますが、月がそろっていることが前提です。


-160x90.png)


