市場シェア分析 — SUM() OVER(PARTITION BY) でカテゴリ別シェアを算出する
ウィンドウ関数は GROUP BY と異なり、行を圧縮せずに各行へ集計値を付与します。競合分析で必須の「カテゴリ内シェア」算出の基本形がこの構文です。
SUM(sales) OVER (PARTITION BY category) -- カテゴリ内合計を全行に付与(行数変化なし) SUM(sales) OVER () -- PARTITION BY 省略 = 全体合計
IT ソフトウェア4社の年間売上データから、カテゴリ別の市場シェア(%)を各社ごとに算出してください。取得列は category, company_name, annual_revenue, market_share_pct(小数第1位)、カテゴリ昇順→シェア降順でソートして返してください。
| company_id | company_name |
|---|---|
| 1 | AlphaTech |
| 2 | BetaSoft |
| 3 | GammaSys |
| 4 | DeltaNet |
| company_id | category | annual_revenue |
|---|---|---|
| 1 | クラウド | 4200 |
| 2 | クラウド | 2800 |
| 3 | クラウド | 1500 |
| 4 | クラウド | 500 |
| 1 | セキュリティ | 1800 |
| 2 | セキュリティ | 2200 |
| 3 | セキュリティ | 900 |
| 4 | セキュリティ | 600 |
※ annual_revenue 単位: 億円
| category | company_name | annual_revenue | market_share_pct |
|---|---|---|---|
| クラウド | AlphaTech | 4200 | 46.7 |
| クラウド | BetaSoft | 2800 | 31.1 |
| クラウド | GammaSys | 1500 | 16.7 |
| クラウド | DeltaNet | 500 | 5.6 |
| セキュリティ | BetaSoft | 2200 | 40.0 |
| セキュリティ | AlphaTech | 1800 | 32.7 |
| セキュリティ | GammaSys | 900 | 16.4 |
| セキュリティ | DeltaNet | 600 | 10.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 → 並び替えて出力 */
LEGEND
① FROM (テーブル参照)
FROM companies c, sales_data sd分析の対象となる2つのテーブルを読み込みます。左が「companies(企業マスタ)」、右が「sales_data(売上実績)」です。| company_id | company_name |
|---|---|
| 1 | AlphaTech |
| 2 | BetaSoft |
| 3 | GammaSys |
| 4 | DeltaNet |
| company_id | category | annual_revenue |
|---|---|---|
| 1 | クラウド | 4200 |
| 2 | クラウド | 2800 |
| 3 | クラウド | 1500 |
| 4 | クラウド | 500 |
| 1 | セキュリティ | 1800 |
| 2 | セキュリティ | 2200 |
| 3 | セキュリティ | 900 |
| 4 | セキュリティ | 600 |
GROUP BY category で SUM すると行が圧縮されカテゴリ合計の1行しか残りません。SUM() OVER (PARTITION BY category) は行数を保ちながら各行に集計値を付与します。「個別行の値 ÷ グループ合計」という式が同一 SELECT で書けるのはこの特性のためです。PARTITION BY sd.category を省略すると全行を1ウィンドウとして扱い、全カテゴリ合算(14,500億)に対するシェアが返ります。競合分析では「カテゴリ内シェア」と「全体シェア」を要件に応じて使い分け、PARTITION BY の有無と粒度で制御します。annual_revenue * 100.0 の 100.0 は整数を浮動小数点へアップキャストするトリックです。100(整数)のままだと DBMS によっては整数除算になり小数が切り捨てられます。PostgreSQL では ::numeric でも代替できます。GROUP BY company_name で集計した後にシェアを割り算しようとすると、分母のカテゴリ合計を同時に取得できません。サブクエリが必要になり冗長です。シェア計算は SUM() OVER が第一選択です。market_totals 参照テーブルを作成し LEFT JOIN で分母に使うパターンが一般的です。また PARTITION BY の粒度(製品カテゴリ / 地域 / 顧客セグメント / 会計期間)が分析の意味を左右するため、SQL を書く前にステークホルダーと合意することが品質確保の要です。前年同期比(YoY)分析 — CTE + LAG() で競合の成長率を比較する
LAG() はウィンドウ関数の一種で、同一パーティション内の「1行前の値」を現在行に返します。前年比・前月比など時系列の差分計算に最適です。
LAG(revenue) OVER ( PARTITION BY company_name -- 企業ごとに独立したウィンドウ ORDER BY fiscal_year -- 年度昇順で「前の行」を確定 ) AS prev_revenue -- 先頭行は NULL(前の行なし)
(当期 - 前期) / 前期 の前期が 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 昇順で返してください。
| company_name | fiscal_year | revenue |
|---|---|---|
| AlphaTech | 2022 | 320 |
| AlphaTech | 2023 | 380 |
| AlphaTech | 2024 | 420 |
| BetaSoft | 2022 | 280 |
| BetaSoft | 2023 | 260 |
| BetaSoft | 2024 | 310 |
| GammaSys | 2022 | 150 |
| GammaSys | 2023 | 180 |
| GammaSys | 2024 | 220 |
※ revenue 単位: 億円
| company_name | fiscal_year | revenue | prev_revenue | yoy_pct |
|---|---|---|---|---|
| AlphaTech | 2022 | 320 | NULL | NULL |
| AlphaTech | 2023 | 380 | 320 | 18.8 |
| AlphaTech | 2024 | 420 | 380 | 10.5 |
| BetaSoft | 2022 | 280 | NULL | NULL |
| BetaSoft | 2023 | 260 | 280 | -7.1 |
| BetaSoft | 2024 | 310 | 260 | 19.2 |
| GammaSys | 2022 | 150 | NULL | NULL |
| GammaSys | 2023 | 180 | 150 | 20.0 |
| GammaSys | 2024 | 220 | 180 | 22.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 → 並び替えて出力 */
LEGEND
① CTE: FROM annual_revenue
WITH base AS ( SELECT ... FROM annual_revenue )annual_revenue テーブルの9行を CTE として読み込みます。CTE は後続クエリで繰り返し参照できる名前付きのサブクエリです。| company_name | fiscal_year | revenue |
|---|---|---|
| AlphaTech | 2022 | 320 |
| AlphaTech | 2023 | 380 |
| AlphaTech | 2024 | 420 |
| BetaSoft | 2022 | 280 |
| BetaSoft | 2023 | 260 |
| BetaSoft | 2024 | 310 |
| GammaSys | 2022 | 150 |
| GammaSys | 2023 | 180 |
| GammaSys | 2024 | 220 |
PARTITION BY company_name がないと、AlphaTech 2023 の「前行」が全体の並び順における前の行(他社のデータ)になります。企業をまたいだ差分は意味を失います。時系列 LAG には必ず PARTITION BY を指定してください。prev_revenue という名前で CTE に切り出すことで、外側クエリで簡潔に書けます。CTE なしで書くと LAG() OVER(...) を2回書く必要があり、書き間違いと保守コストが増大します。LAG(revenue, 1) は「1行前」、LEAD(revenue, 1) は「1行後」を返します。第2引数でオフセット(デフォルト1)、第3引数でデフォルト値(先頭行の NULL を 0 にするなど)を指定できます。LAG(revenue) OVER (ORDER BY company_name, fiscal_year) と書くと、BetaSoft の先頭年の「前行」が AlphaTech の最終年になります。企業をまたいだ差分は完全に無意味なデータです。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 が標準指標として使われています。カテゴリ別ランキング — RANK() OVER(PARTITION BY) で競合の順位を付ける
順位付けのウィンドウ関数は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 昇順で返してください。
| company_name | category | avg_score | review_count |
|---|---|---|---|
| AlphaTech | クラウド | 4.3 | 1250 |
| BetaSoft | クラウド | 4.1 | 890 |
| GammaSys | クラウド | 3.8 | 430 |
| DeltaNet | クラウド | 3.5 | 210 |
| BetaSoft | セキュリティ | 4.5 | 920 |
| AlphaTech | セキュリティ | 4.0 | 680 |
| GammaSys | セキュリティ | 3.9 | 310 |
| DeltaNet | セキュリティ | 3.2 | 150 |
| category | company_name | avg_score | review_count | score_rank |
|---|---|---|---|---|
| クラウド | AlphaTech | 4.3 | 1250 | 1 |
| クラウド | BetaSoft | 4.1 | 890 | 2 |
| クラウド | GammaSys | 3.8 | 430 | 3 |
| クラウド | DeltaNet | 3.5 | 210 | 4 |
| セキュリティ | BetaSoft | 4.5 | 920 | 1 |
| セキュリティ | AlphaTech | 4.0 | 680 | 2 |
| セキュリティ | GammaSys | 3.9 | 310 | 3 |
| セキュリティ | DeltaNet | 3.2 | 150 | 4 |
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 → 並び替えて出力 */
LEGEND
① FROM competitor_scores
FROM competitor_scorescompetitor_scores テーブルの8行を読み込みます。この後 RANK() OVER が各行にカテゴリ内ランクを付与します。| company_name | category | avg_score | review_count |
|---|---|---|---|
| AlphaTech | クラウド | 4.3 | 1250 |
| BetaSoft | クラウド | 4.1 | 890 |
| GammaSys | クラウド | 3.8 | 430 |
| DeltaNet | クラウド | 3.5 | 210 |
| BetaSoft | セキュリティ | 4.5 | 920 |
| AlphaTech | セキュリティ | 4 | 680 |
| GammaSys | セキュリティ | 3.9 | 310 |
| DeltaNet | セキュリティ | 3.2 | 150 |
RANK() OVER (PARTITION BY category) と ORDER BY を省略すると、全行が同じランク(1)になります。ランク付けには ORDER BY が必須です。RANK() OVER (ORDER BY avg_score DESC) と書くと全8行を1つのウィンドウとして扱い、全カテゴリ横断のランキングになります。要件に応じて意図的に使い分けてください。WHERE RANK() OVER (...) = 1 は SQL エラーになります。ウィンドウ関数は WHERE より後に評価されるため、WHERE では直接使えません。「各カテゴリの1位のみ取得」には CTE やサブクエリで score_rank を計算してから外側で WHERE score_rank = 1 と書きます。RANK() OVER (PARTITION BY company_name ORDER BY avg_score DESC) AS company_rank と PARTITION BY を入れ替えるだけで「企業ごとのカテゴリランキング」に変換できます。これを CTE に入れ WHERE company_rank = 1 でフィルタすると、各社の得意カテゴリ一覧が簡単に作れます。PARTITION BY の軸を変えるだけで分析視点が変わる、というのがウィンドウ関数の強力な点です。競合KPIピボット集計 — CASE WHEN + GROUP BY で縦持ちデータを横展開する
データが「縦持ち(Long format)」で格納されている場合、CASE WHEN + MAX + GROUP BY の組み合わせで「横持ち(Wide format)」に変換(ピボット)できます。
| 縦持ち (Long format) | ||
|---|---|---|
| company | kpi_name | kpi_value |
| AlphaTech | 売上高(億円) | 420 |
| AlphaTech | 利益率(%) | 22.5 |
↓ CASE WHEN + GROUP BY でピボット
| 横持ち (Wide format) | ||
|---|---|---|
| company | 売上高(億円) | 利益率(%) |
| AlphaTech | 420 | 22.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 降順でソートして返してください。
| company_name | kpi_name | kpi_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_name | revenue_100m | op_margin_pct | new_customers_10k |
|---|---|---|---|
| AlphaTech | 420 | 22.5 | 3.2 |
| BetaSoft | 310 | 18.0 | 2.8 |
| GammaSys | 220 | 15.5 | 1.5 |
| DeltaNet | 95 | 8.0 | 0.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 → 並び替えて出力 */
LEGEND
① FROM competitor_kpi(縦持ち12行)
FROM competitor_kpicompetitor_kpi テーブルの12行を読み込みます。各社3行(売上高・営業利益率・顧客獲得数)の縦持ち形式です。このままでは横並びの比較が難しく、ピボット変換が必要です。| company_name | kpi_name | kpi_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 |
MAX(NULL, 420, NULL) は NULL を無視して 420 を返します。各グループに1値しか存在しないので MAX = その値になります。MIN でも動作は同じです(値が1つなので)。CASE WHEN kpi_name = '売上高(億円)' は表記ゆれ(スペース・全半角)で全行 NULL になります。実務では TRIM(kpi_name) や LOWER() で正規化するか、kpi_name に ENUM 型や外部キー制約を使って表記ゆれを防ぐ設計が重要です。tablefunc の crosstab() 関数でより簡潔なピボットが書けます。ただし出力列の定義が必要なため、KPI 種類が固定の場合は CASE WHEN + MAX の方が可読性が高いことも多いです。Redshift・BigQuery・Snowflake など分析系 DB には PIVOT 構文がありますが、値の列挙方法や動的ピボットの対応範囲は DB ごとに異なります。方言差を把握しておくと移植時の手戻りが減ります。価格帯ポジショニング分析 — NTILE() で競合製品を価格四分位に分類する
NTILE(n) はウィンドウ関数の一種で、行を ORDER BY で並べた後に n 等分のバケツに振り分けます。価格帯分析・顧客分類・売上分位など多用途に使えます。
NTILE(4) OVER ( ORDER BY monthly_price -- 価格昇順で並べて4等分 ) AS price_quartile -- 1=最安、4=最高
4社の競合製品11件から、価格を4分位(低価格帯〜高価格帯)に分類してください。サブクエリで price_quartile(NTILE値)を計算し、外側クエリで price_segment(日本語ラベル)を付与してください。取得列は product_name, company_name, monthly_price, price_quartile, price_segment、monthly_price 昇順で返してください。
| product_id | product_name | company_name | monthly_price |
|---|---|---|---|
| 1 | AlphaCloud Free | AlphaTech | 0 |
| 2 | DeltaCloud Mini | DeltaNet | 300 |
| 3 | BetaCloud Basic | BetaSoft | 500 |
| 4 | GammaCloud Lite | GammaSys | 980 |
| 5 | AlphaCloud Std | AlphaTech | 1200 |
| 6 | BetaCloud Std | BetaSoft | 2500 |
| 7 | AlphaCloud Pro | AlphaTech | 3800 |
| 8 | GammaCloud Pro | GammaSys | 4500 |
| 9 | DeltaCloud Ent | DeltaNet | 6000 |
| 10 | BetaCloud Ent | BetaSoft | 8000 |
| 11 | AlphaTech Ent | AlphaTech | 12000 |
※ monthly_price 単位: 円/月
| product_name | company_name | monthly_price | price_quartile | price_segment |
|---|---|---|---|---|
| AlphaCloud Free | AlphaTech | 0 | 1 | 低価格帯 |
| DeltaCloud Mini | DeltaNet | 300 | 1 | 低価格帯 |
| BetaCloud Basic | BetaSoft | 500 | 1 | 低価格帯 |
| GammaCloud Lite | GammaSys | 980 | 2 | 中低価格帯 |
| AlphaCloud Std | AlphaTech | 1200 | 2 | 中低価格帯 |
| BetaCloud Std | BetaSoft | 2500 | 2 | 中低価格帯 |
| AlphaCloud Pro | AlphaTech | 3800 | 3 | 中高価格帯 |
| GammaCloud Pro | GammaSys | 4500 | 3 | 中高価格帯 |
| DeltaCloud Ent | DeltaNet | 6000 | 3 | 中高価格帯 |
| BetaCloud Ent | BetaSoft | 8000 | 4 | 高価格帯 |
| AlphaTech Ent | AlphaTech | 12000 | 4 | 高価格帯 |
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. 外側クエリ */
LEGEND
① FROM competitor_products
FROM competitor_productscompetitor_products テーブルの11行を読み込みます。| product_name | company_name | monthly_price |
|---|---|---|
| AlphaCloud Free | AlphaTech | 0 |
| DeltaCloud Mini | DeltaNet | 300 |
| BetaCloud Basic | BetaSoft | 500 |
| GammaCloud Lite | GammaSys | 980 |
| AlphaCloud Std | AlphaTech | 1200 |
| BetaCloud Std | BetaSoft | 2500 |
| AlphaCloud Pro | AlphaTech | 3800 |
| GammaCloud Pro | GammaSys | 4500 |
| DeltaCloud Ent | DeltaNet | 6000 |
| BetaCloud Ent | BetaSoft | 8000 |
| AlphaTech Ent | AlphaTech | 12000 |
price_quartile を、その SELECT 句内の CASE 式から直接参照することはできないため、サブクエリで段階化します。NTILE(4) OVER(...) を CASE 式でも繰り返すより、サブクエリに切り出す方が可読性が高く保守しやすいです。CTE を使って同じ2段階構成にすることもできます。NTILE(4) OVER (PARTITION BY category ORDER BY monthly_price) とすることで「カテゴリ別の価格四分位」が計算できます。競合分析では製品カテゴリや顧客セグメントごとの価格ポジショニングを比較する場面で応用してください。CASE WHEN monthly_price < 1000 THEN '低価格帯' ... のように固定閾値を使ってください。