本文へ移動
売上・LTV分析SQL 更新日: 2026年5月2日 約19分で読めます

前年同月比・前月比をSQLで計算する方法|売上推移を同時比較するクエリ


「先月より増えた」は本当に良いことか

月次レポートで「今月の売上は先月より10%増えました」という報告を見たとき、素直に喜んでいいのだろうか。

実は通販業界には強い季節性がある。年末に向けてギフト需要が高まり、1月は反動で落ちる。夏に日焼け止めが売れ、冬に保湿クリームが売れる。前月比だけを見ていると、この季節変動を「施策の成果」と誤認してしまう。

だから売上分析では前月比前年同月比の2つを常にセットで見るのが鉄則だ。前月比は「今月の動き」を、前年同月比は「季節変動を排除した成長率」を教えてくれる。

この記事では、LAG関数を使って前月・前年同月の数値を同じ行に並べ、変化率まで一発で計算するクエリを作る。月次レポートの定番クエリとして、そのまま使い回せる内容にした。


使用するテーブル

今回は orders テーブルを使う。

-- orders テーブル
-- order_id     : 注文ID
-- customer_id  : 顧客ID
-- order_date   : 注文日(DATE型)
-- total_amount : 注文金額
-- status       : 'completed' / 'cancelled' など

STEP 1 ― 月次売上を集計する

まず月ごとの売上・注文件数・購買顧客数を集計する。これが比較の土台になる。

-- STEP1: 月次売上の集計

WITH monthly_sales AS (
    SELECT
        DATE_TRUNC('month', order_date)      AS sales_month,
        COUNT(DISTINCT order_id)             AS order_count,
        COUNT(DISTINCT customer_id)          AS customer_count,
        SUM(total_amount)                    AS total_sales,
        ROUND(AVG(total_amount), 0)          AS avg_order_value
    FROM orders
    WHERE status = 'completed'
    GROUP BY DATE_TRUNC('month', order_date)
)
SELECT *
FROM monthly_sales
ORDER BY sales_month;

出力イメージ

sales_monthorder_countcustomer_counttotal_salesavg_order_value
2023-01-011,2411,08918,420,00014,843
2023-02-011,1981,05217,280,00014,424
2024-01-011,3891,21421,340,00015,364
2024-02-011,3121,14819,860,00015,137

月ごとに1行のデータが揃った。次はこの「縦に並んだ時系列」を横に展開して、前月・前年同月を同じ行に持ってくる。


STEP 2 ― LAG関数で前月・前年同月の値を取得する

LAG関数は「現在の行から N 行前の値を取得する」ウィンドウ関数だ。

  • 前月の値:LAG(col, 1) OVER (ORDER BY sales_month) ← 1行前
  • 前年同月の値:LAG(col, 12) OVER (ORDER BY sales_month) ← 12行前(月次データなので12ヶ月前)
-- STEP2: LAGで前月・前年同月の値を同じ行に並べる

WITH monthly_sales AS (
    SELECT
        DATE_TRUNC('month', order_date)      AS sales_month,
        COUNT(DISTINCT order_id)             AS order_count,
        COUNT(DISTINCT customer_id)          AS customer_count,
        SUM(total_amount)                    AS total_sales,
        ROUND(AVG(total_amount), 0)          AS avg_order_value
    FROM orders
    WHERE status = 'completed'
    GROUP BY DATE_TRUNC('month', order_date)
),
with_lag AS (
    SELECT
        sales_month,
        order_count,
        customer_count,
        total_sales,
        avg_order_value,

        -- 前月の値
        LAG(total_sales,    1) OVER (ORDER BY sales_month)  AS prev_month_sales,
        LAG(order_count,    1) OVER (ORDER BY sales_month)  AS prev_month_orders,
        LAG(customer_count, 1) OVER (ORDER BY sales_month)  AS prev_month_customers,

        -- 前年同月の値(12行前)
        LAG(total_sales,    12) OVER (ORDER BY sales_month) AS prev_year_sales,
        LAG(order_count,    12) OVER (ORDER BY sales_month) AS prev_year_orders,
        LAG(customer_count, 12) OVER (ORDER BY sales_month) AS prev_year_customers
    FROM monthly_sales
)
SELECT *
FROM with_lag
ORDER BY sales_month;

出力イメージ(一部)

sales_monthtotal_salesprev_month_salesprev_year_sales
2023-01-0118,420,000NULLNULL
2023-02-0117,280,00018,420,000NULL
2024-01-0121,340,00019,120,00018,420,000
2024-02-0119,860,00021,340,00017,280,000

2024年1月には「前月(2023年12月)の売上」と「前年同月(2023年1月)の売上」が同じ行に並んでいる。LAGの行数(1行前・12行前)がデータの月数に依存するため、データが12ヶ月以上揃っている必要がある点に注意しよう。


STEP 3 ― 変化率を計算する(完成版)

前月・前年同月の売上が揃ったので、変化率(増減率)を計算して完成させる。

ゼロ除算を防ぐため、分母には NULLIF を使う。これはLTV編でも登場したテクニックだ。

-- STEP3: 前月比・前年同月比まで含めた完成版クエリ

WITH monthly_sales AS (
    SELECT
        DATE_TRUNC('month', order_date)      AS sales_month,
        COUNT(DISTINCT order_id)             AS order_count,
        COUNT(DISTINCT customer_id)          AS customer_count,
        SUM(total_amount)                    AS total_sales,
        ROUND(AVG(total_amount), 0)          AS avg_order_value
    FROM orders
    WHERE status = 'completed'
    GROUP BY DATE_TRUNC('month', order_date)
),
with_lag AS (
    SELECT
        sales_month,
        order_count,
        customer_count,
        total_sales,
        avg_order_value,
        LAG(total_sales,    1)  OVER (ORDER BY sales_month)  AS prev_month_sales,
        LAG(order_count,    1)  OVER (ORDER BY sales_month)  AS prev_month_orders,
        LAG(customer_count, 1)  OVER (ORDER BY sales_month)  AS prev_month_customers,
        LAG(total_sales,    12) OVER (ORDER BY sales_month)  AS prev_year_sales,
        LAG(order_count,    12) OVER (ORDER BY sales_month)  AS prev_year_orders,
        LAG(customer_count, 12) OVER (ORDER BY sales_month)  AS prev_year_customers
    FROM monthly_sales
)
SELECT
    sales_month,

    -- 今月の実績
    total_sales,
    order_count,
    customer_count,
    avg_order_value,

    -- 前月比(売上)
    prev_month_sales,
    ROUND(
        (total_sales - prev_month_sales) * 100.0
        / NULLIF(prev_month_sales, 0)
    , 1)  AS mom_sales_growth_pct,        -- Month over Month

    -- 前年同月比(売上)
    prev_year_sales,
    ROUND(
        (total_sales - prev_year_sales) * 100.0
        / NULLIF(prev_year_sales, 0)
    , 1)  AS yoy_sales_growth_pct,        -- Year over Year

    -- 前月比(注文件数)
    ROUND(
        (order_count - prev_month_orders) * 100.0
        / NULLIF(prev_month_orders, 0)
    , 1)  AS mom_order_growth_pct,

    -- 前年同月比(注文件数)
    ROUND(
        (order_count - prev_year_orders) * 100.0
        / NULLIF(prev_year_orders, 0)
    , 1)  AS yoy_order_growth_pct,

    -- 前月比(購買顧客数)
    ROUND(
        (customer_count - prev_month_customers) * 100.0
        / NULLIF(prev_month_customers, 0)
    , 1)  AS mom_customer_growth_pct,

    -- 前年同月比(購買顧客数)
    ROUND(
        (customer_count - prev_year_customers) * 100.0
        / NULLIF(prev_year_customers, 0)
    , 1)  AS yoy_customer_growth_pct

FROM with_lag
WHERE sales_month >= DATE '2024-01-01'  -- 表示したい期間を絞る
ORDER BY sales_month;

完成した出力イメージ

sales_monthtotal_salesmom_sales_growth_pctyoy_sales_growth_pctmom_order_growth_pctyoy_order_growth_pct
2024-01-0121,340,000+11.6+15.8+9.4+12.0
2024-02-0119,860,000-7.0+15.0-6.1+9.6
2024-03-0122,180,000+11.7+14.2+10.3+11.8
2024-04-0120,940,000-5.6+12.9-4.8+10.4

2月の前月比はマイナス7.0%と落ち込んでいるが、前年同月比はプラス15.0%だ。「今月は先月より下がった」と焦る必要はない。2月は例年そういう月なのかもしれず、むしろ前年より15%成長しているなら健全な状態だと判断できる。


応用:商品カテゴリ別に前年同月比を出す

月次全体の比較だけでなく、カテゴリ別に前年同月比を並べると「成長しているカテゴリ」と「停滞しているカテゴリ」が一目で分かる。

PARTITION BY を追加するだけで、カテゴリを分けたままLAGを適用できる。

-- カテゴリ別 前年同月比

WITH monthly_category_sales AS (
    SELECT
        DATE_TRUNC('month', o.order_date)    AS sales_month,
        oi.category,
        SUM(oi.quantity * oi.unit_price)     AS total_sales
    FROM orders       o
    JOIN order_items  oi  ON o.order_id = oi.order_id
    WHERE o.status = 'completed'
    GROUP BY
        DATE_TRUNC('month', o.order_date),
        oi.category
),
with_yoy AS (
    SELECT
        sales_month,
        category,
        total_sales,
        -- PARTITION BY category で、カテゴリ内の12行前を取得
        LAG(total_sales, 12) OVER (
            PARTITION BY category
            ORDER BY sales_month
        )  AS prev_year_sales
    FROM monthly_category_sales
)
SELECT
    sales_month,
    category,
    total_sales,
    prev_year_sales,
    ROUND(
        (total_sales - prev_year_sales) * 100.0
        / NULLIF(prev_year_sales, 0)
    , 1)  AS yoy_growth_pct
FROM with_yoy
WHERE
    sales_month >= DATE '2024-01-01'
    AND prev_year_sales IS NOT NULL
ORDER BY sales_month, yoy_growth_pct DESC;

出力イメージ

sales_monthcategorytotal_salesprev_year_salesyoy_growth_pct
2024-01-01スキンケア8,420,0006,980,000+20.6
2024-01-01ヘアケア6,240,0005,510,000+13.2
2024-01-01ボディケア4,180,0003,920,000+6.6
2024-01-01メイク2,500,0002,610,000-4.2

1月はスキンケアが前年比+20.6%と好調だが、メイクカテゴリは-4.2%と前年割れしている。全体の売上が成長していても、このカテゴリ別の凸凹が見えないと打ち手が定まらない。


実務での運用ヒント

① 変化率を「絶対額の差」も合わせて出す

変化率だけでは規模感が伝わらない。前年比+30%でも、絶対額の差が100万円と1億円では意味が全く違う。total_sales - prev_year_sales AS yoy_sales_diff を追加して両方出しておこう。

② データ欠損月(売上ゼロの月)の扱い

キャンペーン停止などで特定の月に売上がゼロになると、その月がCTEのGROUP BYで生成されない。すると LAG の「12行前」がズレて正しくない値を拾ってしまう。対策として、カレンダーテーブル(全月のマスタ)とLEFT JOINして売上がない月にも0を補完する方法がある。

-- カレンダーCTEで欠損月を補完するイメージ
WITH calendar AS (
    SELECT DATE_TRUNC('month', dt) AS sales_month
    FROM UNNEST(SEQUENCE(DATE '2023-01-01', DATE '2024-12-01', INTERVAL '1' MONTH)) AS t(dt)
)
SELECT
    c.sales_month,
    COALESCE(m.total_sales, 0)  AS total_sales
FROM calendar          c
LEFT JOIN monthly_sales  m  ON c.sales_month = m.sales_month

③ LAGは「行数」で数えることを常に意識する

LAG(col, 12) は「12行前の値」であり「12ヶ月前の値」ではない。月次データに1行ずつデータが揃っていれば一致するが、データが欠損していたり週次・日次データを集計している場合はズレが生じる。日次データから前年同日比を出したい場合は LAG(col, 365) ではなく、DATE_ADD('year', -1, date) を使って明示的に日付で対応する方法が安全だ。


まとめ

今回のクエリの骨格を振り返る。

  1. GROUP BY DATE_TRUNC('month', ...)月次集計を作る
  2. LAG(col, 1) OVER (ORDER BY sales_month)前月の値を同じ行に引っ張る
  3. LAG(col, 12) OVER (ORDER BY sales_month)前年同月の値を引っ張る
  4. (今月 - 前月) / NULLIF(前月, 0)変化率を計算する
  5. カテゴリ別比較には PARTITION BY を加えるだけで対応できる

前月比と前年同月比を常にセットで見る習慣を持つだけで、売上の読み方が格段に深くなる。「季節変動に乗っているだけか」「本当に成長しているか」を数字で判断できることが、データドリブンなマーケターとそうでない人の分かれ目だ。


次のアクション

SQLやデータ活用を、手元で試しながら理解する

記事で読んだ考え方を、SQL練習場や関連カテゴリの記事でさらに深掘りできます。相談やご依頼もお問い合わせページから受け付けています。

SQL練習場で試す お問い合わせ

この記事を書いた人:martechfarm

Treasure Data Top Lapidarist Award受賞。

SQL / Digdag / Python / CDP設計 / CRM設計を横断し、企業のデータ活用を支援。

実績・支援内容を見る →

MarTech Farmをもっと見る

今すぐ購読し、続きを読んで、すべてのアーカイブにアクセスしましょう。

続きを読む