SQL 統計分析 — CORR・線形回帰・NTILEの基礎

基礎統計分析CORR / 線形回帰RANK / NTILELAGPostgreSQL対応全5問
1 / 5 · 学習中 0 / 5 問完了
QUESTION 6

相関分析 — CORR でピアソン相関係数を算出し変数間の線形関係を定量化する

CORR相関分析ピアソン係数線形関係
前提知識

CORR(Y, X) は2変数間のピアソン積率相関係数を返す集計関数です。値の範囲は −1 〜 +1 で、+1 に近いほど強い正の線形相関、−1 に近いほど強い負の線形相関、0 は線形相関なしを意味します。

-- 書式: CORR(Y, X)  ← 相関は対称なので引数の順序は結果に影響しない
CORR(revenue, ad_cost)

-- 計算式(ピアソン積率相関係数):
--   r = Σ(xi−x̄)(yi−ȳ) / √[ Σ(xi−x̄)² × Σ(yi−ȳ)² ]
--   分子: 共分散の和(正→正の相関, 負→負の相関)
--   分母: 両変数の標準偏差の積(スケール正規化 → −1〜+1 に収まる)
集計関数として GROUP BY と組み合わせ可能:CORR は通常の集計関数なので GROUP BY category でカテゴリ別の相関を一括算出できます。一方 OVER 句(ウィンドウ関数としての使用)は非対応です。また CORR は double precision を返すため、ROUND と組み合わせる際は ::numeric キャストが必要です。
問題

ad_spend テーブルを使い、広告費(ad_cost)と売上(revenue)のピアソン相関係数計算対象の行数(n)を1行で算出してください。取得列は corr_coef(小数第4位まで), n

使用テーブル
▸ ad_spend(6行)
weekad_costrevenue
1100280
2200650
3300780
44001100
55001250
66001640
期待出力
corr_coefn
0.99146
模範解答コード
SELECT
  ROUND(CORR(revenue, ad_cost)::numeric, 4)  AS corr_coef,  -- ピアソン相関係数(−1〜+1)
  COUNT(*)                                    AS n          -- 計算対象の行数
FROM ad_spend;

/*
  実行順序(SQLの論理的な評価順):
  1. FROM ad_spend           → 6行読込
  2. CORR(revenue, ad_cost)  → 相関係数を計算
  3. COUNT(*)                → 件数を集計
  4. ROUND(...::numeric, 4)  → 小数4桁に丸め
  5. SELECT                  → 1行出力
  */
解説(テーブル変化・ポイント)
SELECT ROUND(CORR(revenue, ad_cost)::numeric, 4) AS corr_coef, COUNT(*) AS n FROM ad_spend;
LEGEND
データ取得・読込対象
① FROM ad_spend — 広告費と売上の週次データ6行
FROM ad_spendad_spend テーブルから全6行を読み込みます。ad_cost(説明変数 X)と revenue(目的変数 Y)の線形関係を CORR 関数で定量化します。NULL を含む行は自動除外されます。
1 / 4
week▸ ad_cost▸ revenue
1100280
2200650
3300780
44001100
55001250
66001640
ad_spend: 6行(全行がCORR計算の対象)
学習ポイント
相関係数の解釈基準(目安):|r| ≥ 0.9「非常に強い」、0.7〜0.9「強い」、0.4〜0.7「中程度」、0.4未満「弱い」。本問の 0.9914 は散布図でほぼ直線状となり、ad_cost の変動が revenue の変動の約 98% を説明します(R² = 0.9914² ≈ 0.98)。
CORR と NULL 処理:CORR(Y, X) は X・Y のどちらか一方でも NULL の行を自動除外します。COUNT(*) と REGR_COUNT(Y, X) の差を確認することで、NULL による有効サンプル減少を検出できます。
GROUP BY でカテゴリ別の相関を一括算出:CORR(revenue, ad_cost)GROUP BY channel を加えると、SNS・TV・検索広告ごとの相関係数を1クエリで比較できます。相関係数がチャネルで大きく異なる場合は広告の効き方がジャンルに依存しているというインサイトになります。
アンチパターン
「相関あり = 因果関係あり」と結論付ける:CORR が高くても因果関係は保証されません。気温という交絡変数が「アイスの売上と溺死者数の相関」を生む古典例が示すように、相関はあくまで傾向の定量化です。因果推論には介入研究・傾向スコア分析などの追加手法が必要です。
散布図を確認せず CORR だけ信頼する:アンスコムの四重奏のように、まったく異なる分布形状でも CORR が同じ値になるケースがあります。外れ値・曲線関係・層別効果を見落とさないよう、CORR 算出後は必ず散布図で形状を目視確認してください。
実務コラム:マーケティング分析での相関活用
広告費と売上の相関分析は「どのチャネルが効いているか」を素早く仮説化する第一歩です。CORR > 0.7 のチャネルは予算増額候補、CORR ≈ 0 は配分見直しのサインです。実務では LAG(ad_cost,1) OVER (ORDER BY week) を使って1週間ラグの相関も算出し、広告の翌週への遅延効果も検出します。
QUESTION 7

線形回帰 — REGR_SLOPE / REGR_INTERCEPT / REGR_R2 で回帰モデルを構築し売上を予測する

REGR_SLOPEREGR_INTERCEPT線形回帰R² / 予測
前提知識

PostgreSQL の REGR_* 関数群は、最小二乗法による単回帰分析をSQLで完結させます。目的変数 Y と説明変数 X の関係を Y = slope × X + intercept でモデル化します。

REGR_SLOPE(Y, X)      -- 傾き: Σ(xi−x̄)(yi−ȳ) / Σ(xi−x̄)²
REGR_INTERCEPT(Y, X)  -- Y切片: ȳ − slope × x̄
REGR_R2(Y, X)         -- 決定係数 R²: CORR(Y,X)² — 0〜1でモデルの当てはまりを示す
REGR_COUNT(Y, X)      -- NULL を除いた有効サンプル数

-- 予測値の算出(CTE の slope・intercept を再利用):
(slope * 700 + intercept)::numeric
CTE で中間結果を再利用:REGR_SLOPE と REGR_INTERCEPT を同じ SELECT で繰り返し書くと計算が重複します。WITH 句(CTE)で統計量を先に確定させ、外側クエリで slope × X + intercept を計算するパターンが実務標準です。
問題

同じ ad_spend テーブルを使い、傾き(slope)・切片(intercept)・決定係数(r_squared)と、ad_cost = 700 のときの予測売上(predicted_700)を1行で算出してください。slope は小数第4位、intercept は小数第2位、r_squared は小数第4位、predicted_700 は整数丸め。

使用テーブル
▸ ad_spend(6行)
weekad_costrevenue
1100280
2200650
3300780
44001100
55001250
66001640
期待出力
slopeinterceptr_squaredpredicted_700
2.548658.000.98291842
模範解答コード
WITH stats AS (
  -- REGR_* 関数で回帰パラメータを一括算出(CTE で slope・intercept を再利用)
  SELECT
    REGR_SLOPE(revenue, ad_cost)     AS slope,
    REGR_INTERCEPT(revenue, ad_cost) AS intercept,
    REGR_R2(revenue, ad_cost)        AS r_squared
  FROM ad_spend
)
SELECT
  ROUND(slope::numeric,                     4) AS slope,
  ROUND(intercept::numeric,                 2) AS intercept,
  ROUND(r_squared::numeric,                 4) AS r_squared,
  ROUND((slope * 700 + intercept)::numeric, 0) AS predicted_700  -- 回帰式でad_cost=700の予測値
FROM stats;

/*
  実行順序(SQLの論理的な評価順):
  1. CTE stats — FROM ad_spend  → 6行読込
  2. REGR_SLOPE(...)            → 回帰の傾きを計算
  3. REGR_INTERCEPT(...)        → 切片を計算
  4. REGR_R2(...)               → 決定係数を計算
  5. FROM stats                 → CTE の1行を参照
  6. predicted_700              → 予測値を計算
  7. 各 ROUND 適用                 → 丸め
  8. SELECT                     → 1行出力
  */
解説(テーブル変化・ポイント)
WITH stats AS ( SELECT REGR_SLOPE(revenue, ad_cost) AS slope, REGR_INTERCEPT(revenue, ad_cost) AS intercept, REGR_R2(revenue, ad_cost) AS r_squared FROM ad_spend ) SELECT ROUND(slope::numeric, 4) AS slope, ROUND(intercept::numeric, 2) AS intercept, ROUND(r_squared::numeric, 4) AS r_squared, ROUND((slope * 700 + intercept)::numeric, 0) AS predicted_700 FROM stats;
LEGEND
データ取得・読込対象
① FROM ad_spend — 同じデータで回帰モデルを構築
FROM ad_spendQ6で CORR=0.9914 という強い正の相関を確認済み。このデータで Y=slope×X+intercept の回帰式を構築し、広告費から売上を予測するモデルを作ります。x̄=350, ȳ=950。
1 / 5
week▸ ad_cost (X)▸ revenue (Y)
1100280
2200650
3300780
44001100
55001250
66001640
ad_spend: 6行(x̄=350, ȳ=950)
学習ポイント
slope と intercept のビジネス解釈:slope=2.5486 は「広告費を100円増やすと売上が約255円増加する」を意味します。intercept=58.00 は「広告費ゼロでも理論上58円の売上がある」という仮想基準値(外挿なので実際には意味をなさない場合あり)です。
R²(決定係数)の読み方:R²=0.9829 は「モデルが目的変数の分散の98.29%を説明できる」を意味します。残り1.71%は季節性・競合・天候など説明変数で捉えられない誤差です。R²が1.0に近すぎる場合は過学習を疑い、学習データ外での検証が必要です。
REGR_COUNT で NULL の影響を確認:REGR_COUNT(Y, X) は Y・X どちらかが NULL の行を除いた有効件数を返します。COUNT(*) と REGR_COUNT の差が大きい場合は NULL が多く、モデルの信頼性が低下している可能性があります。
アンチパターン
学習データの範囲外に外挿する:本問の ad_cost=700 は最大値 600 を超える外挿です。線形回帰モデルは学習データ範囲内でのみ信頼できます。外挿予測は参考値として扱い、追加データ収集後に再検証してください。
単回帰を多変量問題に適用する:REGR_SLOPE(Y,X) は説明変数が1つの単回帰専用です。ad_cost + season + competitor など複数の説明変数がある場合は単回帰では不十分で、重回帰には専用ツール(Python scikit-learn 等)との連携が必要です。
実務コラム:SQLによる売上予測の自動化
REGR_SLOPE / REGR_INTERCEPT を CTE で算出し、外側クエリで future_ad_spend テーブルと CROSS JOIN すると、複数の広告費シナリオの予測売上を一括試算できます。予算500/700/900円の3シナリオを1クエリで返すことで、企画書への数値提供が自動化されます。さらに GROUP BY quarter で四半期別モデルを並列構築すると、季節性を持つカテゴリで「夏モデル」「冬モデル」を使い分ける高度な予測も実現できます。
QUESTION 8

ランキング分析 — RANK / DENSE_RANK / ROW_NUMBER でタイ(同点)処理の違いを理解する

RANKDENSE_RANKROW_NUMBERランキングPARTITION BYタイ処理
前提知識

ウィンドウ関数によるランキングには3種類あり、同点(タイ)のときの挙動が異なります。PARTITION BY でグループごとに独立したランキングを付与できます。

RANK()        OVER (PARTITION BY region ORDER BY sales DESC)
-- タイ行に同じ順位 / 次の順位をスキップ: 1,1,3(2をスキップ)

DENSE_RANK() OVER (PARTITION BY region ORDER BY sales DESC)
-- タイ行に同じ順位 / 次の順位をスキップしない: 1,1,2(連続)

ROW_NUMBER() OVER (PARTITION BY region ORDER BY sales DESC)
-- 常に連番 / タイ行でも異なる番号(タイ時の順序は非決定的): 1,2,3
いつどれを使うか:公平な順位(穴あり可)→ RANK。連続順位でトップN抽出 → DENSE_RANK。行ごとに一意番号(重複排除・ページネーション)→ ROW_NUMBER(タイブレーカー追加推奨)。
問題

sales_rep テーブルを使い、地域(region)ごとに売上(sales_amount)の降順ランキングを RANK・DENSE_RANK・ROW_NUMBER の3種類で付与してください。取得列は rep_id, region, sales_amount, rank_val, dense_rank_val, row_numregion, rank_val, rep_id の順で並べてください。

使用テーブル
▸ sales_rep(9行)
rep_idregionsales_amount
1East8500
2East6200
3East8500
4West9100
5West7800
6West5500
7North7200
8North7200
9North4300
期待出力
rep_idregionsales_amountrank_valdense_rank_valrow_num
1East8500111
3East8500112
2East6200323
7North7200111
8North7200112
9North4300323
4West9100111
5West7800222
6West5500333
模範解答コード
SELECT
  rep_id,
  region,
  sales_amount,
  RANK()        OVER (PARTITION BY region ORDER BY sales_amount DESC) AS rank_val,
  DENSE_RANK() OVER (PARTITION BY region ORDER BY sales_amount DESC) AS dense_rank_val,
  ROW_NUMBER() OVER (PARTITION BY region ORDER BY sales_amount DESC, rep_id ASC) AS row_num  -- タイは rep_id で安定化
FROM   sales_rep
ORDER BY region, rank_val, rep_id;

/*
  実行順序(SQLの論理的な評価順):
  1. FROM sales_rep                     → 9行読込
  2. PARTITION BY region                → 3グループに分割
  3. ORDER BY sales_amount DESC         → 各グループ内ソート
  4. RANK()                             → タイは同順位・次をスキップ
  5. DENSE_RANK()                       → タイは同順位・スキップなし
  6. ROW_NUMBER()                       → 常に連番
  7. ORDER BY region, rank_val, rep_id  → 最終ソート
  8. SELECT                             → 9行出力
  */
解説(テーブル変化・ポイント)
SELECT rep_id, region, sales_amount, RANK() OVER (PARTITION BY region ORDER BY sales_amount DESC) AS rank_val, DENSE_RANK() OVER (PARTITION BY region ORDER BY sales_amount DESC) AS dense_rank_val, ROW_NUMBER() OVER (PARTITION BY region ORDER BY sales_amount DESC, rep_id ASC) AS row_num FROM sales_rep ORDER BY region, rank_val, rep_id;
LEGEND
データ取得・読込対象
① FROM sales_rep — 3地域・9名の担当者別売上
FROM sales_repEast・West・North の3地域、各3名の担当者データを読み込みます。East(rep1,3: 8500同点)とNorth(rep7,8: 7200同点)にタイが存在し、3つのランキング関数の挙動の違いが明確に現れます。
1 / 4
rep_id▸ region▸ sales_amount
1East8500
2East6200
3East8500
4West9100
5West7800
6West5500
7North7200
8North7200
9North4300
sales_rep: 9行(East×3 / West×3 / North×3)
学習ポイント
3関数の使い分け早見表:「スポーツ公式順位表(同率で2位が欠番になる)」→ RANK。「上位N位全員取得(タイを含め全員)」→ DENSE_RANK(WHERE dense_rank_val <= N)。「ページネーション・重複排除・N件固定取得」→ ROW_NUMBER(タイブレーカー追加で非決定性を排除)。
ROW_NUMBER のタイ時非決定性:OVER 句の ORDER BY がタイを含む場合、ROW_NUMBER が割り当てる連番は実行のたびに変わる可能性があります。一意性を保証するには ORDER BY sales_amount DESC, rep_id ASC のようにタイブレーカーを追加してください。
PARTITION BY の有無で「ランキングの単位」が変わる:PARTITION BY を省略すると全行を1グループとして全社ランキングになります。地域別・カテゴリ別の独立したランキングが必要な場合は必ず PARTITION BY を明示し、ビジネスの「ランキングの単位」と合わせましょう。
アンチパターン
タイを無視して ROW_NUMBER で1位を抽出する:WHERE row_num = 1 でタイの行を絞ると1人しか取得されず同点者が漏れます。同率1位全員を取得する場合は WHERE rank_val = 1 を使ってください。
RANK で「上位N件固定」を取ろうとする:タイが多いデータで WHERE rank_val <= 3 にすると取得件数が予測しにくくなります(同率3位が複数なら3件超)。件数を N 件に確定したい場合は ROW_NUMBER(タイブレーカー付き)を使ってください。
実務コラム:営業ダッシュボードのランキング表示
月次売上ダッシュボードで「地域別トップ3」を表示する際、DENSE_RANK を使うと同率3位の担当者全員が表示されるため公平感のある画面になります。ROW_NUMBER なら常に3件固定でレイアウトが安定します。RANK() OVER (ORDER BY sales_amount DESC)(PARTITION BY なし)で全社ランキングを取りつつ、同一クエリで PARTITION BY region のランキングも並べれば「全社3位かつ地域1位」のような複合KPIも実現できます。
QUESTION 9

分位数分割 — NTILE でユーザーを購買額四分位にセグメント化する

NTILECTE分位数ユーザーセグメント四分位
前提知識

NTILE(n) は行を n 等分し、各行にバケット番号(1〜n)を割り当てるウィンドウ関数です。RFM 分析・ユーザーセグメント分類・パーセンタイルグループ化に活用されます。

NTILE(4) OVER (ORDER BY total_spent DESC)
-- 8行を4分割 → 各グループ2行ずつ
-- quartile=1: 購買額上位25%(VIP)
-- quartile=4: 購買額下位25%(ライト層)
-- 行数が n で割り切れない場合:
--   余り分を上位のバケットから1行ずつ追加(例: 9行÷4 → バケット1だけ3行)
CTE でバケット番号を先に確定:NTILE の結果を CASE WHEN で参照するには、同一 SELECT で NTILE 列の別名は再利用できません。CTE(またはサブクエリ)で quartile を先に確定させてから、外側クエリで CASE WHEN を適用するのが実務の標準パターンです。
問題

user_spend テーブルの total_spent を降順で4分割し、各ユーザーの分位番号(quartile: 1〜4)とセグメント名(segment: VIP / Heavy / Middle / Light)を付与してください。CTE でバケットを付与してから外側クエリでセグメント名を生成し、quartile, total_spent DESC の順で返してください。

使用テーブル
▸ user_spend(8行)
user_idtotal_spent
1250
21800
3420
43200
5890
65600
7150
82100
期待出力
user_idtotal_spentquartilesegment
656001VIP
432001VIP
821002Heavy
218002Heavy
58903Middle
34203Middle
12504Light
71504Light
模範解答コード
WITH segmented AS (
  -- NTILE(4) で降順購買額を4等分し、バケット番号(1=上位25%)を付与
  SELECT
    user_id,
    total_spent,
    NTILE(4) OVER (ORDER BY total_spent DESC) AS quartile
  FROM user_spend
)
SELECT
  user_id,
  total_spent,
  quartile,
  CASE quartile
    WHEN 1 THEN 'VIP'
    WHEN 2 THEN 'Heavy'
    WHEN 3 THEN 'Middle'
    WHEN 4 THEN 'Light'
  END AS segment
FROM   segmented
ORDER BY quartile, total_spent DESC;

/*
  実行順序(SQLの論理的な評価順):
  1. CTE segmented — FROM user_spend      → 8行読込
  2. NTILE(4) OVER (...)                  → 4分位に割り当て
  3. 外側SELECT FROM segmented              → CTE を参照
  4. CASE quartile                        → 番号をビジネス用語に変換
  5. ORDER BY quartile, total_spent DESC  → 最終ソート
  6. SELECT                               → 8行出力
  */
解説(テーブル変化・ポイント)
WITH segmented AS ( SELECT user_id, total_spent, NTILE(4) OVER (ORDER BY total_spent DESC) AS quartile FROM user_spend ) SELECT user_id, total_spent, quartile, CASE quartile WHEN 1 THEN 'VIP' WHEN 2 THEN 'Heavy' WHEN 3 THEN 'Middle' WHEN 4 THEN 'Light' END AS segment FROM segmented ORDER BY quartile, total_spent DESC;
LEGEND
データ取得・読込対象
① FROM user_spend — 8ユーザーの累計購買額(順序バラバラ)
FROM user_spend8ユーザーの total_spent を読み込みます。このまま順序はバラバラです。NTILE(4) で購買額の高い順に4等分し、VIP・Heavy・Middle・Light の4セグメントに分類します。
1 / 4
user_id▸ total_spent
1250
21800
3420
43200
5890
65600
7150
82100
user_spend: 8行(購買額はバラバラ)
学習ポイント
NTILE と WIDTH_BUCKET の使い分け:NTILE はデータを件数ベースで等分(分位数)するため、各バケットの購買額範囲は均等ではありません。一方 Q4 で学んだ WIDTH_BUCKET は値ベースで等幅に分割します。「上位 N% のユーザー」を切り出すなら NTILE、「0〜2,000円の層」のように金額帯で分けるなら WIDTH_BUCKET が適切です。
行数が n で割り切れない場合の動作:例えば9行を NTILE(4) で分割すると「9÷4=2余り1」となり、余り1行が上位バケット(quartile=1)に加算され3行になります。余りの行は常に下位バケットではなく上位から順に追加される点に注意してください。
PERCENTILE_CONT との組み合わせ:NTILE でセグメントを切った後、各セグメントの統計量を集計すると洞察が深まります。GROUP BY quartile で各層の AVG(total_spent) や COUNT を算出し、「VIP層の平均購買額はLight層の何倍か」を把握するのが実務の分析フローです。
アンチパターン
ORDER BY の昇順・降順を取り違える:NTILE(4) OVER (ORDER BY total_spent ASC) にすると、quartile=1 が最低購買額層(Light)になります。「quartile=1 = 上位層」にするには必ず DESC を指定してください。意図を明確にするためクエリのコメントに「1=VIP(上位25%)」と記載する習慣を持ちましょう。
NTILE のバケット番号を直接ユーザーに表示する:quartile=4 を見たユーザーが「4位」と勘違いする恐れがあります。CASE WHEN で必ず意味のあるラベルに変換してからレポートに使用してください。CTE でバケット番号を先に確定させるパターンがこの変換をクリーンに実現します。
実務コラム:RFM分析への拡張
NTILE は RFM(Recency・Frequency・Monetary)分析の基盤として活用できます。3指標ごとに NTILE(5) で1〜5 のスコアを付け、R+F+M の合計でユーザーグレードを決定するのが定番手法です。例えば NTILE(5) OVER (ORDER BY last_order_date DESC) AS r_score(最近注文した人ほどスコアが高い)のように3つのウィンドウ関数を並べ、CTE で集約します。このスコアをセグメント名に変換し、CRMツールや広告配信プラットフォームに連携するのが実務的なユーザーセグメント活用の完成形です。
QUESTION 10

前期比分析 — LAG / LEAD で月次成長率(MoM)を1クエリで算出する

LAGLEAD前期比時系列分析MoM成長率
前提知識

LAG(col, offset, default) は現在行より offset 行前の値を返すウィンドウ関数です。前月比・前年同期比などの時系列比較に不可欠で、自己 JOIN よりシンプルに記述できます。

LAG(col, 1)  OVER (ORDER BY month)  -- 1行前の値(前月)
LAG(col, 12) OVER (ORDER BY month)  -- 12行前の値(前年同月)
LEAD(col, 1) OVER (ORDER BY month)  -- 1行後の値(翌月)

-- 前月比(MoM)の計算:
-- (当月revenue − 前月revenue) / 前月revenue × 100
-- ※ 最初の行は前月がないため NULL になる(正常な動作)
CTE でLAGを先に確定:前月比の計算式に LAG(...) を2回書くと冗長です。CTE でまず prev_revenue = LAG(revenue, 1) OVER (...) を確定させ、外側クエリで (revenue - prev_revenue) / prev_revenue と書くパターンが可読性と保守性に優れます。
問題

monthly_revenue テーブルを使い、各月の売上・前月売上(prev_revenue)・前月比成長率(mom_growth_pct: 小数第2位)を算出してください。CTE でまず LAG による前月値を付与し、外側クエリで成長率を計算してください。初月は prev_revenue / mom_growth_pct ともに NULL で構いません。

使用テーブル
▸ monthly_revenue(6行)
monthrevenue
2024-011200000
2024-021350000
2024-031180000
2024-041520000
2024-051680000
2024-061450000
期待出力
monthrevenueprev_revenuemom_growth_pct
2024-011200000NULLNULL
2024-021350000120000012.50
2024-0311800001350000-12.59
2024-041520000118000028.81
2024-051680000152000010.53
2024-0614500001680000-13.69
模範解答コード
WITH lagged AS (
  -- LAG で前月売上を同じ行に並べる(CTE で先に確定させ外側で再利用)
  SELECT
    month,
    revenue,
    LAG(revenue, 1) OVER (ORDER BY month) AS prev_revenue
  FROM monthly_revenue
)
SELECT
  month,
  revenue,
  prev_revenue,
  ROUND(
    (revenue - prev_revenue)::numeric / prev_revenue * 100,
  2) AS mom_growth_pct  -- prev_revenue=NULL の場合も NULL が自動伝播
FROM   lagged
ORDER BY month;

/*
  実行順序(SQLの論理的な評価順):
  1. CTE lagged — FROM monthly_revenue  → 6行読込
  2. LAG(revenue, 1) OVER (...)         → 前月の revenue を付与
  3. FROM lagged                        → CTE を参照
  4. (revenue - prev) / prev * 100      → 前月比成長率を計算
  5. ROUND(..., 2)                      → 小数第2位で丸め
  6. ORDER BY month                     → 時系列昇順
  7. SELECT                             → 6行出力
  */
解説(テーブル変化・ポイント)
WITH lagged AS ( SELECT month, revenue, LAG(revenue, 1) OVER (ORDER BY month) AS prev_revenue FROM monthly_revenue ) SELECT month, revenue, prev_revenue, ROUND( (revenue - prev_revenue)::numeric / prev_revenue * 100, 2) AS mom_growth_pct FROM lagged ORDER BY month;
LEGEND
データ取得・読込対象
① FROM monthly_revenue — 6ヶ月の月次売上データ
FROM monthly_revenue2024年1〜6月の月次売上データ6行を読み込みます。LAG 関数で各月の「前月の revenue」を同じ行に並べることで、自己 JOIN なしで前月比成長率を1クエリで算出します。
1 / 4
month▸ revenue
2024-011,200,000
2024-021,350,000
2024-031,180,000
2024-041,520,000
2024-051,680,000
2024-061,450,000
monthly_revenue: 6行(2024-01〜2024-06)
学習ポイント
LAG vs LEAD の使い分け:LAG はウィンドウ内の前行の値、LEAD は後行の値を参照します。前月比なら LAG(revenue, 1)、翌月予測との差分なら LEAD(revenue, 1)、前年同月比なら LAG(revenue, 12) と offset を調整するだけです。PARTITION BY を組み合わせると「カテゴリ別の前月比」も1クエリで算出できます。
NULL の伝播と COALESCE による制御:初月の prev_revenue が NULL のとき、演算結果(revenue - NULL)も NULL に自動伝播します。これは正常な動作です。NULL を 0 に置換したい場合は COALESCE(mom_growth_pct, 0)、表示を「−」にしたい場合は COALESCE(CAST(mom_growth_pct AS TEXT), '−') のように COALESCE で後処理します。
LAG の第3引数(デフォルト値):LAG(revenue, 1, 0) のように第3引数を指定すると、前行がない場合に NULL ではなく指定したデフォルト値(この例では 0)を返します。ただし 0 除算が発生するため成長率の計算には注意が必要です。目的に応じて NULL 伝播とデフォルト値指定を使い分けてください。
アンチパターン
OVER 句に ORDER BY を指定しない:LAG(revenue, 1) OVER () と ORDER BY を省略すると、行の参照順序が不定になり前月比が毎回変わる可能性があります。LAG/LEAD には必ず ORDER BY を指定して時系列の順序を確定させてください。
prev_revenue が 0 のときに割り算エラーが発生する:前月売上が 0 の場合、/ prev_revenue はゼロ除算エラーになります。NULLIF(prev_revenue, 0) でゼロを NULL に変換してから割り算し、ゼロ除算を防ぐのが実務の定番パターンです:(revenue - prev_revenue)::numeric / NULLIF(prev_revenue, 0) * 100
実務コラム:時系列ダッシュボードへの応用
LAG を活用すると、前月比・前年同月比・週次成長率を1テーブルで並べた時系列ダッシュボードが構築できます。CTE で LAG(revenue,1)(前月)・LAG(revenue,12)(前年同月)・LAG(revenue,4)(前四半期)を一括付与し、外側クエリで各比率を算出するパターンが実務の定番です。さらに PARTITION BY product_category を加えるとカテゴリ別の時系列比較も同一クエリで実現でき、BIツールへのデータ供給クエリとして直接活用できます。