SQL 競合分析 — Anti Join・移動平均・累積シェアの基礎

基礎競合他社分析アンチジョイン移動平均HAVING / CROSS JOIN累積シェアPostgreSQL対応全5問
1 / 5 · 学習中 0 / 5 問完了
QUESTION 6

アンチジョイン — LEFT JOIN + IS NULL で競合参入済み・自社未参入の空白市場を発見する

LEFT JOINIS NULL空白市場発見アンチジョイン
前提知識

アンチジョイン(Anti-JOIN)は「Aにはあって、Bにはない」レコードを取得するパターンです。LEFT JOIN でマッチしなかった行は結合キー列が NULL になるという性質を WHERE で絞り込みます。

-- アンチジョインの基本形
SELECT a.*
FROM   table_a a
LEFT JOIN table_b b ON a.key = b.key   -- 存在しない行はb列がNULL
WHERE  b.key IS NULL;                  -- NULL = Bに存在しない行のみ
NOT IN との違い:NOT IN (subquery) はサブクエリ結果に NULL が含まれると全行が除外される罠があります。LEFT JOIN + IS NULL は NULL-safe でパフォーマンスも安定しており、実務でより信頼性が高いです。
問題

競合3社が参入している市場のうち、自社(our_markets)が未参入の「空白市場」を会社名・市場IDともに列挙してください。取得列は company_name, market_id, market_name、company_name 昇順 → market_id 昇順で返してください。

使用テーブル
▸ markets
market_idmarket_name
1国内ERP
2国内CRM
3AI/ML
4IoT
5データ分析
▸ our_markets(自社参入済み)
market_id
1
2
▸ competitor_markets(競合参入市場)
company_namemarket_id
BetaSoft1
BetaSoft2
BetaSoft3
GammaSys2
GammaSys4
GammaSys5
DeltaNet3
DeltaNet4
期待出力
company_namemarket_idmarket_name
BetaSoft3AI/ML
DeltaNet3AI/ML
DeltaNet4IoT
GammaSys4IoT
GammaSys5データ分析
模範解答コード
SELECT
  cm.company_name,
  m.market_id,
  m.market_name
FROM   competitor_markets cm
JOIN   markets m         USING (market_id)   -- 市場名を付与
LEFT JOIN our_markets om  USING (market_id)  -- 自社参入済みと左結合
WHERE  om.market_id IS NULL                  -- NULLの行 = 自社が未参入
ORDER BY cm.company_name, m.market_id;

/*
  実行順序(SQLの論理的な評価順):
  1. FROM competitor_markets cm        → 行を読み込む
  2. JOIN markets m                    → 結合(一致行のみ)
  3. LEFT JOIN our_markets om          → 結合(左表を全行保持)
  4. WHERE om.market_id IS NULL        → 行を絞り込む
  5. SELECT                            → 列を評価
  6. ORDER BY company_name, market_id  → 並び替えて出力
*/
解説(テーブル変化・ポイント)
SELECT cm.company_name, m.market_id, m.market_name FROM competitor_markets cm JOIN markets m USING (market_id) LEFT JOIN our_markets om USING (market_id) WHERE om.market_id IS NULL ORDER BY cm.company_name, m.market_id;
LEGEND
データ取得・読込対象
① FROM + JOIN markets
FROM competitor_markets cm JOIN markets m USING (market_id)competitor_markets(8行)を基点に markets と INNER JOIN して市場名を付与します。全8行が残り、各行に market_name が追加されます。
1 / 4
company_namemarket_idmarket_name
BetaSoft1国内ERP
BetaSoft2国内CRM
BetaSoft3AI/ML
GammaSys2国内CRM
GammaSys4IoT
GammaSys5データ分析
DeltaNet3AI/ML
DeltaNet4IoT
JOIN後: 8行
学習ポイント
LEFT JOIN の NULL = 「存在しないこと」の証明:INNER JOIN は一致した行だけを返しますが、LEFT JOIN は左テーブルの全行を保ちつつ、右テーブルと一致しない行の右側列を NULL にします。「NULL = 右テーブルに存在しなかった」という解釈がアンチジョインの核心です。
NOT IN との決定的な違い:WHERE market_id NOT IN (SELECT market_id FROM our_markets) でも同じ結果が得られますが、our_markets に NULL 行が混入した場合 NOT IN は全行を除外してしまいます。LEFT JOIN + IS NULL は NULL-safe で、実データの汚れに強いです。
NOT EXISTS との使い分け:WHERE NOT EXISTS (SELECT 1 FROM our_markets om WHERE om.market_id = cm.market_id) も等価で、大規模テーブルではクエリプランナーが最適化しやすい場合があります。可読性では LEFT JOIN + IS NULL、明示性では NOT EXISTS が好まれます。
アンチパターン
NOT IN にサブクエリを使う際の NULL トラップ:WHERE market_id NOT IN (SELECT market_id FROM our_markets) は our_markets.market_id に NULL が1行でも含まれると、全行が除外されます(NULL との比較は常に UNKNOWN)。本番データでは NOT IN より LEFT JOIN + IS NULL を選択してください。
INNER JOIN との混同:INNER JOIN に変えると「自社・競合ともに参入している市場」を返してしまいます。アンチジョインには必ずLEFT JOIN + WHERE 右テーブルのキー IS NULLのセットが必要です。WHERE を忘れるとただの LEFT JOIN になります。
実務コラム:市場空白地帯分析の実務応用
アンチジョインは競合分析の最頻出パターンの一つです。「競合が取り込んでいる顧客で自社が失注した案件」「競合が対応している機能で自社製品にない機能」「競合が出稿している広告媒体で自社が未出稿のもの」など、差集合(差分)を見ることで戦略的優先度が明確になります。EXCEPT 演算子も同じ差集合操作ですが、SELECT 列数とデータ型が一致している必要があり、JOIN パターンより制約が多いです。
QUESTION 7

移動平均分析 — ROWS BETWEEN で競合の四半期売上トレンドを平滑化する

ROWS BETWEENAVG OVER移動平均ウィンドウフレーム
前提知識

ウィンドウフレーム(ROWS BETWEEN)は、OVER(ORDER BY ...) に追加することで集計する行の範囲を制御します。移動平均の実装に不可欠です。

AVG(revenue) OVER (
  PARTITION BY company_name
  ORDER BY     quarter
  ROWS BETWEEN 2 PRECEDING AND CURRENT ROW  -- 直近3行の平均
)
フレーム指定意味
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW現在行 + 前2行(計3行)
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW先頭行〜現在行(累積)
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING前1行・現在行・後1行(計3行)
デフォルトフレームの落とし穴:ORDER BY を指定したウィンドウのデフォルトフレームは、同順位の行も現在行のフレームに含める RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW 相当です。移動平均には必ず ROWS BETWEEN N PRECEDING AND CURRENT ROW を明示してください。
問題

3社の四半期売上データから、各社の3四半期移動平均(moving_avg_3q、小数第1位)を算出してください。取得列は company_name, quarter, revenue, moving_avg_3q、company_name 昇順 → quarter 昇順で返してください。

使用テーブル
▸ quarterly_revenue
company_namequarterrevenue
AlphaTech2023Q1300
AlphaTech2023Q2360
AlphaTech2023Q3280
AlphaTech2023Q4400
BetaSoft2023Q1250
BetaSoft2023Q2220
BetaSoft2023Q3290
BetaSoft2023Q4310
GammaSys2023Q1120
GammaSys2023Q2150
GammaSys2023Q3170
GammaSys2023Q4160

※ revenue 単位: 億円

期待出力
company_namequarterrevenuemoving_avg_3q
AlphaTech2023Q1300300.0
AlphaTech2023Q2360330.0
AlphaTech2023Q3280313.3
AlphaTech2023Q4400346.7
BetaSoft2023Q1250250.0
BetaSoft2023Q2220235.0
BetaSoft2023Q3290253.3
BetaSoft2023Q4310273.3
GammaSys2023Q1120120.0
GammaSys2023Q2150135.0
GammaSys2023Q3170146.7
GammaSys2023Q4160160.0
模範解答コード
SELECT
  company_name,
  quarter,
  revenue,
  ROUND(
    AVG(revenue) OVER (
      PARTITION BY company_name            -- 企業ごとに独立したウィンドウ
      ORDER BY     quarter               -- 四半期昇順でフレームの「現在行」を定義
      ROWS BETWEEN 2 PRECEDING AND CURRENT ROW  -- 直近3四半期を対象
    ), 1
  ) AS moving_avg_3q
FROM   quarterly_revenue
ORDER BY company_name, quarter;

/*
  実行順序(SQLの論理的な評価順):
  1. FROM quarterly_revenue          → 行を読み込む
  2. AVG(revenue) OVER (...)         → ウィンドウ関数を評価(行数は保持)
  3. SELECT                          → 列を評価(moving_avg_3q)
  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 FROM quarterly_revenue ORDER BY company_name, quarter;
LEGEND
データ取得・読込対象
① FROM quarterly_revenue
FROM quarterly_revenuequarterly_revenue テーブルの12行(3社×4四半期)を読み込みます。この後 PARTITION BY で企業別ウィンドウに分割し、ROWS BETWEEN で移動平均を算出します。
1 / 4
company_namequarterrevenue
AlphaTech2023Q1300
AlphaTech2023Q2360
AlphaTech2023Q3280
AlphaTech2023Q4400
BetaSoft2023Q1250
BetaSoft2023Q2220
BetaSoft2023Q3290
BetaSoft2023Q4310
GammaSys2023Q1120
GammaSys2023Q2150
GammaSys2023Q3170
GammaSys2023Q4160
12行読込
学習ポイント
フレームが縮小される先頭行の挙動:Q1(先頭行)の「2行前」は存在しないため、フレームは1行のみになります。AVG(300) = 300.0 です。ROWS BETWEEN は実在する行のみでフレームを構成し、存在しない行を 0 として扱いません。この挙動はシーズンスタート時・新規競合の初期データで重要です。
ROWS vs RANGE の違い:ROWS BETWEEN は物理的な行数でフレームを確定します。RANGE BETWEEN は値の範囲で確定するため、同値の行を同一フレームに含める場合があります。移動平均には常に ROWS を使用してください。RANGE では同一 quarter の複数行が意図せず合算されることがあります。
N の選択基準:3四半期移動平均は 2 PRECEDING(前2行+現在行=3行)です。月次データで12ヶ月移動平均なら 11 PRECEDING を指定します。「N行の移動平均」は「(N-1) PRECEDING AND CURRENT ROW」と覚えてください。
アンチパターン
ORDER BY なしで ROWS BETWEEN を書く:OVER (PARTITION BY company_name ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) は ORDER BY がないため「現在行」が不定となり、移動平均の結果が不定になります。ROWS BETWEEN には必ず ORDER BY をセットで指定してください。
ROWS BETWEEN を省略した累積集計との混同:ORDER BY だけ書いて ROWS BETWEEN を省略すると、デフォルトのフレーム UNBOUNDED PRECEDING AND CURRENT ROW(先頭〜現在行の累積)になります。移動平均と累積集計は全く別の計算です。意図しない累積値が出力されていないか必ず検証してください。
実務コラム:競合トレンド分析での移動平均の使い所
四半期売上は季節変動・キャンペーン効果・決算期のずれなどで短期的に上下します。移動平均で平滑化することで基調トレンドが見えやすくなり、「競合が本当に成長しているのか、それとも一時的なノイズか」の判断が可能になります。競合比較では各社の移動平均線を重ねて可視化し、交差点(追い抜き・逆転)を検出するのが一般的なダッシュボード設計です。また ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING(前後対称フレーム)は中央値的な平滑化に使え、イベント効果の前後比較にも応用できます。
QUESTION 8

HAVING 複合条件フィルタ — 複数KPI基準を全て満たす競合を一括スクリーニングする

GROUP BYHAVING競合スクリーニング複合集計条件
前提知識

HAVING は GROUP BY で集約した後の集計結果に対してフィルタをかける句です。WHERE が集約前の行レベルのフィルタであるのに対し、HAVING は集約後のグループレベルのフィルタです。

SELECT   company_name, SUM(revenue)
FROM     data
WHERE    year = 2024         -- ① 行レベルフィルタ(集約前)
GROUP BY company_name
HAVING   SUM(revenue) > 1000  -- ② グループレベルフィルタ(集約後)
複数条件の AND:HAVING 句には AND / OR で複数の集計条件を組み合わせられます。「売上合計かつ平均解約率かつ顧客獲得数」をまとめてフィルタできます。
問題

競合3社の四半期実績データから、「年間売上合計 1,000億以上」「平均解約率 5.0% 以下」「年間新規顧客獲得数 50万件以上」の3条件を全て満たす競合を抽出してください。取得列は company_name, total_revenue, avg_churn_pct, total_new_customers、total_revenue 降順で返してください。

使用テーブル
▸ competitor_metrics(単位: revenue=億円, new_customers=万件)
company_namequarterrevenuenew_customerschurn_rate_pct
AlphaTech2023Q1320153.2
AlphaTech2023Q2360182.8
AlphaTech2023Q3280124.1
AlphaTech2023Q4400203.5
BetaSoft2023Q1250106.2
BetaSoft2023Q222087.0
BetaSoft2023Q3290115.8
BetaSoft2023Q4310136.5
GammaSys2023Q112084.5
GammaSys2023Q2150103.8
GammaSys2023Q3170123.2
GammaSys2023Q416094.0
期待出力
company_nametotal_revenueavg_churn_pcttotal_new_customers
AlphaTech13603.465
模範解答コード
SELECT
  company_name,
  SUM(revenue)                    AS total_revenue,
  ROUND(AVG(churn_rate_pct), 1)  AS avg_churn_pct,
  SUM(new_customers)              AS total_new_customers
FROM   competitor_metrics
GROUP BY company_name
HAVING   SUM(revenue)          >= 1000   -- 条件①: 年間売上1,000億以上
     AND AVG(churn_rate_pct)   <=    5.0  -- 条件②: 平均解約率5.0%以下
     AND SUM(new_customers)    >=   50   -- 条件③: 年間新規顧客50万件以上
ORDER BY total_revenue DESC;

/*
  実行順序(SQLの論理的な評価順):
  1. FROM competitor_metrics      → 行を読み込む
  2. GROUP BY company_name        → グループ化
  3. SUM/AVG                      → 集計関数を評価
  4. HAVING(3条件)              → グループを絞り込む
  5. SELECT                       → 列を評価
  6. ORDER BY total_revenue DESC  → 並び替えて出力
*/
解説(テーブル変化・ポイント)
SELECT company_name, SUM(revenue) AS total_revenue, ROUND(AVG(churn_rate_pct), 1) AS avg_churn_pct, SUM(new_customers) AS total_new_customers FROM competitor_metrics GROUP BY company_name HAVING SUM(revenue) >= 1000 AND AVG(churn_rate_pct) <= 5.0 AND SUM(new_customers) >= 50 ORDER BY total_revenue DESC;
LEGEND
データ取得・読込対象
① FROM competitor_metrics(12行)
FROM competitor_metricscompetitor_metrics テーブルの12行(3社×4四半期)を読み込みます。この後 GROUP BY で3社グループに集約し、HAVING で条件フィルタをかけます。
1 / 4
company_namequarterrevenuenew_customerschurn_rate_pct
AlphaTech2023Q1320153.2
AlphaTech2023Q2360182.8
AlphaTech2023Q3280124.1
AlphaTech2023Q4400203.5
BetaSoft2023Q1250106.2
BetaSoft2023Q222087
BetaSoft2023Q3290115.8
BetaSoft2023Q4310136.5
GammaSys2023Q112084.5
GammaSys2023Q2150103.8
GammaSys2023Q3170123.2
GammaSys2023Q416094
12行読込(3社×4四半期)
学習ポイント
WHERE と HAVING の評価タイミングの違い:WHERE は GROUP BY 前に行単位で評価されます。集計関数(SUM、AVG など)は使えません。HAVING は GROUP BY 後にグループ単位で評価され、集計関数が使えます。「四半期ごとに 200 億以上の行だけを含めたい」なら WHERE、「年間合計が 1,000 億以上の企業だけを出したい」なら HAVING です。
AND 条件の評価順に依存しない:AND で繋いだ HAVING 条件は全て真のグループだけを残しますが、SQL は記述順での評価や短絡評価を保証しません。DB のオプティマイザは条件を並べ替えることがあります。性能やエラー回避を左からの評価順に依存させず、各条件を単独でも安全な式として記述してください。
SELECT エイリアスは HAVING で使えない:標準 SQL と PostgreSQL では、HAVING 句から同じ SELECT リストのエイリアス(total_revenue など)を参照できません。HAVING total_revenue >= 1000 はエラーになります。HAVING には集計関数式を直接書くか、エイリアスを使いたい場合は集約をサブクエリや CTE に分けて外側の WHERE で絞り込んでください。
アンチパターン
WHERE に集計関数を書く:WHERE SUM(revenue) >= 1000 は SQL 文法エラーです。集計関数は WHERE では使用できません。集約後の条件は必ず HAVING に書いてください。
HAVING だけで事前フィルタを代替する:例えば「2023年だけのデータで集計したい」場合、WHERE を使わず HAVING で代替すると全期間のデータを読み込んでから除外するため非効率です。行レベルの絞り込みには WHERE、集約後の絞り込みには HAVINGを使い分けてください。
実務コラム:競合スクリーニングの実務応用
複合 HAVING によるスクリーニングは、投資対象・提携先・ベンチマーク候補の自動抽出に直結します。「売上成長率 20%超 かつ 利益率 15%超 かつ 顧客数 100万以上」のような複数財務指標を AND で連結するパターンは、株式スクリーニング・競合 KPI モニタリングダッシュボードで頻繁に使われます。条件を可変にしたい場合はパラメータ化(プレースホルダ)と組み合わせ、ユーザーが閾値をダイナミックに変更できるインタラクティブフィルタとして実装するのが実務の定石です。
QUESTION 9

CROSS JOIN + COALESCE — 全社×全カテゴリ比較マトリクスの欠損を 0 で補完する

CROSS JOINCOALESCEマトリクス補完NULL→0変換
前提知識

CROSS JOIN は左右テーブルの全行の組み合わせ(直積)を生成します。「全社×全カテゴリ」など、欠損なしのマトリクスを作りたいときに LEFT JOIN と組み合わせて使います。

SELECT c.company_name, cat.category_name,
       COALESCE(sd.revenue, 0) AS revenue   -- NULLを0に置換
FROM   companies   c
CROSS JOIN categories cat              -- 全組み合わせ生成(3社×3カテゴリ=9行)
LEFT JOIN  sales_data sd
         ON sd.company_id  = c.company_id
        AND sd.category_id = cat.category_id  -- 実績がなければNULL
COALESCE の使い方:COALESCE(expr1, expr2, ...) は左から評価して最初の非NULL値を返します。COALESCE(sd.annual_revenue, 0) は annual_revenue が NULL(= 未参入)のときに 0 を返し、比較可能な数値に変換します。
問題

3社の売上実績テーブルから、全社×全カテゴリのマトリクスを生成し、未参入カテゴリを 0 で補完してください。取得列は company_name, category_name, annual_revenue(欠損は0)、company_name 昇順 → category_id 昇順で返してください。

使用テーブル
▸ companies
company_idcompany_name
1AlphaTech
2BetaSoft
3GammaSys
▸ categories
category_idcategory_name
1クラウド
2セキュリティ
3AI/ML
▸ sales_data(実績6行のみ)
company_idcategory_idannual_revenue
114200
121800
212800
231500
32900
33650
期待出力
company_namecategory_nameannual_revenue
AlphaTechクラウド4200
AlphaTechセキュリティ1800
AlphaTechAI/ML0
BetaSoftクラウド2800
BetaSoftセキュリティ0
BetaSoftAI/ML1500
GammaSysクラウド0
GammaSysセキュリティ900
GammaSysAI/ML650
模範解答コード
SELECT
  c.company_name,
  cat.category_name,
  COALESCE(sd.annual_revenue, 0) AS annual_revenue  -- NULLを0に置換
FROM       companies   c
CROSS JOIN categories  cat                     -- 3社×3カテゴリ=9通りの全組み合わせ
LEFT JOIN  sales_data  sd
           ON  sd.company_id  = c.company_id
           AND sd.category_id = cat.category_id  -- 両キーで照合
ORDER BY c.company_name, cat.category_id;

/*
  実行順序(SQLの論理的な評価順):
  1. FROM companies c                    → 行を読み込む
  2. CROSS JOIN categories cat           → 結合(直積)
  3. LEFT JOIN sales_data sd             → 結合(左表を全行保持)
  4. SELECT                              → 列を評価(annual_revenue を整形)
  5. ORDER BY company_name, category_id  → 並び替えて出力
*/
解説(テーブル変化・ポイント)
SELECT c.company_name, cat.category_name, COALESCE(sd.annual_revenue, 0) AS annual_revenue FROM companies c CROSS JOIN categories cat LEFT JOIN sales_data sd ON sd.company_id = c.company_id AND sd.category_id = cat.category_id ORDER BY c.company_name, cat.category_id;
LEGEND
データ取得・読込対象
① CROSS JOIN companies × categories → 9通りの全組み合わせ
FROM companies c CROSS JOIN categories catcompanies(3行)と categories(3行)を CROSS JOIN します。3×3=9通りの全組み合わせが生成されます。実績の有無に関係なく、全ての(company_id, category_id)ペアが行として存在します。
1 / 4
company_idcompany_namecategory_idcategory_name
1AlphaTech1クラウド
1AlphaTech2セキュリティ
1AlphaTech3AI/ML
2BetaSoft1クラウド
2BetaSoft2セキュリティ
2BetaSoft3AI/ML
3GammaSys1クラウド
3GammaSys2セキュリティ
3GammaSys3AI/ML
CROSS JOIN後: 9行(全組み合わせ)
学習ポイント
CROSS JOIN の行数計算:CROSS JOIN は左テーブル行数 × 右テーブル行数の行を生成します。3社×3カテゴリ=9行。100社×50カテゴリなら5,000行です。大規模テーブルへの CROSS JOIN は爆発的に行数が増加するため、必要な組み合わせを事前に WHERE や IN で絞ってから CROSS JOIN するか、CTE で制限してください。
COALESCE の多段フォールバック:COALESCE(a, b, c) は a→b→c の順に左から非NULL値を返します。COALESCE(sd.annual_revenue, 0) は「実績があれば実績値、なければ0」を意味します。ISNULL(SQL Server方言)や NVL(Oracle方言)の代わりに、標準 SQL の COALESCE を使うとポータビリティが高くなります。
複合キー JOIN の ON 句:USING は片方のテーブルにしかない列名や複数列への対応が制限される場合があります。CROSS JOIN 後の LEFT JOIN では両テーブルの列を明示的に ON 句で指定するのが確実です。ON 句の AND で複数列を指定することで、company_id と category_id の両方が一致する行のみを結合できます。
アンチパターン
CROSS JOIN を WHERE で絞ることを忘れる(意図しない直積):2つのテーブルを FROM に並べて JOIN 条件を書き忘れると、暗黙的な CROSS JOIN になります。例: FROM companies, categories, sales_data は3テーブルの直積(3×3×6=54行)が発生します。JOIN には必ず結合条件を明示してください。
NULL のまま集計する:COALESCE で 0 に変換せず NULL のまま SUM や AVG にかけると、NULL は計算から除外されます(AVG(NULL, NULL, 900) = 900。0 として平均したい場合とは結果が異なります)。集計前に COALESCE で明示的に 0 変換するかどうかをビジネス要件に合わせて選択してください。
実務コラム:競合比較マトリクスの実務応用
「全社×全カテゴリ」の完全マトリクスは、ヒートマップ・バブルチャートへの入力データとして BI ツール(Tableau・Looker・Power BI)に渡すのに最適な形式です。0 に補完されたセルは「未参入の空白地帯」を視覚的に浮かび上がらせ、新規参入候補の優先度付けに使えます。また CROSS JOIN パターンは日付マスタとの組み合わせでも使われます。date_series CROSS JOIN companies で「全日×全社」の骨格を作り、LEFT JOIN で実績を付与することで欠損のない日次トレンド分析テーブルを構築できます。
QUESTION 10

累積シェア分析 — SUM() OVER(ORDER BY) でパレート原則(80/20ルール)を検証する

ROWS UNBOUNDEDSUM OVER累積シェアパレート分析
前提知識

累積集計(Running Total)は、SUM() OVER (ORDER BY ...) にウィンドウフレーム ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW を指定することで実現します。大きい順に並べた累積シェアはパレート分析の基礎です。

SUM(metric_value) OVER (
  ORDER BY metric_value DESC
  ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW  -- 先頭〜現在行の累積
) AS running_value
全体合計との比率:SUM(metric_value) OVER ()(PARTITION BY も ORDER BY もなし)は全行を1ウィンドウとして合計を返します。これを分母にして累積値 ÷ 全体合計 × 100 でパーセンタイルを算出します。
問題

IT 競合6社の年間売上から、売上降順に並べた累積売上(cumulative_revenue)と累積シェア(cumulative_share_pct、小数第1位)を算出してください。「上位N社で市場の何%を占めるか」を可視化するパレート分析です。取得列は company_name, annual_revenue, cumulative_revenue, cumulative_share_pct、annual_revenue 降順で返してください。

使用テーブル
▸ competitor_revenue
company_nameannual_revenue
AlphaTech4200
BetaSoft2800
GammaSys1500
DeltaNet500
EpsilonSys300
ZetaCloud200
期待出力
company_nameannual_revenuecumulative_revenuecumulative_share_pct
AlphaTech4200420044.2
BetaSoft2800700073.7
GammaSys1500850089.5
DeltaNet500900094.7
EpsilonSys300930097.9
ZetaCloud2009500100.0
模範解答コード
SELECT
  company_name,
  annual_revenue,
  SUM(annual_revenue) OVER (
    ORDER BY annual_revenue DESC
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW  -- 先頭〜現在行の累積
  ) AS cumulative_revenue,
  ROUND(
    SUM(annual_revenue) OVER (
      ORDER BY annual_revenue DESC
      ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) * 100.0 /
    SUM(annual_revenue) OVER (),                      -- 分母: 全体合計(OVER()=全行)
    1
  ) AS cumulative_share_pct
FROM   competitor_revenue
ORDER BY annual_revenue DESC;

/*
  実行順序(SQLの論理的な評価順):
  1. FROM competitor_revenue       → 行を読み込む
  2. SUM(...) OVER ()              → 全体合計を全行に付与
  3. SUM(...) OVER (...)           → 累積集計(Running Total)
  4. 累積値 / 全体合計                    → 累積シェア(%)を算出
  5. SELECT                        → 列を選択
  6. ORDER BY annual_revenue DESC  → 並び替えて出力
  */
解説(テーブル変化・ポイント)
SELECT company_name, annual_revenue, SUM(annual_revenue) OVER ( ORDER BY annual_revenue DESC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS cumulative_revenue, ROUND( SUM(annual_revenue) OVER ( ORDER BY annual_revenue DESC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) * 100.0 / SUM(annual_revenue) OVER (), 1 ) AS cumulative_share_pct FROM competitor_revenue ORDER BY annual_revenue DESC;
LEGEND
データ取得・読込対象
① FROM competitor_revenue
FROM competitor_revenuecompetitor_revenue テーブルの6行を読み込みます。この後 SUM() OVER () で全体合計を、SUM() OVER (ORDER BY DESC ROWS...) で累積合計を各行に付与します。
1 / 4
company_nameannual_revenue
AlphaTech4200
BetaSoft2800
GammaSys1500
DeltaNet500
EpsilonSys300
ZetaCloud200
6行読込
学習ポイント
OVER() と OVER(ORDER BY) の違い:SUM(x) OVER () は全行合計(固定値)を全行に付与します。SUM(x) OVER (ORDER BY x DESC ROWS UNBOUNDED...) は行ごとに異なる累積値を返します。前者が「分母」、後者が「分子(累積値)」という役割で組み合わせることで累積シェアが一発で計算できます。
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW を明示する理由:ORDER BY を指定したウィンドウのデフォルトフレームは、同順位の行も現在行までに含める RANGE 相当です。ROWS を明示すると物理行単位の累積という意図が明確になり、同額売上の行がある場合もフレームの意味を取り違えにくくなります。
CTE で重複を排除する書き方:本問では SUM() OVER (...ROWS...) を2回書いています。CTE を使えば一度だけ書いてエイリアスで参照でき、保守性が上がります。複雑なウィンドウ関数式の重複は CTE で解消するのが実務のベストプラクティスです。
アンチパターン
ROWS を省略してデフォルトフレームに任せる:SUM(annual_revenue) OVER (ORDER BY annual_revenue DESC) と書くと累積されますが、同値レコードが複数ある場合に「現在行と同値の全行を現在フレームに含める」RANGE 動作になり、意図しない累積値が出ることがあります。ROWS BETWEEN を明示して RANGE と ROWS を混同しないようにしてください。
累積シェアをサブクエリで二段計算する:SUM() OVER なしで累積シェアを計算しようとすると、自己結合(FROM t a JOIN t b ON b.revenue >= a.revenue)や複数のサブクエリが必要になり、パフォーマンスが大幅に悪化します。累積集計はウィンドウ関数が第一選択です。
実務コラム:パレート分析と80/20ルールの競合戦略への応用
本問の結果では上位3社が市場の89.5%を占め、3社目で累積シェアが80%を超えます。競合分析でのパレート活用例は多岐にわたります。「上位何%の競合が市場の80%を持つか」を把握することで、リソース集中投資先の絞り込みができます。同様に「上位20%の顧客が売上の80%を生む」「上位20%の機能が利用の80%を占める」という仮説も同じ SQL パターンで検証できます。「80%到達までの上位N社」を自動抽出するには、この集計を CTE やサブクエリにして、外側のクエリで cumulative_share_pct と直前行の値を使って到達境界を判定します。