SQL 競合分析 — シェア・成長率・ランキングの基礎

基礎競合他社分析ウィンドウ関数PostgreSQL対応全5問
1 / 5 · 学習中 0 / 5 問完了
QUESTION 1

市場シェア分析 — SUM() OVER(PARTITION BY) でカテゴリ別シェアを算出する

SUM OVERPARTITION BY市場シェアウィンドウ関数
前提知識

ウィンドウ関数は GROUP BY と異なり、行を圧縮せずに各行へ集計値を付与します。競合分析で必須の「カテゴリ内シェア」算出の基本形がこの構文です。

SUM(sales) OVER (PARTITION BY category)  -- カテゴリ内合計を全行に付与(行数変化なし)
SUM(sales) OVER ()                          -- PARTITION BY 省略 = 全体合計
行数が変わらないことがポイント:PARTITION BY で分割しても元の行数は保持されます。結果として「個別行の値 ÷ グループ合計」という割り算が同一 SELECT で完結します。
問題

IT ソフトウェア4社の年間売上データから、カテゴリ別の市場シェア(%)を各社ごとに算出してください。取得列は category, company_name, annual_revenue, market_share_pct(小数第1位)、カテゴリ昇順→シェア降順でソートして返してください。

使用テーブル
▸ companies
company_idcompany_name
1AlphaTech
2BetaSoft
3GammaSys
4DeltaNet
▸ sales_data
company_idcategoryannual_revenue
1クラウド4200
2クラウド2800
3クラウド1500
4クラウド500
1セキュリティ1800
2セキュリティ2200
3セキュリティ900
4セキュリティ600

※ annual_revenue 単位: 億円

期待出力
categorycompany_nameannual_revenuemarket_share_pct
クラウドAlphaTech420046.7
クラウドBetaSoft280031.1
クラウドGammaSys150016.7
クラウドDeltaNet5005.6
セキュリティBetaSoft220040.0
セキュリティAlphaTech180032.7
セキュリティGammaSys90016.4
セキュリティDeltaNet60010.9
模範解答コード
SELECT
  sd.category,
  c.company_name,
  sd.annual_revenue,
  ROUND(
    sd.annual_revenue * 100.0 /
    SUM(sd.annual_revenue) OVER (PARTITION BY sd.category),  -- カテゴリ内合計を分母に
    1
  ) AS market_share_pct
FROM   companies c
JOIN   sales_data sd USING (company_id)    -- company_id で等値結合
ORDER BY sd.category, market_share_pct DESC;

/*
  実行順序(SQLの論理的な評価順):
  1. FROM companies c       → 行を読み込む
  2. JOIN sales_data sd     → 結合(一致行のみ)
  3. SUM(...) OVER (...)    → ウィンドウ関数を評価(行数は保持)
  4. ROUND(...)            → 値を整形(market_share_pct)
  5. SELECT                → 列を評価
  6. ORDER BY              → 並び替えて出力
*/
解説(テーブル変化・ポイント)
SELECT sd.category, c.company_name, sd.annual_revenue, ROUND( sd.annual_revenue * 100.0 / SUM(sd.annual_revenue) OVER ( PARTITION BY sd.category), 1 ) AS market_share_pct FROM companies c JOIN sales_data sd USING (company_id) ORDER BY sd.category, market_share_pct DESC;
LEGEND
データ取得・読込対象
① FROM (テーブル参照)
FROM companies c, sales_data sd分析の対象となる2つのテーブルを読み込みます。左が「companies(企業マスタ)」、右が「sales_data(売上実績)」です。
1 / 5
▸ companies c
company_idcompany_name
1AlphaTech
2BetaSoft
3GammaSys
4DeltaNet
▸ sales_data sd
company_idcategoryannual_revenue
1クラウド4200
2クラウド2800
3クラウド1500
4クラウド500
1セキュリティ1800
2セキュリティ2200
3セキュリティ900
4セキュリティ600
companies: 4行 / sales_data: 8行
学習ポイント
ウィンドウ関数と GROUP BY の決定的な違い:GROUP BY category で SUM すると行が圧縮されカテゴリ合計の1行しか残りません。SUM() OVER (PARTITION BY category)行数を保ちながら各行に集計値を付与します。「個別行の値 ÷ グループ合計」という式が同一 SELECT で書けるのはこの特性のためです。
PARTITION BY の粒度設計:PARTITION BY sd.category を省略すると全行を1ウィンドウとして扱い、全カテゴリ合算(14,500億)に対するシェアが返ります。競合分析では「カテゴリ内シェア」と「全体シェア」を要件に応じて使い分け、PARTITION BY の有無と粒度で制御します。
100.0 によるキャスト:annual_revenue * 100.0100.0 は整数を浮動小数点へアップキャストするトリックです。100(整数)のままだと DBMS によっては整数除算になり小数が切り捨てられます。PostgreSQL では ::numeric でも代替できます。
アンチパターン
GROUP BY でシェアを計算しようとする:GROUP BY company_name で集計した後にシェアを割り算しようとすると、分母のカテゴリ合計を同時に取得できません。サブクエリが必要になり冗長です。シェア計算は SUM() OVER が第一選択です。
分母の取り違え(PARTITION BY省略):全カテゴリ合算を分母にすると、カテゴリ間で売上規模が異なる場合に意味のないシェアが出ます。競合分析では「何に対するシェアか」が最重要な要件です。レビュー時に PARTITION BY の粒度を必ず確認しましょう。
実務コラム:市場シェア計算における分母設計
実際の競合分析では「市場全体データ」は自社 DB だけでは揃いません。Gartner・IDC のレポートや業界団体の公開データを取り込んだ market_totals 参照テーブルを作成し LEFT JOIN で分母に使うパターンが一般的です。また PARTITION BY の粒度(製品カテゴリ / 地域 / 顧客セグメント / 会計期間)が分析の意味を左右するため、SQL を書く前にステークホルダーと合意することが品質確保の要です。
QUESTION 2

前年同期比(YoY)分析 — CTE + LAG() で競合の成長率を比較する

LAGCTEYoY成長率NULLIF
前提知識

LAG() はウィンドウ関数の一種で、同一パーティション内の「1行前の値」を現在行に返します。前年比・前月比など時系列の差分計算に最適です。

LAG(revenue) OVER (
  PARTITION BY company_name   -- 企業ごとに独立したウィンドウ
  ORDER BY     fiscal_year    -- 年度昇順で「前の行」を確定
) AS prev_revenue             -- 先頭行は NULL(前の行なし)
NULLIF でゼロ除算を防ぐ:成長率計算 (当期 - 前期) / 前期 の前期が 0 の場合にゼロ除算エラーが発生します。NULLIF(prev_revenue, 0) は値が 0 のとき NULL を返し、NULL を含む除算結果は NULL(エラーなし)になります。
問題

3社の年次売上データから、各社の前年同期比成長率(YoY %)を計算してください。CTE で前年売上(prev_revenue)を付与し、外側クエリで成長率(yoy_pct 小数第1位)を算出してください。取得列は company_name, fiscal_year, revenue, prev_revenue, yoy_pct、company_name 昇順→fiscal_year 昇順で返してください。

使用テーブル
▸ annual_revenue
company_namefiscal_yearrevenue
AlphaTech2022320
AlphaTech2023380
AlphaTech2024420
BetaSoft2022280
BetaSoft2023260
BetaSoft2024310
GammaSys2022150
GammaSys2023180
GammaSys2024220

※ revenue 単位: 億円

期待出力
company_namefiscal_yearrevenueprev_revenueyoy_pct
AlphaTech2022320NULLNULL
AlphaTech202338032018.8
AlphaTech202442038010.5
BetaSoft2022280NULLNULL
BetaSoft2023260280-7.1
BetaSoft202431026019.2
GammaSys2022150NULLNULL
GammaSys202318015020.0
GammaSys202422018022.2
模範解答コード
WITH base AS (
  SELECT
    company_name,
    fiscal_year,
    revenue,
    LAG(revenue) OVER (
      PARTITION BY company_name    -- 企業ごとに独立したウィンドウ
      ORDER BY     fiscal_year     -- 年度昇順で1行前を参照
    ) AS prev_revenue               -- 各社先頭年(2022)はNULL
  FROM   annual_revenue
)
SELECT
  company_name,
  fiscal_year,
  revenue,
  prev_revenue,
  ROUND(
    (revenue - prev_revenue) * 100.0 /
    NULLIF(prev_revenue, 0),         -- prev=0のときNULLを返しゼロ除算を防ぐ
    1
  ) AS yoy_pct
FROM   base
ORDER BY company_name, fiscal_year;

/*
  実行順序(SQLの論理的な評価順):
  1. CTE base    → CTE を定義(LAG でウィンドウ関数を評価)
  2. 外側クエリ:
     FROM base    → 行を読み込む
     SELECT       → 列を評価(yoy_pct を整形)
     ORDER BY     → 並び替えて出力
*/
解説(テーブル変化・ポイント)
WITH base AS ( SELECT company_name, fiscal_year, revenue, LAG(revenue) OVER ( PARTITION BY company_name ORDER BY fiscal_year ) AS prev_revenue FROM annual_revenue ) SELECT company_name, fiscal_year, revenue, prev_revenue, ROUND( (revenue - prev_revenue) * 100.0 / NULLIF(prev_revenue, 0), 1 ) AS yoy_pct FROM base ORDER BY company_name, fiscal_year;
LEGEND
データ取得・読込対象
① CTE: FROM annual_revenue
WITH base AS ( SELECT ... FROM annual_revenue )annual_revenue テーブルの9行を CTE として読み込みます。CTE は後続クエリで繰り返し参照できる名前付きのサブクエリです。
1 / 4
company_namefiscal_yearrevenue
AlphaTech2022320
AlphaTech2023380
AlphaTech2024420
BetaSoft2022280
BetaSoft2023260
BetaSoft2024310
GammaSys2022150
GammaSys2023180
GammaSys2024220
9行読込
学習ポイント
PARTITION BY を忘れると全行が1ウィンドウになる:PARTITION BY company_name がないと、AlphaTech 2023 の「前行」が全体の並び順における前の行(他社のデータ)になります。企業をまたいだ差分は意味を失います。時系列 LAG には必ず PARTITION BY を指定してください。
CTE で可読性と再利用性を高める:LAG の値を prev_revenue という名前で CTE に切り出すことで、外側クエリで簡潔に書けます。CTE なしで書くと LAG() OVER(...) を2回書く必要があり、書き間違いと保守コストが増大します。
LEAD() との使い分け:LAG(revenue, 1) は「1行前」、LEAD(revenue, 1) は「1行後」を返します。第2引数でオフセット(デフォルト1)、第3引数でデフォルト値(先頭行の NULL を 0 にするなど)を指定できます。
アンチパターン
PARTITION BY を省略した全体 LAG:LAG(revenue) OVER (ORDER BY company_name, fiscal_year) と書くと、BetaSoft の先頭年の「前行」が AlphaTech の最終年になります。企業をまたいだ差分は完全に無意味なデータです。
NULLIF を省略してゼロ除算:前期売上が 0 の行(新規参入企業の初年度など)で NULLIF がないと DIVISION BY ZERO エラーで全クエリが失敗します。ゼロ除算ガードとして NULLIF は必ずセットで書くべきです。
実務コラム:YoY 以外の成長率指標(CAGR)
前年比(YoY)は1年単位の変動を捉えますが、競合比較では年平均成長率(CAGR)もよく使われます。計算式は POWER(最終値 / 初期値, 1.0 / 年数) - 1 です。SQL では POWER(MAX(revenue) FILTER(WHERE fiscal_year=2024) / MIN(revenue) FILTER(WHERE fiscal_year=2022), 1.0/2) - 1 のように書けます。単年の外れ値に影響されず長期トレンドを比較できるため、IR 資料や戦略分析では CAGR が標準指標として使われています。
QUESTION 3

カテゴリ別ランキング — RANK() OVER(PARTITION BY) で競合の順位を付ける

RANKDENSE_RANK順位付け競合スコア
前提知識

順位付けのウィンドウ関数は3種類あり、同率(タイ)の扱いが異なります

関数同率の扱い例(同率2位が2名)
RANK()同じランクを付け次を飛ばす1, 2, 2, 4
DENSE_RANK()同じランクを付け次を飛ばさない1, 2, 2, 3
ROW_NUMBER()同率でも強制的に連番を振る1, 2, 3, 4
RANK() OVER (
  PARTITION BY category      -- カテゴリごとに1位からリセット
  ORDER BY     avg_score DESC  -- スコア降順でランク付け
) AS score_rank
問題

カテゴリ別・企業別の顧客満足度スコアから、カテゴリ内のスコアランキング(score_rankを付けてください。取得列は category, company_name, avg_score, review_count, score_rank、カテゴリ昇順→score_rank 昇順で返してください。

使用テーブル
▸ competitor_scores
company_namecategoryavg_scorereview_count
AlphaTechクラウド4.31250
BetaSoftクラウド4.1890
GammaSysクラウド3.8430
DeltaNetクラウド3.5210
BetaSoftセキュリティ4.5920
AlphaTechセキュリティ4.0680
GammaSysセキュリティ3.9310
DeltaNetセキュリティ3.2150
期待出力
categorycompany_nameavg_scorereview_countscore_rank
クラウドAlphaTech4.312501
クラウドBetaSoft4.18902
クラウドGammaSys3.84303
クラウドDeltaNet3.52104
セキュリティBetaSoft4.59201
セキュリティAlphaTech4.06802
セキュリティGammaSys3.93103
セキュリティDeltaNet3.21504
模範解答コード
SELECT
  category,
  company_name,
  avg_score,
  review_count,
  RANK() OVER (
    PARTITION BY category        -- カテゴリごとに1位からリセット
    ORDER BY     avg_score DESC  -- スコア高い順に1,2,3,...を付与
  ) AS score_rank
FROM   competitor_scores
ORDER BY category, score_rank;

/*
  実行順序(SQLの論理的な評価順):
  1. FROM competitor_scores  → 行を読み込む
  2. RANK() OVER (...)       → ウィンドウ関数を評価(行数は保持)
  3. SELECT                  → 列を評価
  4. ORDER BY                → 並び替えて出力
*/
解説(テーブル変化・ポイント)
SELECT category, company_name, avg_score, review_count, RANK() OVER ( PARTITION BY category ORDER BY avg_score DESC ) AS score_rank FROM competitor_scores ORDER BY category, score_rank;
LEGEND
データ取得・読込対象
① FROM competitor_scores
FROM competitor_scorescompetitor_scores テーブルの8行を読み込みます。この後 RANK() OVER が各行にカテゴリ内ランクを付与します。
1 / 4
company_namecategoryavg_scorereview_count
AlphaTechクラウド4.31250
BetaSoftクラウド4.1890
GammaSysクラウド3.8430
DeltaNetクラウド3.5210
BetaSoftセキュリティ4.5920
AlphaTechセキュリティ4680
GammaSysセキュリティ3.9310
DeltaNetセキュリティ3.2150
8行読込
学習ポイント
RANK / DENSE_RANK / ROW_NUMBER の使い分け:同率が発生する可能性がある列(スコア・売上など)に対し、「3位が2社いるなら次は5位」→ RANK「3位が2社いても次は4位」→ DENSE_RANK、「強制的に順番をつけたい(社内管理番号など)」→ ROW_NUMBER が適切です。競合ランキングでは一般に DENSE_RANK が使いやすいです。
ORDER BY を省略すると全行が同一ランク:RANK() OVER (PARTITION BY category) と ORDER BY を省略すると、全行が同じランク(1)になります。ランク付けには ORDER BY が必須です。
PARTITION BY なしの全体ランキング:RANK() OVER (ORDER BY avg_score DESC) と書くと全8行を1つのウィンドウとして扱い、全カテゴリ横断のランキングになります。要件に応じて意図的に使い分けてください。
アンチパターン
RANK を WHERE で直接フィルタしようとする:WHERE RANK() OVER (...) = 1 は SQL エラーになります。ウィンドウ関数は WHERE より後に評価されるため、WHERE では直接使えません。「各カテゴリの1位のみ取得」には CTE やサブクエリで score_rank を計算してから外側で WHERE score_rank = 1 と書きます。
同率の扱いを考慮しない:avg_score に同点が多い実データで RANK を使うと「1,2,2,4」と3位が消えます。ビジネス要件として「3位も必要」なら DENSE_RANK を選択してください。テストデータで同率がなくても、本番データで初めて問題が発覚するケースが多いです。
実務コラム:PARTITION BY 軸の切り替え(各社の上位カテゴリを特定)
カテゴリ別ランキングで「各社が最も強いカテゴリ」を抽出する場合、RANK() OVER (PARTITION BY company_name ORDER BY avg_score DESC) AS company_rank と PARTITION BY を入れ替えるだけで「企業ごとのカテゴリランキング」に変換できます。これを CTE に入れ WHERE company_rank = 1 でフィルタすると、各社の得意カテゴリ一覧が簡単に作れます。PARTITION BY の軸を変えるだけで分析視点が変わる、というのがウィンドウ関数の強力な点です。
QUESTION 4

競合KPIピボット集計 — CASE WHEN + GROUP BY で縦持ちデータを横展開する

CASE WHENGROUP BYピボット集計縦持ち→横持ち
前提知識

データが「縦持ち(Long format)」で格納されている場合、CASE WHEN + MAX + GROUP BY の組み合わせで「横持ち(Wide format)」に変換(ピボット)できます。

縦持ち (Long format)
companykpi_namekpi_value
AlphaTech売上高(億円)420
AlphaTech利益率(%)22.5

↓ CASE WHEN + GROUP BY でピボット

横持ち (Wide format)
company売上高(億円)利益率(%)
AlphaTech42022.5
MAX(CASE WHEN kpi_name = '売上高(億円)' THEN kpi_value END) AS revenue_100m
問題

competitor_kpi テーブル(縦持ち)から、各社のKPIを横展開してください。取得列は company_name, revenue_100m, op_margin_pct, new_customers_10k、revenue_100m 降順でソートして返してください。

使用テーブル
▸ competitor_kpi(縦持ち形式 — 12行)
company_namekpi_namekpi_value
AlphaTech売上高(億円)420
AlphaTech営業利益率(%)22.5
AlphaTech顧客獲得数(万件)3.2
BetaSoft売上高(億円)310
BetaSoft営業利益率(%)18.0
BetaSoft顧客獲得数(万件)2.8
GammaSys売上高(億円)220
GammaSys営業利益率(%)15.5
GammaSys顧客獲得数(万件)1.5
DeltaNet売上高(億円)95
DeltaNet営業利益率(%)8.0
DeltaNet顧客獲得数(万件)0.7
期待出力
company_namerevenue_100mop_margin_pctnew_customers_10k
AlphaTech42022.53.2
BetaSoft31018.02.8
GammaSys22015.51.5
DeltaNet958.00.7
模範解答コード
SELECT
  company_name,
  MAX(CASE WHEN kpi_name = '売上高(億円)'    THEN kpi_value END) AS revenue_100m,  -- 縦持ちを横持ちにピボット
  MAX(CASE WHEN kpi_name = '営業利益率(%)'   THEN kpi_value END) AS op_margin_pct,
  MAX(CASE WHEN kpi_name = '顧客獲得数(万件)' THEN kpi_value END) AS new_customers_10k
FROM   competitor_kpi
GROUP BY company_name
ORDER BY revenue_100m DESC;

/*
  実行順序(SQLの論理的な評価順):
  1. FROM competitor_kpi         → 縦持ちデータを読み込む
  2. GROUP BY company_name       → 会社ごとにグループ化
  3. CASE + MAX(...)             → ピボット(横持ちに変換)
  4. SELECT                      → 列を選択
  5. ORDER BY revenue_100m DESC  → 並び替えて出力
  */
解説(テーブル変化・ポイント)
SELECT company_name, MAX(CASE WHEN kpi_name = '売上高(億円)' THEN kpi_value END) AS revenue_100m, MAX(CASE WHEN kpi_name = '営業利益率(%)' THEN kpi_value END) AS op_margin_pct, MAX(CASE WHEN kpi_name = '顧客獲得数(万件)' THEN kpi_value END) AS new_customers_10k FROM competitor_kpi GROUP BY company_name ORDER BY revenue_100m DESC;
LEGEND
データ取得・読込対象
① FROM competitor_kpi(縦持ち12行)
FROM competitor_kpicompetitor_kpi テーブルの12行を読み込みます。各社3行(売上高・営業利益率・顧客獲得数)の縦持ち形式です。このままでは横並びの比較が難しく、ピボット変換が必要です。
1 / 4
company_namekpi_namekpi_value
AlphaTech売上高(億円)420
AlphaTech営業利益率(%)22.5
AlphaTech顧客獲得数(万件)3.2
BetaSoft売上高(億円)310
BetaSoft営業利益率(%)18
BetaSoft顧客獲得数(万件)2.8
GammaSys売上高(億円)220
GammaSys営業利益率(%)15.5
GammaSys顧客獲得数(万件)1.5
DeltaNet売上高(億円)95
DeltaNet営業利益率(%)8
DeltaNet顧客獲得数(万件)0.7
縦持ち: 12行
学習ポイント
なぜ MAX を使うのか:CASE WHEN で1グループあたり1行だけ非NULL値が入り、残り2行は NULL になります。MAX(NULL, 420, NULL) は NULL を無視して 420 を返します。各グループに1値しか存在しないので MAX = その値になります。MIN でも動作は同じです(値が1つなので)。
縦持ち↔横持ちの使い分け:縦持ちはKPI種類の追加が容易(行追加だけ)ですが、横方向の比較が SQL で難しくなります。競合ダッシュボードや分析レポートでは横持ちが見やすいため、縦持ちで蓄積→横持ちにピボットして分析というパターンが一般的です。
kpi_name の完全一致に注意:CASE WHEN kpi_name = '売上高(億円)' は表記ゆれ(スペース・全半角)で全行 NULL になります。実務では TRIM(kpi_name)LOWER() で正規化するか、kpi_name に ENUM 型や外部キー制約を使って表記ゆれを防ぐ設計が重要です。
アンチパターン
MAX の代わりに SUM を使う:各グループに1値しかないので理論上は SUM でも同じ結果になりますが、データ品質の問題で同一 kpi_name が複数行ある場合に合計値になってしまいます。意図を明確にするため MAX を使うのが正しい実践です。
GROUP BY を忘れた場合:company_name を SELECT したまま GROUP BY を省略すると、非集約列を選択しているため PostgreSQL ではエラーになります。company_name まで除けば全12行が1グループになり、各 MAX は全社の最大値を返します。会社別の CASE WHEN ピボットには GROUP BY が必須です。
実務コラム:PostgreSQL の crosstab() と方言差
PostgreSQL では拡張モジュール tablefunc の crosstab() 関数でより簡潔なピボットが書けます。ただし出力列の定義が必要なため、KPI 種類が固定の場合は CASE WHEN + MAX の方が可読性が高いことも多いです。Redshift・BigQuery・Snowflake など分析系 DB には PIVOT 構文がありますが、値の列挙方法や動的ピボットの対応範囲は DB ごとに異なります。方言差を把握しておくと移植時の手戻りが減ります。
QUESTION 5

価格帯ポジショニング分析 — NTILE() で競合製品を価格四分位に分類する

NTILEサブクエリ価格ポジショニング四分位分析
前提知識

NTILE(n) はウィンドウ関数の一種で、行を ORDER BY で並べた後に n 等分のバケツに振り分けます。価格帯分析・顧客分類・売上分位など多用途に使えます。

NTILE(4) OVER (
  ORDER BY monthly_price    -- 価格昇順で並べて4等分
) AS price_quartile         -- 1=最安、4=最高
余り行の配分ルール:行数が n で割り切れない場合、余り行は先頭のバケツから順に1行ずつ割り振られます。11行÷4 = 2余り3 → バケツ1,2,3が3行、バケツ4が2行になります。
問題

4社の競合製品11件から、価格を4分位(低価格帯〜高価格帯)に分類してください。サブクエリで price_quartile(NTILE値)を計算し、外側クエリで price_segment(日本語ラベル)を付与してください。取得列は product_name, company_name, monthly_price, price_quartile, price_segment、monthly_price 昇順で返してください。

使用テーブル
▸ competitor_products
product_idproduct_namecompany_namemonthly_price
1AlphaCloud FreeAlphaTech0
2DeltaCloud MiniDeltaNet300
3BetaCloud BasicBetaSoft500
4GammaCloud LiteGammaSys980
5AlphaCloud StdAlphaTech1200
6BetaCloud StdBetaSoft2500
7AlphaCloud ProAlphaTech3800
8GammaCloud ProGammaSys4500
9DeltaCloud EntDeltaNet6000
10BetaCloud EntBetaSoft8000
11AlphaTech EntAlphaTech12000

※ monthly_price 単位: 円/月

期待出力
product_namecompany_namemonthly_priceprice_quartileprice_segment
AlphaCloud FreeAlphaTech01低価格帯
DeltaCloud MiniDeltaNet3001低価格帯
BetaCloud BasicBetaSoft5001低価格帯
GammaCloud LiteGammaSys9802中低価格帯
AlphaCloud StdAlphaTech12002中低価格帯
BetaCloud StdBetaSoft25002中低価格帯
AlphaCloud ProAlphaTech38003中高価格帯
GammaCloud ProGammaSys45003中高価格帯
DeltaCloud EntDeltaNet60003中高価格帯
BetaCloud EntBetaSoft80004高価格帯
AlphaTech EntAlphaTech120004高価格帯
模範解答コード
SELECT
  product_name,
  company_name,
  monthly_price,
  price_quartile,
  CASE price_quartile
    WHEN 1 THEN '低価格帯'     -- Q1: 最も安いグループ
    WHEN 2 THEN '中低価格帯'
    WHEN 3 THEN '中高価格帯'
    WHEN 4 THEN '高価格帯'    -- Q4: 最も高いグループ
  END AS price_segment
FROM (
  SELECT
    product_name,
    company_name,
    monthly_price,
    NTILE(4) OVER (
      ORDER BY monthly_price  -- 価格昇順で4等分に振り分け
    ) AS price_quartile
  FROM   competitor_products
) sub
ORDER BY monthly_price;

/*
  実行順序(SQLの論理的な評価順):
  1. サブクエリ sub
  2. 外側クエリ
  */
解説(テーブル変化・ポイント)
SELECT product_name, company_name, monthly_price, price_quartile, CASE price_quartile WHEN 1 THEN '低価格帯' WHEN 2 THEN '中低価格帯' WHEN 3 THEN '中高価格帯' WHEN 4 THEN '高価格帯' END AS price_segment FROM ( SELECT product_name, company_name, monthly_price, NTILE(4) OVER ( ORDER BY monthly_price ) AS price_quartile FROM competitor_products ) sub ORDER BY monthly_price;
LEGEND
データ取得・読込対象
① FROM competitor_products
FROM competitor_productscompetitor_products テーブルの11行を読み込みます。
1 / 4
product_namecompany_namemonthly_price
AlphaCloud FreeAlphaTech0
DeltaCloud MiniDeltaNet300
BetaCloud BasicBetaSoft500
GammaCloud LiteGammaSys980
AlphaCloud StdAlphaTech1200
BetaCloud StdBetaSoft2500
AlphaCloud ProAlphaTech3800
GammaCloud ProGammaSys4500
DeltaCloud EntDeltaNet6000
BetaCloud EntBetaSoft8000
AlphaTech EntAlphaTech12000
11行読込
学習ポイント
NTILE の余り行配分ルール:11行を4等分すると 11÷4=2余り3 になります。余り行は先頭バケツから順に1行ずつ追加されるため、バケツ1・2・3が3行、バケツ4が2行になります。この配分ロジックを知らないと、バケツごとの行数が異なる理由が説明できません。
サブクエリで NTILE 結果を再利用:同じ SELECT 句の別名 price_quartile を、その SELECT 句内の CASE 式から直接参照することはできないため、サブクエリで段階化します。NTILE(4) OVER(...) を CASE 式でも繰り返すより、サブクエリに切り出す方が可読性が高く保守しやすいです。CTE を使って同じ2段階構成にすることもできます。
PARTITION BY との組み合わせ:NTILE(4) OVER (PARTITION BY category ORDER BY monthly_price) とすることで「カテゴリ別の価格四分位」が計算できます。競合分析では製品カテゴリや顧客セグメントごとの価格ポジショニングを比較する場面で応用してください。
アンチパターン
同一価格が多い場合の振る舞い:価格が同一の製品が多い場合、NTILE は同じ価格の製品を異なるバケツに分けることがあります(ROW_NUMBER 的な連番で等分するため)。同率を同じバケツにしたい場合は PERCENT_RANK() や CUME_DIST() を検討してください。
NTILE のバケツ番号は順序を表すだけ:price_quartile=2 が price_quartile=1 の「2倍高い」という意味ではありません。バケツ番号はあくまで相対的な順序(第1四分位・第2四分位…)です。絶対的な価格閾値でセグメントを区切りたい場合は CASE WHEN monthly_price < 1000 THEN '低価格帯' ... のように固定閾値を使ってください。
実務コラム:価格ポジショニング分析の実務応用
本問のような四分位分析は、競合製品ポートフォリオの可視化に直結します。「自社製品が特定価格帯に集中していないか」「競合が手薄な価格帯はどこか」を把握することで、新製品の価格設定の根拠になります。また NTILE を使った顧客 LTV 四分位(上位25%の優良顧客特定)、広告費対効果の四分位分析なども同じ構文パターンで実装できます。競合分析・顧客分析を問わず、分布を等分割して比較するという思考はビジネス SQL の基本テクニックです。