SQLで前月比・前年比を出す方法|毎月の手作業の比較をLAG関数1本にする

月別の売上表にLAG関数で前月の売上を並べ、前月比を計算する流れと、月が抜けていると前々月と比べてしまう落とし穴を表したイメージ 実務で使うデータ・AIスキル
前月比は、LAG関数1本で出す 補足: 月の抜け・店舗の混ざりに注意

毎月、先月の資料を開いて前月の売上を書き写し、電卓で前月比を出している。前年比も出したいが、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)

ymsales
2026-07400
2026-08500
2026-09450

ym は「2026-07」のように、月を2桁にそろえた文字列です。桁がそろっていれば、文字の並び順が月の順番と一致します。

まず、前月の売上を横に並べます。

SELECT
  ym,
  sales,
  LAG(sales) OVER (ORDER BY ym) AS prev_sales
FROM monthly_sales;

実行結果

ymsalesprev_sales
2026-07400NULL
2026-08500400
2026-09450500

なぜこうなるか: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;

実行結果

ymsalesmom_pct
2026-07400NULL
2026-08500125.0
2026-0945090.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月の行がない表を見てみます。

ymsales
2026-07400
2026-09450
2026-10480

確認方法:前の行の月を、一緒に出します。

SELECT
  ym,
  LAG(ym) OVER (ORDER BY ym) AS prev_ym,
  LAG(sales) OVER (ORDER BY ym) AS prev_sales
FROM monthly_sales;
ymprev_ymprev_sales
2026-07NULLNULL
2026-092026-07400
2026-102026-09450

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にします。取り込み漏れだった場合は、元データを直してから集計し直します。

ymsalesprev_sales
2026-07400NULL
2026-080400
2026-094500
2026-10480450

9月の前の行が、8月になりました。このまま前月比のSQLに入れると、9月は前月が0なので、NULLIFによりNULLになります。比べられないことが、そのまま見える形になります。

月の一覧は、Excelで作った月の列を取り込むなどして用意します。PostgreSQLなら、generate_series関数で日付の連番を作ることもできます。

2. 店舗の順に並べれば、店舗ごとの前月と比べられると思う

問題と原因:店舗別の前月比を1つのSQLで出すとします。ORDER BYで店舗順に並べても、店舗の境目はLAGに伝わりません。次の店舗の最初の月が、前の店舗の最後の月と比べられます。

架空の店舗別の表(store_sales)です。

storeymsales
A2026-08500
A2026-09450
B2026-08300
SELECT
  store,
  ym,
  LAG(sales) OVER (ORDER BY store, ym) AS prev_sales
FROM store_sales;
storeymprev_sales
A2026-08NULL
A2026-09500
B2026-08450

店舗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';
ymsalesprev_sales
2026-09450NULL

期間で絞った場合も、絞った最初の月の前月比が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';
ymsalesprev_sales
2026-09450500

それでも合わないとき:LAGの前に、月別の集計表そのものを確かめる

LAGの書き方が正しくても、元の月別の集計がずれていれば、前月比もずれます。

次の順に確かめます。

  1. 月の合計が、既存の月次レポートと一致するか
  2. 月を区切る日付が、資料と同じ列か
  3. 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か月1行で、月の桁がそろっているか
  2. 前の行の月も出して、抜けた月がないか
  3. 店舗別に比べるなら、PARTITION BYで分けたか
  4. 対象月への絞り込みは、LAGを計算した後か
  5. 100.0を掛けてから割り、NULLIFで0を除いたか

前年比はLAG(sales, 12)で出せますが、月がそろっていることが前提です。

次に読む記事

タイトルとURLをコピーしました