SQL 競合分析 — 複数CTE・フレーム句・FIRST_VALUEの応用

応用競合他社分析複数CTEフレーム句FIRST_VALUEPostgreSQL対応全5問
1 / 5 · 学習中 0 / 5 問完了
QUESTION 1

競合スコアカード — 複数 CTE × 条件付き集計で KPI 達成度を総合評価する

WITH CTESUM(CASE WHEN)スコアカードDENSE_RANK
前提知識

複数の 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
)
CTE チェーンのポイント:前の CTE の結果を次の CTE の FROM で参照できます。ロジックをステップ単位で分割することで、可読性・デバッグ性が大幅に向上します。CTE 内で GROUP BY した後、外側でウィンドウ関数を適用する 2 段構成が実務の定石です。
問題

competitor_metrics テーブルには各社の KPI 実績値と目標値が格納されています。解約率は「値が小さいほど良い」逆方向指標であることに注意しながら、各社の KPI 達成数・達成率・総合ランクを算出してください。取得列は company_name, metrics_achieved, total_metrics, achievement_rate, overall_rank、overall_rank 昇順 → company_name 昇順で返してください。

使用テーブル
► competitor_metrics(16行)
company_namemetric_namevaluetarget
AlphaTech売上成長率(%)18.815.0
AlphaTech市場シェア(%)36.535.0
AlphaTechNPS4250
AlphaTech解約率(%)3.24.0
BetaSoft売上成長率(%)19.215.0
BetaSoft市場シェア(%)28.030.0
BetaSoftNPS5550
BetaSoft解約率(%)4.84.0
GammaSys売上成長率(%)22.215.0
GammaSys市場シェア(%)18.020.0
GammaSysNPS3850
GammaSys解約率(%)2.54.0
DeltaNet売上成長率(%)14.015.0
DeltaNet市場シェア(%)15.035.0
DeltaNetNPS3550
DeltaNet解約率(%)5.54.0
期待出力
company_namemetrics_achievedtotal_metricsachievement_rateoverall_rank
AlphaTech3475.01
BetaSoft2450.02
GammaSys2450.02
DeltaNet040.03
模範解答コード
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             → 並び替えて出力
*/
解説(テーブル変化・ポイント)
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;
LEGEND
データ取得・読込対象
① FROM competitor_metrics
FROM competitor_metricscompetitor_metrics テーブルの16行を読み込みます。metric_name・value・target の3列から achieved フラグを計算します。
1 / 4
company_namemetric_namevaluetarget
AlphaTech売上成長率(%)18.815
AlphaTech市場シェア(%)36.535
AlphaTechNPS4250
AlphaTech解約率(%)3.24
BetaSoft売上成長率(%)19.215
BetaSoft市場シェア(%)2830
BetaSoftNPS5550
BetaSoft解約率(%)4.84
GammaSys売上成長率(%)22.215
GammaSys市場シェア(%)1820
GammaSysNPS3850
GammaSys解約率(%)2.54
DeltaNet売上成長率(%)1415
DeltaNet市場シェア(%)1535
DeltaNetNPS3550
DeltaNet解約率(%)5.54
16行読込
学習ポイント
CTE チェーンによるステップ分割:eval → scored → 外側クエリという 3 段構造で「フラグ計算 / 集計 / ランク付与」を分離して書けます。各 CTE を単体で SELECT * FROM eval のようにデバッグできるのが最大のメリットです。一つの長い SELECT に全てを詰め込む場合と比べ、保守コストが大幅に下がります。
SUM(CASE WHEN) の条件付き集計パターン:SUM(CASE WHEN 条件 THEN 1 ELSE 0 END) は「条件を満たす行数を合計する」汎用パターンです。COUNT(CASE WHEN 条件 THEN 1 END) でも同結果ですが、SUM パターンは重み付け(THEN 2 など)に拡張しやすく実務でよく使われます。
DENSE_RANK vs RANK の選択:競合スコアカードで「同率は同ランクにしつつ次の番号を飛ばしたくない」場面では DENSE_RANK が適切です。BetaSoft・GammaSys 同率2位の場合、RANK だと「1,2,2,4」で DeltaNet が4位になりますが、DENSE_RANK なら「1,2,2,3」と連番が保たれます。
アンチパターン
逆方向指標の判定漏れ:解約率・コスト・不良品率など「低い方が良い」指標を他と同じ value >= target で評価すると、良好な実績が「未達成」と判定されます。指標の種別(高い方が良い / 低い方が良い)を設計段階でデータに持たせておくのが実務上の定石です。
GROUP BY 後にウィンドウ関数を同一 SELECT に書こうとする:SELECT SUM(achieved), DENSE_RANK() OVER (...) FROM eval GROUP BY company_name は動作しますが、ウィンドウ関数は GROUP BY 後の集計済みデータに対して適用されます。CTE で集約を先に切り出す方が意図が明確でバグが発生しにくいです。
実務コラム:KPI スコアカードとバランスト・スコアカード
本問のパターンは競合モニタリングダッシュボードの中核ロジックです。売上・顧客満足度・オペレーション・財務の多軸で競合を評価し達成率をスコア化することで、「総合的にどの競合が強いか」を一表で把握できます。指標ごとに重み付けを加えた 加重スコアSUM(achieved * weight) / SUM(weight))への拡張も同じ構文パターンで対応できます。
QUESTION 2

移動平均 × 累積売上 — ROWS BETWEEN フレーム句で四半期トレンドを読む

AVG OVERROWS 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四半期移動平均
)
デフォルトフレームとの違い:ORDER BY を指定した場合のデフォルトフレームは 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 昇順で返してください。

使用テーブル
► quarterly_revenue(12行)
company_namequarterrevenue
AlphaTech2024Q195
AlphaTech2024Q2100
AlphaTech2024Q3108
AlphaTech2024Q4115
BetaSoft2024Q168
BetaSoft2024Q272
BetaSoft2024Q375
BetaSoft2024Q478
GammaSys2024Q138
GammaSys2024Q242
GammaSys2024Q345
GammaSys2024Q450

※ revenue 単位: 億円

期待出力
company_namequarterrevenuemoving_avg_3qcumulative_rev
AlphaTech2024Q19595.095
AlphaTech2024Q210097.5195
AlphaTech2024Q3108101.0303
AlphaTech2024Q4115107.7418
BetaSoft2024Q16868.068
BetaSoft2024Q27270.0140
BetaSoft2024Q37571.7215
BetaSoft2024Q47875.0293
GammaSys2024Q13838.038
GammaSys2024Q24240.080
GammaSys2024Q34541.7125
GammaSys2024Q45045.7175
模範解答コード
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 → 並び替えて出力
*/
解説(テーブル変化・ポイント)
SELECT company_name, quarter, revenue, ROUND( AVG(revenue) OVER ( PARTITION BY company_name ORDER BY quarter ROWS BETWEEN 2 PRECEDING AND CURRENT ROW ), 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;
LEGEND
データ取得・読込対象
① FROM quarterly_revenue
FROM quarterly_revenuequarterly_revenue テーブルの12行を読み込みます。PARTITION BY company_name により企業ごとに独立したウィンドウが設定されます。
1 / 3
company_namequarterrevenue
AlphaTech2024Q195
AlphaTech2024Q2100
AlphaTech2024Q3108
AlphaTech2024Q4115
BetaSoft2024Q168
BetaSoft2024Q272
BetaSoft2024Q375
BetaSoft2024Q478
GammaSys2024Q138
GammaSys2024Q242
GammaSys2024Q345
GammaSys2024Q450
12行読込
学習ポイント
ROWS BETWEEN のフレーム指定:ROWS BETWEEN 2 PRECEDING AND CURRENT ROW は「当行の直前2行〜当行まで(最大3行)」を意味します。境界値には UNBOUNDED PRECEDING(先頭), N PRECEDING, CURRENT ROW, N FOLLOWING, UNBOUNDED FOLLOWING(末尾)が使えます。
ORDER BY ありのデフォルトフレームとの差異:ORDER BY を指定した場合、フレーム省略時は RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW(累積)がデフォルトになります。移動平均のように「直近 N 行のみ」が必要なときは ROWS BETWEEN を明示しないと累積平均が計算されてしまいます。
2つのウィンドウ関数の並列計算:moving_avg_3q と cumulative_rev は同一 SELECT 句内に記述されており、論理的には同じ FROM 結果に対して並列評価されます。それぞれが独立したフレーム句を持てる点がウィンドウ関数の柔軟性の核心です。
アンチパターン
PARTITION BY なしで移動平均を計算する:PARTITION BY を省略すると、BetaSoft 「直前2行」が AlphaTech 2024年第3四半期・第4四半期になり企業をまたいだ無意味な平均が計算されます。時系列ウィンドウ関数では PARTITION BY は必須と考えてください。
ROWS と RANGE の混同:RANGE BETWEEN 2 PRECEDING AND CURRENT ROW は「値が現在値-2以上の行」を対象にする値ベースのフレームです。文字列型の quarter 列には意図通りに動かない可能性があります。行数ベースには ROWS BETWEEN、日付の範囲には RANGE BETWEEN INTERVAL '...' PRECEDING を使い分けてください。
実務コラム:移動平均と季節性調整の活用
競合分析レポートでは四半期売上の単純比較より移動平均の方が「本当のトレンド」を反映することが多いです。SaaS 企業では第4四半期に大型契約が集中する季節性があるため、3〜4 期移動平均でノイズを除去するのが一般的です。また累積売上(cumulative_rev)は年度計画との進捗管理に直結し、Q3 時点で年間目標の何%を達成しているかを可視化する際に累積ウィンドウ関数が活躍します。
QUESTION 3

競合パーセンタイル分析 — PERCENT_RANK / CUME_DIST で市場内ポジションを定量化する

PERCENT_RANKCUME_DISTパーセンタイル市場ポジション
前提知識

PERCENT_RANK と CUME_DIST はどちらも「集団内での相対的な位置」をパーセンテージで表しますが、計算式が異なります。

関数計算式最小値最大値解釈
PERCENT_RANK()(rank − 1) / (n − 1)0.0(最小値の行)1.0(最大値の行)自分より小さい値の割合
CUME_DIST()rank / n1/n(最小値の行)1.0(最大値の行)自分以下の値の割合
PERCENT_RANK() OVER (
  PARTITION BY category
  ORDER BY     annual_revenue   -- 昇順: 最大値が 100%
) * 100 AS pct_rank
n=5 の場合:最下位(rank=1)の PERCENT_RANK = (1-1)/(5-1) = 0%、CUME_DIST = 1/5 = 20%。CUME_DIST は最下位でも 0 にはなりません。
問題

category_performance テーブルの各社カテゴリ別売上に対し、カテゴリ内パーセンタイル順位(pct_rank)と累積分布(cume_dist_pctを算出してください(小数第1位、0〜100のスケール)。取得列は category, company_name, annual_revenue, pct_rank, cume_dist_pct、category 昇順 → annual_revenue 降順で返してください。

使用テーブル
► category_performance(10行 — 5社 × 2カテゴリ)
company_namecategoryannual_revenue
AlphaTechクラウド4200
BetaSoftクラウド2800
GammaSysクラウド1500
DeltaNetクラウド800
EpsilonIOクラウド500
BetaSoftセキュリティ2200
AlphaTechセキュリティ1200
DeltaNetセキュリティ1200
GammaSysセキュリティ900
EpsilonIOセキュリティ600

※ annual_revenue 単位: 億円

期待出力
categorycompany_nameannual_revenuepct_rankcume_dist_pct
クラウドAlphaTech4200100.0100.0
クラウドBetaSoft280075.080.0
クラウドGammaSys150050.060.0
クラウドDeltaNet80025.040.0
クラウドEpsilonIO5000.020.0
セキュリティBetaSoft2200100.0100.0
セキュリティAlphaTech120050.080.0
セキュリティDeltaNet120050.080.0
セキュリティGammaSys90025.040.0
セキュリティEpsilonIO6000.020.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 → 並び替えて出力
*/
解説(テーブル変化・ポイント)
SELECT category, company_name, annual_revenue, ROUND( (PERCENT_RANK() OVER ( PARTITION BY category ORDER BY annual_revenue ) * 100)::numeric, 1) AS pct_rank, ROUND( (CUME_DIST() OVER ( PARTITION BY category ORDER BY annual_revenue ) * 100)::numeric, 1) AS cume_dist_pct FROM category_performance ORDER BY category, annual_revenue DESC;
LEGEND
データ取得・読込対象
① FROM category_performance
FROM category_performancecategory_performance の10行を読み込みます。カテゴリごとに5社ずつのデータがあります。ウィンドウ関数は ORDER BY annual_revenue(昇順)で評価されるため、この段階で昇順に並べておきます。
1 / 4
categorycompany_nameannual_revenue
クラウドEpsilonIO500
クラウドDeltaNet800
クラウドGammaSys1500
クラウドBetaSoft2800
クラウドAlphaTech4200
セキュリティEpsilonIO600
セキュリティGammaSys900
セキュリティAlphaTech1200
セキュリティDeltaNet1200
セキュリティBetaSoft2200
10行読込(昇順表示でランク計算のイメージを掴む)
学習ポイント
PERCENT_RANK の計算式と最小値:(rank-1)/(n-1)。n=5 の場合 rank=1(最小)→0.0、rank=3→0.5、rank=5(最大)→1.0 です。最小値のパーセンタイルは必ず 0.0(「自分より小さい値の割合」= 0%)。これを「最低評価=0点」と誤読しないよう注意してください。
CUME_DIST の計算式と解釈:rank/n。n=5 の場合、最小値でも 1/5=20% になります。「EpsilonIO の cume_dist=20% は、クラウド市場において EpsilonIO 以下の売上の企業が全体の 20% 存在する」という意味です。PERCENT_RANK と異なり、0% には絶対にならないのが特徴です。
同率(タイ)時の挙動の差:2社が同じ売上の場合、PERCENT_RANK は同じランクを付与しますが、CUME_DIST は「その値以下の行数/n」を使います。セキュリティ市場の AlphaTech と DeltaNet (ともに1200億) の場合、PERCENT_RANK は両社とも (3-1)/4 = 50.0% ですが、CUME_DIST は自身を含む以下の行数が4行になるため 4/5 = 80.0% になります。
アンチパターン
「PERCENT_RANK=0 = 最悪」という誤解:pct_rank=0 は「最小値」を意味するだけで絶対的な評価ではありません。セキュリティ市場の EpsilonIO(600億)は pct_rank=0% ですが、クラウドの DeltaNet(800億) の pct_rank=25% より絶対額では小さい、という逆転も起こります。PERCENT_RANK は常にカテゴリ内の相対比較に過ぎません。
NTILE との混同:NTILE(4) は「等分割バケツ番号(1〜4の整数)」、PERCENT_RANK は「0〜1の連続値」で性質が異なります。分布のセグメント分類には NTILE、「上位何%か」の定量表現には PERCENT_RANK が適しています。目的に応じて使い分けてください。
実務コラム:パーセンタイル分析の競合調査への応用
PERCENT_RANK / CUME_DIST は業界調査レポートやベンチマーク分析で頻出します。「自社 NPS は業界内で何パーセンタイルか」「競合製品の価格は市場の何%未満か」といった問いに直接答えられます。またウィンドウ関数の結果を CTE やサブクエリで包み、外側で WHERE cume_dist_pct >= 0.8 と記述すれば「上位20%の企業に絞り込む」フィルタを実現でき、NTILE では難しい柔軟なパーセンタイル閾値フィルタリングが可能です。
QUESTION 4

カテゴリ首位との差分分析 — FIRST_VALUE × CTE でリーダーとのギャップを可視化する

FIRST_VALUEWITH 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
LAST_VALUE の罠:LAST_VALUE のデフォルトフレームは 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 降順で返してください。

使用テーブル
► product_scores(12行 — 4社 × 3カテゴリ)
company_namecategorycomposite_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
期待出力
categorycompany_namecomposite_scoreleader_nameleader_scoregap_to_leader
クラウドAlphaTech88.5AlphaTech88.50.0
クラウドBetaSoft79.2AlphaTech88.5-9.3
クラウドGammaSys71.0AlphaTech88.5-17.5
クラウドDeltaNet65.3AlphaTech88.5-23.2
セキュリティBetaSoft91.3BetaSoft91.30.0
セキュリティAlphaTech74.1BetaSoft91.3-17.2
セキュリティGammaSys68.7BetaSoft91.3-22.6
セキュリティDeltaNet55.2BetaSoft91.3-36.1
データ分析AlphaTech83.4AlphaTech83.40.0
データ分析GammaSys80.1AlphaTech83.4-3.3
データ分析BetaSoft76.8AlphaTech83.4-6.6
データ分析DeltaNet62.0AlphaTech83.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 → 並び替えて出力
*/
解説(テーブル変化・ポイント)
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;
LEGEND
データ取得・読込対象
① FROM product_scores
FROM product_scoresproduct_scores テーブルの12行を読み込みます。カテゴリ別(クラウド・セキュリティ・データ分析)に4社ずつのスコアがあります。この後 FIRST_VALUE が各カテゴリの首位を特定します。
1 / 3
categorycompany_namecomposite_score
クラウドAlphaTech88.5
クラウドBetaSoft79.2
クラウドGammaSys71
クラウドDeltaNet65.3
セキュリティBetaSoft91.3
セキュリティAlphaTech74.1
セキュリティGammaSys68.7
セキュリティDeltaNet55.2
データ分析AlphaTech83.4
データ分析GammaSys80.1
データ分析BetaSoft76.8
データ分析DeltaNet62
12行読込
学習ポイント
FIRST_VALUE で「首位の名前とスコアを同時取得」:MAX(composite_score) OVER (...) なら首位スコアは取得できますが、そのスコアを持つ企業名は取得できません。FIRST_VALUE(company_name) と FIRST_VALUE(composite_score) を組み合わせることで、首位の「名前と値」を同時に全行へ付与できます。これがギャップ分析の鍵です。
CTE でウィンドウ式の重複を排除:FIRST_VALUE(...) OVER (...) を外側クエリに2回書くのは冗長です。CTE ranked に切り出すことで、外側クエリは composite_score - leader_score と簡潔に書けます。同じウィンドウ式は CTE か VIEW に切り出すのが実務の定石です。
gap_to_leader=0.0 が首位の目印:首位企業自身は composite_score - leader_score = 0.0 になります。これを利用して WHERE gap_to_leader = 0 で全カテゴリの首位企業のみを抽出できます(これを外側クエリの WHERE に追加するだけ)。
アンチパターン
LAST_VALUE のデフォルトフレーム問題:LAST_VALUE(composite_score) OVER (PARTITION BY category ORDER BY composite_score DESC) とデフォルトのまま書くと、フレームが当行までの累積になるため常に「自分のスコア」が返ります。カテゴリ内最下位を取るには ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING の明示が必要です。
MAX を使っても企業名は取れない:MAX(composite_score) OVER (PARTITION BY category) で最大スコアは取れますが、「そのスコアを持つ企業名」は取得できません。競合の「誰が首位か」を取得するには FIRST_VALUE が必須です。企業名なしでスコアだけ必要な場合は MAX の方がシンプルです。
実務コラム:ギャップ分析と製品ロードマップへの活用
gap_to_leader を定期的(月次・四半期)に算出しトレンドを追うことで、「競合との差が縮まっているか拡大しているか」を客観データで把握できます。セキュリティで DeltaNet の gap が -36.1 という数値は「この市場に参入するには大きな改善が必要」というシグナルになります。製品開発の優先度付けや M&A 候補の評価にも、カテゴリ別ギャップ分析は直接活用されます。
QUESTION 5

シェア増減トレンド分析 — LAG × CASE × SUM OVER の CTE チェーンで勝者・敗者を分類する

LAGSUM OVERCTE チェーントレンド分類
前提知識

複数の 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;
CTE チェーンの利点:各 CTE を独立してデバッグできます。まず step1 だけを 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 昇順で返してください。

使用テーブル
► market_share_trend(12行 — 4社 × 3期)
company_nameperiodmarket_share_pct
AlphaTech2023H138.5
AlphaTech2023H240.2
AlphaTech2024H142.0
BetaSoft2023H130.1
BetaSoft2023H228.5
BetaSoft2024H126.8
DeltaNet2023H113.0
DeltaNet2023H212.3
DeltaNet2024H111.7
GammaSys2023H118.4
GammaSys2023H219.0
GammaSys2024H119.5
期待出力
company_nameperiodmarket_share_pctprev_shareshare_deltatrendexpanding_periods
AlphaTech2023H240.238.51.7▲ 拡大1
AlphaTech2024H142.040.21.8▲ 拡大2
BetaSoft2023H228.530.1-1.6▼ 縮小0
BetaSoft2024H126.828.5-1.7▼ 縮小0
DeltaNet2023H212.313.0-0.7▼ 縮小0
DeltaNet2024H111.712.3-0.6▼ 縮小0
GammaSys2023H219.018.40.6▲ 拡大1
GammaSys2024H119.519.00.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 → 並び替えて出力
*/
解説(テーブル変化・ポイント)
WITH share_with_prev AS ( SELECT company_name, period, market_share_pct, LAG(market_share_pct) OVER ( PARTITION BY company_name ORDER BY period ) AS prev_share 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 ), 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;
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行)。
1 / 4
company_nameperiodmarket_share_pct▸ prev_share
AlphaTech2023H138.5NULL
AlphaTech2023H240.238.5
AlphaTech2024H14240.2
BetaSoft2023H130.1NULL
BetaSoft2023H228.530.1
BetaSoft2024H126.828.5
DeltaNet2023H113NULL
DeltaNet2023H212.313
DeltaNet2024H111.712.3
GammaSys2023H118.4NULL
GammaSys2023H21918.4
GammaSys2024H119.519
12行 — 各社先頭期(2023H1)の prev_share が NULL
学習ポイント
CTE チェーンによるステップ分割:share_with_prev(LAG で前期値付与)→ deltas(差分計算 + 分類 + NULL除外)→ 外側クエリ(累積カウント)という 3 段階で複雑な変換を整理しています。各 CTE を単体で SELECT して動作確認できるのが、CTE チェーンの最大の実務メリットです。
SUM(CASE WHEN) OVER で「累積フラグカウント」:SUM(CASE WHEN trend='▲ 拡大' THEN 1 ELSE 0 END) OVER (PARTITION BY company_name ORDER BY period) は「その企業がその期までに拡大した期間の累積数」を計算します。ORDER BY を伴うウィンドウ SUM は累積(デフォルトフレーム)になるため、期が進むにつれてカウントが積み上がります。
WHERE IS NOT NULL で境界行を除去:CTE deltas で WHERE prev_share IS NOT NULL を指定することで、LAG が NULL を返す各社先頭期を除外します。このフィルタがないと先頭期の行で share_delta が NULL になり、trend が '─ 横ばい' に誤分類される可能性があります。
アンチパターン
PARTITION BY なしの LAG で企業をまたぐ:LAG(market_share_pct) OVER (ORDER BY period) と書くと、BetaSoft 2023H1 の「前行」が AlphaTech 2024H1 になり、企業をまたいだ前期比が計算されます。シェアが 26.8→38.5 などの意味のない差分が生じます。時系列 LAG には常に PARTITION BY が必須です。
NULL 行を除外しないと expanding_periods が誤集計:先頭期行の trend が NULL の場合、SUM(CASE WHEN trend='▲ 拡大' THEN 1 ELSE 0 END) の CASE が ELSE 0 に評価されてカウントに含まれてしまいます。WHERE で先頭期を除外することで、expanding_periods の起点を正しく設定できます。
実務コラム:市場シェアトレンド分析と競合インテリジェンス
expanding_periods = 2(2期連続拡大)か expanding_periods = 0(縮小のみ)かという分類は、競合の勢い(モメンタム)を定量化するシンプルかつ強力な指標です。実務では半期×3〜4年分のデータで「6期中何期拡大したか」を算出し、競合を「攻勢型 / 安定型 / 後退型」に分類することで、どの競合に対して防御・攻撃が必要かの優先度付けに活用されます。