BI探検ノート

SQL で、月別の累計と前年同月比を出す(ウィンドウ関数と、月が欠けたときの落とし穴)

約5分

濵口 翔長崎で、Power BI のつまずきを記録しています
目次
  1. 使ったデータ
  2. 累計
  3. 年内累計(年ごとにリセット)
  4. 前年同月比(LAG)
  5. 落とし穴: 月が欠けると、ずれる
  6. 結合で前年同月を取る
  7. 使い分け
  8. この記事で確認できたこと・できていないこと

確認日2026年10月21日。PostgreSQL 16.13(Docker のコンテナ)で、疑似データを使って、実際に動かして確かめました。疑似データです。実在の店舗や売上ではありません。 確認できていないことは、末尾にまとめました。

月別の売上から、累計と前年同月比を SQL で出します。累計は SUM(...) OVER、前年同月比は LAG(..., 12) で出ました。ただし、月が1つ欠けていると、LAG は別の月と比べてしまいました。

結論(30秒版)

  • 累計は、SUM(amt) OVER (ORDER BY month) で出ました。年ごとにリセットする年内累計は、PARTITION BY に年を入れます。
  • 前年同月比は、LAG(amt, 12) OVER (ORDER BY month) で出ました。
  • 途中の月が欠けると、LAG(12) は、12行前の別の月を返しました。 月が欠けうるデータでは、結合で前年同月を取る方が安全でした。

使ったデータ

前の記事と同じ疑似データです。売上が2,193行(2024年1月〜2025年12月、3店舗の毎日分)。まず、月別に集計して、m という名前にします。

WITH m AS (
  SELECT date_trunc('month', sale_date)::date AS month, SUM(amount) AS amt
  FROM sales GROUP BY 1
)

累計

SELECT month, amt, SUM(amt) OVER (ORDER BY month) AS running_total
FROM m ORDER BY month;

2024年12月から2025年2月は、次のようになりました。年をまたいでも、足し続けます。

monthamtrunning_total
2024-12-011352821588842
2025-01-011515121740354
2025-02-011379281878282

OVER (ORDER BY month) が、「月の順に並べて、先頭から今の行までを足す」という指定です。

年内累計(年ごとにリセット)

SELECT month, amt,
       SUM(amt) OVER (PARTITION BY date_trunc('year', month) ORDER BY month) AS ytd
FROM m ORDER BY month;
monthamtytd
2024-12-011352821588842
2025-01-01151512151512
2025-02-01137928289440

2025年1月で、累計が 151,512 に戻りました。PARTITION BY が、「年ごとに区切って、それぞれで累計を出す」という指定です。

前年同月比(LAG)

SELECT month, amt,
       LAG(amt, 12) OVER (ORDER BY month) AS prev_year,
       ROUND(100.0 * amt / LAG(amt, 12) OVER (ORDER BY month), 1) AS pct_of_prev
FROM m ORDER BY month;

2025年1月から3月は、次のようになりました。

monthamtprev_yearpct_of_prev
2025-01-01151512132912114.0
2025-02-01137928125748109.7
2025-03-01152550133632114.2

LAG(amt, 12) は、「12行前の値」です。月が1行ずつ並んでいるので、12行前は、前年の同じ月になります。

落とし穴: 月が欠けると、ずれる

2024年6月の売上が1行もなかったとします(店が休業した、データが入っていない、など)。そのとき、月別の集計 m には、2024年6月の行自体が、ありません。

実際に、2024年6月を除いて、LAG(12) を動かしました。

monthamtprev_by_laglag_month
2025-06-011503901286342024-05-01
2025-07-011524541335362024-07-01

2025年6月の前年として、2024年5月の値が出ました。エラーにも、空白にもなりません。もっともらしい数字が、違う月のものとして出るので、気づきにくい失敗です。2025年7月からは、行の並びが戻るので、正しい月と比べていました。

結合で前年同月を取る

月の行があるかどうかに頼らず、「1年前の月」を日付で指定して、自分自身に結合します。

SELECT c.month, c.amt, p.amt AS prev_year
FROM m c
LEFT JOIN m p ON p.month = (c.month - interval '1 year')::date
ORDER BY c.month;

同じ条件(2024年6月なし)で動かすと、次のようになりました。

monthamtprev_year
2025-06-01150390(NULL)
2025-07-01152454133536

前年のデータがない2025年6月は、NULL になりました。別の月の値が入るより、空白の方が、間違いに気づけます。 ここで LEFT JOIN を使う理由は、前の記事と同じです。JOIN にすると、前年のない月の行が消えます。

使い分け

  • 月が、必ずすべて揃っている(カレンダーテーブルがある、など)なら、LAG(12) は、短く書けて分かりやすいです。
  • 月が欠けるかもしれないなら、結合で取る方が安全です。

Power BI の DAX で同じことをする場合は、累計と年初来累計と、前年比(SAMEPERIODLASTYEAR)に書いています。この記事では、SQL の動きだけを確かめました。

この記事で確認できたこと・できていないこと

確認日: 2026年10月21日。PostgreSQL 16.13(Docker、Alpine ベース)。疑似データ(売上2,193行)。

確認できたこと

  • SUM(...) OVER (ORDER BY ...) で、年をまたぐ累計が出ること
  • PARTITION BY に年を入れると、年ごとにリセットされること
  • LAG(..., 12) で、前年同月の値と比が出ること
  • 月が1つ欠けると、LAG(12) は、その月の前後で、別の月の値を返すこと(2025年6月に、2024年5月の値)
  • 結合で取ると、前年のない月は NULL になること

確認できていないこと

  • 日次や週次のデータでの前年比(うるう年の2月29日の扱いなど)
  • PostgreSQL 以外のデータベースでの書き方の違い
  • 大きなデータでの速さ
  • Power BI が発行する SQL との対応
  • 公式ドキュメントとの照合