競合スコアカード — 複数 CTE × 条件付き集計で KPI 達成度を総合評価する
複数の CTE(Common Table Expression)を連鎖させると、生データ → 評価 → 集計 → ランク付けという変換を段階的に分割して書けます。特に条件付き集計(SUM(CASE WHEN ...))は、フラグ列の合計によって「達成 / 未達成」を数え上げる頻出パターンです。
WITH eval AS ( SELECT ..., CASE WHEN value >= target THEN 1 ELSE 0 END AS achieved FROM raw_table ), scored AS ( SELECT company_name, SUM(achieved) AS metrics_achieved, ROUND(SUM(achieved) * 100.0 / COUNT(*), 1) AS achievement_rate FROM eval GROUP BY company_name )
FROM で参照できます。ロジックをステップ単位で分割することで、可読性・デバッグ性が大幅に向上します。CTE 内で GROUP BY した後、外側でウィンドウ関数を適用する 2 段構成が実務の定石です。competitor_metrics テーブルには各社の KPI 実績値と目標値が格納されています。解約率は「値が小さいほど良い」逆方向指標であることに注意しながら、各社の KPI 達成数・達成率・総合ランクを算出してください。取得列は company_name, metrics_achieved, total_metrics, achievement_rate, overall_rank、overall_rank 昇順 → company_name 昇順で返してください。
| company_name | metric_name | value | target |
|---|---|---|---|
| AlphaTech | 売上成長率(%) | 18.8 | 15.0 |
| AlphaTech | 市場シェア(%) | 36.5 | 35.0 |
| AlphaTech | NPS | 42 | 50 |
| AlphaTech | 解約率(%) | 3.2 | 4.0 |
| BetaSoft | 売上成長率(%) | 19.2 | 15.0 |
| BetaSoft | 市場シェア(%) | 28.0 | 30.0 |
| BetaSoft | NPS | 55 | 50 |
| BetaSoft | 解約率(%) | 4.8 | 4.0 |
| GammaSys | 売上成長率(%) | 22.2 | 15.0 |
| GammaSys | 市場シェア(%) | 18.0 | 20.0 |
| GammaSys | NPS | 38 | 50 |
| GammaSys | 解約率(%) | 2.5 | 4.0 |
| DeltaNet | 売上成長率(%) | 14.0 | 15.0 |
| DeltaNet | 市場シェア(%) | 15.0 | 35.0 |
| DeltaNet | NPS | 35 | 50 |
| DeltaNet | 解約率(%) | 5.5 | 4.0 |
| company_name | metrics_achieved | total_metrics | achievement_rate | overall_rank |
|---|---|---|---|---|
| AlphaTech | 3 | 4 | 75.0 | 1 |
| BetaSoft | 2 | 4 | 50.0 | 2 |
| GammaSys | 2 | 4 | 50.0 | 2 |
| DeltaNet | 0 | 4 | 0.0 | 3 |
WITH eval AS ( SELECT company_name, metric_name, value, target, CASE WHEN metric_name = '解約率(%)' AND value <= target THEN 1 -- 逆方向: 値 <= 目標で達成 WHEN metric_name != '解約率(%)' AND value >= target THEN 1 -- 通常方向: 値 >= 目標で達成 ELSE 0 -- 未達成 END AS achieved FROM competitor_metrics ), scored AS ( SELECT company_name, SUM(achieved) AS metrics_achieved, -- 達成フラグを合計 COUNT(*) AS total_metrics, ROUND(SUM(achieved) * 100.0 / COUNT(*), 1) AS achievement_rate -- 達成率(%)を算出 FROM eval GROUP BY company_name ) SELECT company_name, metrics_achieved, total_metrics, achievement_rate, DENSE_RANK() OVER (ORDER BY achievement_rate DESC) AS overall_rank -- 同率は同ランク・連番継続 FROM scored ORDER BY overall_rank, company_name; /* 実行順序(SQLの論理的な評価順): 1. CTE eval を定義 → CTE を定義(達成フラグを評価) 2. CTE scored を定義 → グループ化して集計関数を評価 3. 外側クエリ → DENSE_RANK でウィンドウ関数を評価(行数は保持) 4. ORDER BY → 並び替えて出力 */
LEGEND
① FROM competitor_metrics
FROM competitor_metricscompetitor_metrics テーブルの16行を読み込みます。metric_name・value・target の3列から achieved フラグを計算します。| company_name | metric_name | value | target |
|---|---|---|---|
| AlphaTech | 売上成長率(%) | 18.8 | 15 |
| AlphaTech | 市場シェア(%) | 36.5 | 35 |
| AlphaTech | NPS | 42 | 50 |
| AlphaTech | 解約率(%) | 3.2 | 4 |
| BetaSoft | 売上成長率(%) | 19.2 | 15 |
| BetaSoft | 市場シェア(%) | 28 | 30 |
| BetaSoft | NPS | 55 | 50 |
| BetaSoft | 解約率(%) | 4.8 | 4 |
| GammaSys | 売上成長率(%) | 22.2 | 15 |
| GammaSys | 市場シェア(%) | 18 | 20 |
| GammaSys | NPS | 38 | 50 |
| GammaSys | 解約率(%) | 2.5 | 4 |
| DeltaNet | 売上成長率(%) | 14 | 15 |
| DeltaNet | 市場シェア(%) | 15 | 35 |
| DeltaNet | NPS | 35 | 50 |
| DeltaNet | 解約率(%) | 5.5 | 4 |
SELECT * FROM eval のようにデバッグできるのが最大のメリットです。一つの長い SELECT に全てを詰め込む場合と比べ、保守コストが大幅に下がります。SUM(CASE WHEN 条件 THEN 1 ELSE 0 END) は「条件を満たす行数を合計する」汎用パターンです。COUNT(CASE WHEN 条件 THEN 1 END) でも同結果ですが、SUM パターンは重み付け(THEN 2 など)に拡張しやすく実務でよく使われます。value >= target で評価すると、良好な実績が「未達成」と判定されます。指標の種別(高い方が良い / 低い方が良い)を設計段階でデータに持たせておくのが実務上の定石です。SELECT SUM(achieved), DENSE_RANK() OVER (...) FROM eval GROUP BY company_name は動作しますが、ウィンドウ関数は GROUP BY 後の集計済みデータに対して適用されます。CTE で集約を先に切り出す方が意図が明確でバグが発生しにくいです。SUM(achieved * weight) / SUM(weight))への拡張も同じ構文パターンで対応できます。移動平均 × 累積売上 — ROWS BETWEEN フレーム句で四半期トレンドを読む
ウィンドウ関数のフレーム句(ROWS BETWEEN)を指定すると、「何行前から何行後まで」を集計対象にするか細かく制御できます。移動平均と累積合計はその代表的な応用です。
| フレーム句 | 意味 | 主な用途 |
|---|---|---|
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW | 直前2行〜当行(計3行) | 3期移動平均 |
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW | 先頭行〜当行(全累積) | 累積売上・累積件数 |
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING | 当行〜末尾行 | 残り合計 |
AVG(revenue) OVER ( PARTITION BY company_name ORDER BY quarter ROWS BETWEEN 2 PRECEDING AND CURRENT ROW -- 3四半期移動平均 )
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW(累積)です。移動平均のように「直近 N 行のみ」を対象にするには、ROWS BETWEEN を必ず明示指定してください。quarterly_revenue テーブルの各社四半期売上に対し、3四半期移動平均(moving_avg_3q)と累積売上(cumulative_rev)を算出してください。取得列は company_name, quarter, revenue, moving_avg_3q, cumulative_rev、company_name 昇順 → quarter 昇順で返してください。
| company_name | quarter | revenue |
|---|---|---|
| AlphaTech | 2024Q1 | 95 |
| AlphaTech | 2024Q2 | 100 |
| AlphaTech | 2024Q3 | 108 |
| AlphaTech | 2024Q4 | 115 |
| BetaSoft | 2024Q1 | 68 |
| BetaSoft | 2024Q2 | 72 |
| BetaSoft | 2024Q3 | 75 |
| BetaSoft | 2024Q4 | 78 |
| GammaSys | 2024Q1 | 38 |
| GammaSys | 2024Q2 | 42 |
| GammaSys | 2024Q3 | 45 |
| GammaSys | 2024Q4 | 50 |
※ revenue 単位: 億円
| company_name | quarter | revenue | moving_avg_3q | cumulative_rev |
|---|---|---|---|---|
| AlphaTech | 2024Q1 | 95 | 95.0 | 95 |
| AlphaTech | 2024Q2 | 100 | 97.5 | 195 |
| AlphaTech | 2024Q3 | 108 | 101.0 | 303 |
| AlphaTech | 2024Q4 | 115 | 107.7 | 418 |
| BetaSoft | 2024Q1 | 68 | 68.0 | 68 |
| BetaSoft | 2024Q2 | 72 | 70.0 | 140 |
| BetaSoft | 2024Q3 | 75 | 71.7 | 215 |
| BetaSoft | 2024Q4 | 78 | 75.0 | 293 |
| GammaSys | 2024Q1 | 38 | 38.0 | 38 |
| GammaSys | 2024Q2 | 42 | 40.0 | 80 |
| GammaSys | 2024Q3 | 45 | 41.7 | 125 |
| GammaSys | 2024Q4 | 50 | 45.7 | 175 |
SELECT company_name, quarter, revenue, ROUND( AVG(revenue) OVER ( PARTITION BY company_name -- 企業別に独立ウィンドウ ORDER BY quarter ROWS BETWEEN 2 PRECEDING AND CURRENT ROW -- 直前2行+当行の計3行で平均 ), 1 ) AS moving_avg_3q, SUM(revenue) OVER ( PARTITION BY company_name -- 企業別に独立ウィンドウ ORDER BY quarter ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW -- 先頭〜当行の累積合計 ) AS cumulative_rev FROM quarterly_revenue ORDER BY company_name, quarter; /* 実行順序(SQLの論理的な評価順): 1. FROM quarterly_revenue → 行を読み込む 2. AVG/SUM OVER (...) → ウィンドウ関数を評価(行数は保持) 3. SELECT → 列を評価 4. ORDER BY company_name, quarter → 並び替えて出力 */
LEGEND
① FROM quarterly_revenue
FROM quarterly_revenuequarterly_revenue テーブルの12行を読み込みます。PARTITION BY company_name により企業ごとに独立したウィンドウが設定されます。| company_name | quarter | revenue |
|---|---|---|
| AlphaTech | 2024Q1 | 95 |
| AlphaTech | 2024Q2 | 100 |
| AlphaTech | 2024Q3 | 108 |
| AlphaTech | 2024Q4 | 115 |
| BetaSoft | 2024Q1 | 68 |
| BetaSoft | 2024Q2 | 72 |
| BetaSoft | 2024Q3 | 75 |
| BetaSoft | 2024Q4 | 78 |
| GammaSys | 2024Q1 | 38 |
| GammaSys | 2024Q2 | 42 |
| GammaSys | 2024Q3 | 45 |
| GammaSys | 2024Q4 | 50 |
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW は「当行の直前2行〜当行まで(最大3行)」を意味します。境界値には UNBOUNDED PRECEDING(先頭), N PRECEDING, CURRENT ROW, N FOLLOWING, UNBOUNDED FOLLOWING(末尾)が使えます。ROWS BETWEEN を明示しないと累積平均が計算されてしまいます。RANGE BETWEEN 2 PRECEDING AND CURRENT ROW は「値が現在値-2以上の行」を対象にする値ベースのフレームです。文字列型の quarter 列には意図通りに動かない可能性があります。行数ベースには ROWS BETWEEN、日付の範囲には RANGE BETWEEN INTERVAL '...' PRECEDING を使い分けてください。競合パーセンタイル分析 — PERCENT_RANK / CUME_DIST で市場内ポジションを定量化する
PERCENT_RANK と CUME_DIST はどちらも「集団内での相対的な位置」をパーセンテージで表しますが、計算式が異なります。
| 関数 | 計算式 | 最小値 | 最大値 | 解釈 |
|---|---|---|---|---|
PERCENT_RANK() | (rank − 1) / (n − 1) | 0.0(最小値の行) | 1.0(最大値の行) | 自分より小さい値の割合 |
CUME_DIST() | rank / n | 1/n(最小値の行) | 1.0(最大値の行) | 自分以下の値の割合 |
PERCENT_RANK() OVER ( PARTITION BY category ORDER BY annual_revenue -- 昇順: 最大値が 100% ) * 100 AS pct_rank
category_performance テーブルの各社カテゴリ別売上に対し、カテゴリ内パーセンタイル順位(pct_rank)と累積分布(cume_dist_pct)を算出してください(小数第1位、0〜100のスケール)。取得列は category, company_name, annual_revenue, pct_rank, cume_dist_pct、category 昇順 → annual_revenue 降順で返してください。
| company_name | category | annual_revenue |
|---|---|---|
| AlphaTech | クラウド | 4200 |
| BetaSoft | クラウド | 2800 |
| GammaSys | クラウド | 1500 |
| DeltaNet | クラウド | 800 |
| EpsilonIO | クラウド | 500 |
| BetaSoft | セキュリティ | 2200 |
| AlphaTech | セキュリティ | 1200 |
| DeltaNet | セキュリティ | 1200 |
| GammaSys | セキュリティ | 900 |
| EpsilonIO | セキュリティ | 600 |
※ annual_revenue 単位: 億円
| category | company_name | annual_revenue | pct_rank | cume_dist_pct |
|---|---|---|---|---|
| クラウド | AlphaTech | 4200 | 100.0 | 100.0 |
| クラウド | BetaSoft | 2800 | 75.0 | 80.0 |
| クラウド | GammaSys | 1500 | 50.0 | 60.0 |
| クラウド | DeltaNet | 800 | 25.0 | 40.0 |
| クラウド | EpsilonIO | 500 | 0.0 | 20.0 |
| セキュリティ | BetaSoft | 2200 | 100.0 | 100.0 |
| セキュリティ | AlphaTech | 1200 | 50.0 | 80.0 |
| セキュリティ | DeltaNet | 1200 | 50.0 | 80.0 |
| セキュリティ | GammaSys | 900 | 25.0 | 40.0 |
| セキュリティ | EpsilonIO | 600 | 0.0 | 20.0 |
SELECT category, company_name, annual_revenue, ROUND( (PERCENT_RANK() OVER ( PARTITION BY category -- カテゴリ別に独立ウィンドウ ORDER BY annual_revenue -- 昇順: 最小値=0%, 最大値=100% ) * 100)::numeric, 1 ) AS pct_rank, ROUND( (CUME_DIST() OVER ( PARTITION BY category -- カテゴリ別に独立ウィンドウ ORDER BY annual_revenue -- rank/n で自分以下の割合 ) * 100)::numeric, 1 ) AS cume_dist_pct FROM category_performance ORDER BY category, annual_revenue DESC; /* 実行順序(SQLの論理的な評価順): 1. FROM category_performance → 行を読み込む 2. PERCENT_RANK/CUME_DIST OVER → ウィンドウ関数を評価(行数は保持) 3. ROUND(...) → 値を整形 4. SELECT → 列を評価 5. ORDER BY category, annual_revenue DESC → 並び替えて出力 */
LEGEND
① FROM category_performance
FROM category_performancecategory_performance の10行を読み込みます。カテゴリごとに5社ずつのデータがあります。ウィンドウ関数は ORDER BY annual_revenue(昇順)で評価されるため、この段階で昇順に並べておきます。| category | company_name | annual_revenue |
|---|---|---|
| クラウド | EpsilonIO | 500 |
| クラウド | DeltaNet | 800 |
| クラウド | GammaSys | 1500 |
| クラウド | BetaSoft | 2800 |
| クラウド | AlphaTech | 4200 |
| セキュリティ | EpsilonIO | 600 |
| セキュリティ | GammaSys | 900 |
| セキュリティ | AlphaTech | 1200 |
| セキュリティ | DeltaNet | 1200 |
| セキュリティ | BetaSoft | 2200 |
(rank-1)/(n-1)。n=5 の場合 rank=1(最小)→0.0、rank=3→0.5、rank=5(最大)→1.0 です。最小値のパーセンタイルは必ず 0.0(「自分より小さい値の割合」= 0%)。これを「最低評価=0点」と誤読しないよう注意してください。rank/n。n=5 の場合、最小値でも 1/5=20% になります。「EpsilonIO の cume_dist=20% は、クラウド市場において EpsilonIO 以下の売上の企業が全体の 20% 存在する」という意味です。PERCENT_RANK と異なり、0% には絶対にならないのが特徴です。(3-1)/4 = 50.0% ですが、CUME_DIST は自身を含む以下の行数が4行になるため 4/5 = 80.0% になります。WHERE cume_dist_pct >= 0.8 と記述すれば「上位20%の企業に絞り込む」フィルタを実現でき、NTILE では難しい柔軟なパーセンタイル閾値フィルタリングが可能です。カテゴリ首位との差分分析 — FIRST_VALUE × CTE でリーダーとのギャップを可視化する
FIRST_VALUE() は OVER 句で定義したウィンドウの「最初の行の値」を全行に付与するウィンドウ関数です。ORDER BY score DESC と組み合わせれば「そのパーティション内の最大値(首位の値)」を取得できます。
| 関数 | 取得内容 | 用途 |
|---|---|---|
FIRST_VALUE(col) | ウィンドウの先頭行の値 | カテゴリ内首位の値・名前 |
LAST_VALUE(col) | ウィンドウの末尾行の値※ | カテゴリ内最下位の値 |
NTH_VALUE(col,n) | ウィンドウの n 番目の行の値 | 2位・3位の値 |
FIRST_VALUE(company_name) OVER ( PARTITION BY category ORDER BY composite_score DESC -- 降順: 先頭行が最高スコア ) AS leader_name
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW(当行まで累積)のため、常に「自分自身の値」が返ります。末尾の値を取るには ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING が必要です。product_scores テーブルから、各カテゴリの首位企業名(leader_name)・首位スコア(leader_score)・首位とのスコア差(gap_to_leader)を算出してください。CTE を使って実装してください。取得列は category, company_name, composite_score, leader_name, leader_score, gap_to_leader、カテゴリはクラウド→セキュリティ→データ分析の指定順、カテゴリ内は composite_score 降順で返してください。
| company_name | category | composite_score |
|---|---|---|
| AlphaTech | クラウド | 88.5 |
| BetaSoft | クラウド | 79.2 |
| GammaSys | クラウド | 71.0 |
| DeltaNet | クラウド | 65.3 |
| BetaSoft | セキュリティ | 91.3 |
| AlphaTech | セキュリティ | 74.1 |
| GammaSys | セキュリティ | 68.7 |
| DeltaNet | セキュリティ | 55.2 |
| AlphaTech | データ分析 | 83.4 |
| GammaSys | データ分析 | 80.1 |
| BetaSoft | データ分析 | 76.8 |
| DeltaNet | データ分析 | 62.0 |
| category | company_name | composite_score | leader_name | leader_score | gap_to_leader |
|---|---|---|---|---|---|
| クラウド | AlphaTech | 88.5 | AlphaTech | 88.5 | 0.0 |
| クラウド | BetaSoft | 79.2 | AlphaTech | 88.5 | -9.3 |
| クラウド | GammaSys | 71.0 | AlphaTech | 88.5 | -17.5 |
| クラウド | DeltaNet | 65.3 | AlphaTech | 88.5 | -23.2 |
| セキュリティ | BetaSoft | 91.3 | BetaSoft | 91.3 | 0.0 |
| セキュリティ | AlphaTech | 74.1 | BetaSoft | 91.3 | -17.2 |
| セキュリティ | GammaSys | 68.7 | BetaSoft | 91.3 | -22.6 |
| セキュリティ | DeltaNet | 55.2 | BetaSoft | 91.3 | -36.1 |
| データ分析 | AlphaTech | 83.4 | AlphaTech | 83.4 | 0.0 |
| データ分析 | GammaSys | 80.1 | AlphaTech | 83.4 | -3.3 |
| データ分析 | BetaSoft | 76.8 | AlphaTech | 83.4 | -6.6 |
| データ分析 | DeltaNet | 62.0 | AlphaTech | 83.4 | -21.4 |
WITH ranked AS ( SELECT category, company_name, composite_score, FIRST_VALUE(company_name) OVER ( PARTITION BY category -- カテゴリ別に独立ウィンドウ ORDER BY composite_score DESC -- 降順: 先頭行が最高スコア ) AS leader_name, FIRST_VALUE(composite_score) OVER ( PARTITION BY category -- 同一ウィンドウで首位スコアを取得 ORDER BY composite_score DESC ) AS leader_score FROM product_scores ) SELECT category, company_name, composite_score, leader_name, leader_score, ROUND(composite_score - leader_score, 1) AS gap_to_leader -- 首位との差分(負=遅れ) FROM ranked ORDER BY CASE category WHEN 'クラウド' THEN 1 WHEN 'セキュリティ' THEN 2 WHEN 'データ分析' THEN 3 END, composite_score DESC, company_name; /* 実行順序(SQLの論理的な評価順): 1. CTE ranked を定義 → FIRST_VALUE でウィンドウ関数を評価(行数は保持) 2. 外側クエリ → 差分を評価し ROUND で値を整形 3. ORDER BY CASE category WHEN 'クラウド' THEN 1 WHEN 'セキュリティ' THEN 2 WHEN 'データ分析' THEN 3 END, composite_score DESC, company_name → 並び替えて出力 */
LEGEND
① FROM product_scores
FROM product_scoresproduct_scores テーブルの12行を読み込みます。カテゴリ別(クラウド・セキュリティ・データ分析)に4社ずつのスコアがあります。この後 FIRST_VALUE が各カテゴリの首位を特定します。| category | company_name | composite_score |
|---|---|---|
| クラウド | AlphaTech | 88.5 |
| クラウド | BetaSoft | 79.2 |
| クラウド | GammaSys | 71 |
| クラウド | DeltaNet | 65.3 |
| セキュリティ | BetaSoft | 91.3 |
| セキュリティ | AlphaTech | 74.1 |
| セキュリティ | GammaSys | 68.7 |
| セキュリティ | DeltaNet | 55.2 |
| データ分析 | AlphaTech | 83.4 |
| データ分析 | GammaSys | 80.1 |
| データ分析 | BetaSoft | 76.8 |
| データ分析 | DeltaNet | 62 |
ranked に切り出すことで、外側クエリは composite_score - leader_score と簡潔に書けます。同じウィンドウ式は CTE か VIEW に切り出すのが実務の定石です。WHERE gap_to_leader = 0 で全カテゴリの首位企業のみを抽出できます(これを外側クエリの WHERE に追加するだけ)。LAST_VALUE(composite_score) OVER (PARTITION BY category ORDER BY composite_score DESC) とデフォルトのまま書くと、フレームが当行までの累積になるため常に「自分のスコア」が返ります。カテゴリ内最下位を取るには ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING の明示が必要です。MAX(composite_score) OVER (PARTITION BY category) で最大スコアは取れますが、「そのスコアを持つ企業名」は取得できません。競合の「誰が首位か」を取得するには FIRST_VALUE が必須です。企業名なしでスコアだけ必要な場合は MAX の方がシンプルです。シェア増減トレンド分析 — LAG × CASE × SUM OVER の CTE チェーンで勝者・敗者を分類する
複数の CTE を直列に繋ぐ CTE チェーンを使うと、複雑なデータ変換を「前期比計算 → 分類 → 集計」というステップで整理できます。本問では LAG・CASE WHEN・SUM OVER の 3 つの構文を連携させるパターンを学びます。
WITH step1 AS ( SELECT ..., LAG(val) OVER (PARTITION BY grp ORDER BY t) AS prev_val FROM src ), step2 AS ( SELECT ..., val - prev_val AS delta, CASE WHEN val > prev_val THEN '▲ 拡大' ELSE '▼ 縮小' END AS trend FROM step1 WHERE prev_val IS NOT NULL -- 先頭期の NULL を除外 ) SELECT ..., SUM(CASE WHEN trend = '▲ 拡大' THEN 1 ELSE 0 END) OVER (PARTITION BY company_name ORDER BY period) AS expanding_periods FROM step2;
SELECT * FROM step1 で確認し、step2 を重ね、最後に集計する段階的な開発が可能です。market_share_trend テーブルには4社の半期別市場シェアが格納されています。2つの CTE を連鎖させ、① 前期比シェア変化量(share_delta)、② 増減ラベル(trend)、③ 累積拡大期間数(expanding_periods)を算出してください。先頭期(prev_share が NULL の行)は除外してください。取得列は company_name, period, market_share_pct, prev_share, share_delta, trend, expanding_periods、company_name 昇順 → period 昇順で返してください。
| company_name | period | market_share_pct |
|---|---|---|
| AlphaTech | 2023H1 | 38.5 |
| AlphaTech | 2023H2 | 40.2 |
| AlphaTech | 2024H1 | 42.0 |
| BetaSoft | 2023H1 | 30.1 |
| BetaSoft | 2023H2 | 28.5 |
| BetaSoft | 2024H1 | 26.8 |
| DeltaNet | 2023H1 | 13.0 |
| DeltaNet | 2023H2 | 12.3 |
| DeltaNet | 2024H1 | 11.7 |
| GammaSys | 2023H1 | 18.4 |
| GammaSys | 2023H2 | 19.0 |
| GammaSys | 2024H1 | 19.5 |
| company_name | period | market_share_pct | prev_share | share_delta | trend | expanding_periods |
|---|---|---|---|---|---|---|
| AlphaTech | 2023H2 | 40.2 | 38.5 | 1.7 | ▲ 拡大 | 1 |
| AlphaTech | 2024H1 | 42.0 | 40.2 | 1.8 | ▲ 拡大 | 2 |
| BetaSoft | 2023H2 | 28.5 | 30.1 | -1.6 | ▼ 縮小 | 0 |
| BetaSoft | 2024H1 | 26.8 | 28.5 | -1.7 | ▼ 縮小 | 0 |
| DeltaNet | 2023H2 | 12.3 | 13.0 | -0.7 | ▼ 縮小 | 0 |
| DeltaNet | 2024H1 | 11.7 | 12.3 | -0.6 | ▼ 縮小 | 0 |
| GammaSys | 2023H2 | 19.0 | 18.4 | 0.6 | ▲ 拡大 | 1 |
| GammaSys | 2024H1 | 19.5 | 19.0 | 0.5 | ▲ 拡大 | 2 |
WITH share_with_prev AS ( SELECT company_name, period, market_share_pct, LAG(market_share_pct) OVER ( PARTITION BY company_name -- 企業別に独立ウィンドウ ORDER BY period -- 期の昇順で1行前のシェアを参照 ) AS prev_share -- 先頭期(2023H1)は前行がなく NULL FROM market_share_trend ), deltas AS ( SELECT company_name, period, market_share_pct, prev_share, ROUND(market_share_pct - prev_share, 1) AS share_delta, -- 前期比変化量 CASE WHEN market_share_pct > prev_share THEN '▲ 拡大' WHEN market_share_pct < prev_share THEN '▼ 縮小' ELSE '— 横ばい' END AS trend FROM share_with_prev WHERE prev_share IS NOT NULL -- 先頭期(NULL行)を除外 ) SELECT company_name, period, market_share_pct, prev_share, share_delta, trend, SUM(CASE WHEN trend = '▲ 拡大' THEN 1 ELSE 0 END) OVER ( PARTITION BY company_name -- 企業別に累積 ORDER BY period -- 期順に拡大フラグを積み上げ ) AS expanding_periods FROM deltas ORDER BY company_name, period; /* 実行順序(SQLの論理的な評価順): 1. CTE share_with_prev を定義 → LAG でウィンドウ関数を評価(行数は保持) 2. CTE deltas を定義 → WHERE で絞り込み、変化量とラベルを評価 3. 外側クエリ → SUM OVER でウィンドウ関数を評価(行数は保持) 4. ORDER BY company_name, period → 並び替えて出力 */
LEGEND
① CTE share_with_prev: FROM + LAG()
WITH share_with_prev AS ( ... LAG(...) OVER(...) AS prev_share )market_share_trend の12行を読み込み、企業別・期順に LAG() で前期シェアを付与します。各社の先頭期(2023H1)は前行がないため prev_share=NULL(4行)。| company_name | period | market_share_pct | ▸ prev_share |
|---|---|---|---|
| AlphaTech | 2023H1 | 38.5 | NULL |
| AlphaTech | 2023H2 | 40.2 | 38.5 |
| AlphaTech | 2024H1 | 42 | 40.2 |
| BetaSoft | 2023H1 | 30.1 | NULL |
| BetaSoft | 2023H2 | 28.5 | 30.1 |
| BetaSoft | 2024H1 | 26.8 | 28.5 |
| DeltaNet | 2023H1 | 13 | NULL |
| DeltaNet | 2023H2 | 12.3 | 13 |
| DeltaNet | 2024H1 | 11.7 | 12.3 |
| GammaSys | 2023H1 | 18.4 | NULL |
| GammaSys | 2023H2 | 19 | 18.4 |
| GammaSys | 2024H1 | 19.5 | 19 |
SUM(CASE WHEN trend='▲ 拡大' THEN 1 ELSE 0 END) OVER (PARTITION BY company_name ORDER BY period) は「その企業がその期までに拡大した期間の累積数」を計算します。ORDER BY を伴うウィンドウ SUM は累積(デフォルトフレーム)になるため、期が進むにつれてカウントが積み上がります。WHERE prev_share IS NOT NULL を指定することで、LAG が NULL を返す各社先頭期を除外します。このフィルタがないと先頭期の行で share_delta が NULL になり、trend が '─ 横ばい' に誤分類される可能性があります。LAG(market_share_pct) OVER (ORDER BY period) と書くと、BetaSoft 2023H1 の「前行」が AlphaTech 2024H1 になり、企業をまたいだ前期比が計算されます。シェアが 26.8→38.5 などの意味のない差分が生じます。時系列 LAG には常に PARTITION BY が必須です。SUM(CASE WHEN trend='▲ 拡大' THEN 1 ELSE 0 END) の CASE が ELSE 0 に評価されてカウントに含まれてしまいます。WHERE で先頭期を除外することで、expanding_periods の起点を正しく設定できます。