アンチジョイン — LEFT JOIN + IS 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 (subquery) はサブクエリ結果に NULL が含まれると全行が除外される罠があります。LEFT JOIN + IS NULL は NULL-safe でパフォーマンスも安定しており、実務でより信頼性が高いです。競合3社が参入している市場のうち、自社(our_markets)が未参入の「空白市場」を会社名・市場IDともに列挙してください。取得列は company_name, market_id, market_name、company_name 昇順 → market_id 昇順で返してください。
| market_id | market_name |
|---|---|
| 1 | 国内ERP |
| 2 | 国内CRM |
| 3 | AI/ML |
| 4 | IoT |
| 5 | データ分析 |
| market_id |
|---|
| 1 |
| 2 |
| company_name | market_id |
|---|---|
| BetaSoft | 1 |
| BetaSoft | 2 |
| BetaSoft | 3 |
| GammaSys | 2 |
| GammaSys | 4 |
| GammaSys | 5 |
| DeltaNet | 3 |
| DeltaNet | 4 |
| company_name | market_id | market_name |
|---|---|---|
| BetaSoft | 3 | AI/ML |
| DeltaNet | 3 | AI/ML |
| DeltaNet | 4 | IoT |
| GammaSys | 4 | IoT |
| GammaSys | 5 | データ分析 |
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 → 並び替えて出力 */
LEGEND
① FROM + JOIN markets
FROM competitor_markets cm JOIN markets m USING (market_id)competitor_markets(8行)を基点に markets と INNER JOIN して市場名を付与します。全8行が残り、各行に market_name が追加されます。| company_name | market_id | market_name |
|---|---|---|
| BetaSoft | 1 | 国内ERP |
| BetaSoft | 2 | 国内CRM |
| BetaSoft | 3 | AI/ML |
| GammaSys | 2 | 国内CRM |
| GammaSys | 4 | IoT |
| GammaSys | 5 | データ分析 |
| DeltaNet | 3 | AI/ML |
| DeltaNet | 4 | IoT |
WHERE market_id NOT IN (SELECT market_id FROM our_markets) でも同じ結果が得られますが、our_markets に NULL 行が混入した場合 NOT IN は全行を除外してしまいます。LEFT JOIN + IS NULL は NULL-safe で、実データの汚れに強いです。WHERE NOT EXISTS (SELECT 1 FROM our_markets om WHERE om.market_id = cm.market_id) も等価で、大規模テーブルではクエリプランナーが最適化しやすい場合があります。可読性では LEFT JOIN + IS NULL、明示性では NOT EXISTS が好まれます。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 を選択してください。移動平均分析 — ROWS BETWEEN で競合の四半期売上トレンドを平滑化する
ウィンドウフレーム(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行) |
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 昇順で返してください。
| company_name | quarter | revenue |
|---|---|---|
| AlphaTech | 2023Q1 | 300 |
| AlphaTech | 2023Q2 | 360 |
| AlphaTech | 2023Q3 | 280 |
| AlphaTech | 2023Q4 | 400 |
| BetaSoft | 2023Q1 | 250 |
| BetaSoft | 2023Q2 | 220 |
| BetaSoft | 2023Q3 | 290 |
| BetaSoft | 2023Q4 | 310 |
| GammaSys | 2023Q1 | 120 |
| GammaSys | 2023Q2 | 150 |
| GammaSys | 2023Q3 | 170 |
| GammaSys | 2023Q4 | 160 |
※ revenue 単位: 億円
| company_name | quarter | revenue | moving_avg_3q |
|---|---|---|---|
| AlphaTech | 2023Q1 | 300 | 300.0 |
| AlphaTech | 2023Q2 | 360 | 330.0 |
| AlphaTech | 2023Q3 | 280 | 313.3 |
| AlphaTech | 2023Q4 | 400 | 346.7 |
| BetaSoft | 2023Q1 | 250 | 250.0 |
| BetaSoft | 2023Q2 | 220 | 235.0 |
| BetaSoft | 2023Q3 | 290 | 253.3 |
| BetaSoft | 2023Q4 | 310 | 273.3 |
| GammaSys | 2023Q1 | 120 | 120.0 |
| GammaSys | 2023Q2 | 150 | 135.0 |
| GammaSys | 2023Q3 | 170 | 146.7 |
| GammaSys | 2023Q4 | 160 | 160.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 → 並び替えて出力 */
LEGEND
① FROM quarterly_revenue
FROM quarterly_revenuequarterly_revenue テーブルの12行(3社×4四半期)を読み込みます。この後 PARTITION BY で企業別ウィンドウに分割し、ROWS BETWEEN で移動平均を算出します。| company_name | quarter | revenue |
|---|---|---|
| AlphaTech | 2023Q1 | 300 |
| AlphaTech | 2023Q2 | 360 |
| AlphaTech | 2023Q3 | 280 |
| AlphaTech | 2023Q4 | 400 |
| BetaSoft | 2023Q1 | 250 |
| BetaSoft | 2023Q2 | 220 |
| BetaSoft | 2023Q3 | 290 |
| BetaSoft | 2023Q4 | 310 |
| GammaSys | 2023Q1 | 120 |
| GammaSys | 2023Q2 | 150 |
| GammaSys | 2023Q3 | 170 |
| GammaSys | 2023Q4 | 160 |
ROWS BETWEEN は物理的な行数でフレームを確定します。RANGE BETWEEN は値の範囲で確定するため、同値の行を同一フレームに含める場合があります。移動平均には常に ROWS を使用してください。RANGE では同一 quarter の複数行が意図せず合算されることがあります。2 PRECEDING(前2行+現在行=3行)です。月次データで12ヶ月移動平均なら 11 PRECEDING を指定します。「N行の移動平均」は「(N-1) PRECEDING AND CURRENT ROW」と覚えてください。OVER (PARTITION BY company_name ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) は ORDER BY がないため「現在行」が不定となり、移動平均の結果が不定になります。ROWS BETWEEN には必ず ORDER BY をセットで指定してください。UNBOUNDED PRECEDING AND CURRENT ROW(先頭〜現在行の累積)になります。移動平均と累積集計は全く別の計算です。意図しない累積値が出力されていないか必ず検証してください。ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING(前後対称フレーム)は中央値的な平滑化に使え、イベント効果の前後比較にも応用できます。HAVING 複合条件フィルタ — 複数KPI基準を全て満たす競合を一括スクリーニングする
HAVING は GROUP BY で集約した後の集計結果に対してフィルタをかける句です。WHERE が集約前の行レベルのフィルタであるのに対し、HAVING は集約後のグループレベルのフィルタです。
SELECT company_name, SUM(revenue) FROM data WHERE year = 2024 -- ① 行レベルフィルタ(集約前) GROUP BY company_name HAVING SUM(revenue) > 1000 -- ② グループレベルフィルタ(集約後)
競合3社の四半期実績データから、「年間売上合計 1,000億以上」「平均解約率 5.0% 以下」「年間新規顧客獲得数 50万件以上」の3条件を全て満たす競合を抽出してください。取得列は company_name, total_revenue, avg_churn_pct, total_new_customers、total_revenue 降順で返してください。
| company_name | quarter | revenue | new_customers | churn_rate_pct |
|---|---|---|---|---|
| AlphaTech | 2023Q1 | 320 | 15 | 3.2 |
| AlphaTech | 2023Q2 | 360 | 18 | 2.8 |
| AlphaTech | 2023Q3 | 280 | 12 | 4.1 |
| AlphaTech | 2023Q4 | 400 | 20 | 3.5 |
| BetaSoft | 2023Q1 | 250 | 10 | 6.2 |
| BetaSoft | 2023Q2 | 220 | 8 | 7.0 |
| BetaSoft | 2023Q3 | 290 | 11 | 5.8 |
| BetaSoft | 2023Q4 | 310 | 13 | 6.5 |
| GammaSys | 2023Q1 | 120 | 8 | 4.5 |
| GammaSys | 2023Q2 | 150 | 10 | 3.8 |
| GammaSys | 2023Q3 | 170 | 12 | 3.2 |
| GammaSys | 2023Q4 | 160 | 9 | 4.0 |
| company_name | total_revenue | avg_churn_pct | total_new_customers |
|---|---|---|---|
| AlphaTech | 1360 | 3.4 | 65 |
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 → 並び替えて出力 */
LEGEND
① FROM competitor_metrics(12行)
FROM competitor_metricscompetitor_metrics テーブルの12行(3社×4四半期)を読み込みます。この後 GROUP BY で3社グループに集約し、HAVING で条件フィルタをかけます。| company_name | quarter | revenue | new_customers | churn_rate_pct |
|---|---|---|---|---|
| AlphaTech | 2023Q1 | 320 | 15 | 3.2 |
| AlphaTech | 2023Q2 | 360 | 18 | 2.8 |
| AlphaTech | 2023Q3 | 280 | 12 | 4.1 |
| AlphaTech | 2023Q4 | 400 | 20 | 3.5 |
| BetaSoft | 2023Q1 | 250 | 10 | 6.2 |
| BetaSoft | 2023Q2 | 220 | 8 | 7 |
| BetaSoft | 2023Q3 | 290 | 11 | 5.8 |
| BetaSoft | 2023Q4 | 310 | 13 | 6.5 |
| GammaSys | 2023Q1 | 120 | 8 | 4.5 |
| GammaSys | 2023Q2 | 150 | 10 | 3.8 |
| GammaSys | 2023Q3 | 170 | 12 | 3.2 |
| GammaSys | 2023Q4 | 160 | 9 | 4 |
HAVING total_revenue >= 1000 はエラーになります。HAVING には集計関数式を直接書くか、エイリアスを使いたい場合は集約をサブクエリや CTE に分けて外側の WHERE で絞り込んでください。WHERE SUM(revenue) >= 1000 は SQL 文法エラーです。集計関数は WHERE では使用できません。集約後の条件は必ず HAVING に書いてください。CROSS JOIN + COALESCE — 全社×全カテゴリ比較マトリクスの欠損を 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(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 昇順で返してください。
| company_id | company_name |
|---|---|
| 1 | AlphaTech |
| 2 | BetaSoft |
| 3 | GammaSys |
| category_id | category_name |
|---|---|
| 1 | クラウド |
| 2 | セキュリティ |
| 3 | AI/ML |
| company_id | category_id | annual_revenue |
|---|---|---|
| 1 | 1 | 4200 |
| 1 | 2 | 1800 |
| 2 | 1 | 2800 |
| 2 | 3 | 1500 |
| 3 | 2 | 900 |
| 3 | 3 | 650 |
| company_name | category_name | annual_revenue |
|---|---|---|
| AlphaTech | クラウド | 4200 |
| AlphaTech | セキュリティ | 1800 |
| AlphaTech | AI/ML | 0 |
| BetaSoft | クラウド | 2800 |
| BetaSoft | セキュリティ | 0 |
| BetaSoft | AI/ML | 1500 |
| GammaSys | クラウド | 0 |
| GammaSys | セキュリティ | 900 |
| GammaSys | AI/ML | 650 |
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 → 並び替えて出力 */
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)ペアが行として存在します。| company_id | company_name | category_id | category_name |
|---|---|---|---|
| 1 | AlphaTech | 1 | クラウド |
| 1 | AlphaTech | 2 | セキュリティ |
| 1 | AlphaTech | 3 | AI/ML |
| 2 | BetaSoft | 1 | クラウド |
| 2 | BetaSoft | 2 | セキュリティ |
| 2 | BetaSoft | 3 | AI/ML |
| 3 | GammaSys | 1 | クラウド |
| 3 | GammaSys | 2 | セキュリティ |
| 3 | GammaSys | 3 | AI/ML |
COALESCE(a, b, c) は a→b→c の順に左から非NULL値を返します。COALESCE(sd.annual_revenue, 0) は「実績があれば実績値、なければ0」を意味します。ISNULL(SQL Server方言)や NVL(Oracle方言)の代わりに、標準 SQL の COALESCE を使うとポータビリティが高くなります。FROM companies, categories, sales_data は3テーブルの直積(3×3×6=54行)が発生します。JOIN には必ず結合条件を明示してください。AVG(NULL, NULL, 900) = 900。0 として平均したい場合とは結果が異なります)。集計前に COALESCE で明示的に 0 変換するかどうかをビジネス要件に合わせて選択してください。累積シェア分析 — SUM() OVER(ORDER BY) でパレート原則(80/20ルール)を検証する
累積集計(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 降順で返してください。
| company_name | annual_revenue |
|---|---|
| AlphaTech | 4200 |
| BetaSoft | 2800 |
| GammaSys | 1500 |
| DeltaNet | 500 |
| EpsilonSys | 300 |
| ZetaCloud | 200 |
| company_name | annual_revenue | cumulative_revenue | cumulative_share_pct |
|---|---|---|---|
| AlphaTech | 4200 | 4200 | 44.2 |
| BetaSoft | 2800 | 7000 | 73.7 |
| GammaSys | 1500 | 8500 | 89.5 |
| DeltaNet | 500 | 9000 | 94.7 |
| EpsilonSys | 300 | 9300 | 97.9 |
| ZetaCloud | 200 | 9500 | 100.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 → 並び替えて出力 */
LEGEND
① FROM competitor_revenue
FROM competitor_revenuecompetitor_revenue テーブルの6行を読み込みます。この後 SUM() OVER () で全体合計を、SUM() OVER (ORDER BY DESC ROWS...) で累積合計を各行に付与します。| company_name | annual_revenue |
|---|---|
| AlphaTech | 4200 |
| BetaSoft | 2800 |
| GammaSys | 1500 |
| DeltaNet | 500 |
| EpsilonSys | 300 |
| ZetaCloud | 200 |
SUM(x) OVER () は全行合計(固定値)を全行に付与します。SUM(x) OVER (ORDER BY x DESC ROWS UNBOUNDED...) は行ごとに異なる累積値を返します。前者が「分母」、後者が「分子(累積値)」という役割で組み合わせることで累積シェアが一発で計算できます。RANGE 相当です。ROWS を明示すると物理行単位の累積という意図が明確になり、同額売上の行がある場合もフレームの意味を取り違えにくくなります。SUM(annual_revenue) OVER (ORDER BY annual_revenue DESC) と書くと累積されますが、同値レコードが複数ある場合に「現在行と同値の全行を現在フレームに含める」RANGE 動作になり、意図しない累積値が出ることがあります。ROWS BETWEEN を明示して RANGE と ROWS を混同しないようにしてください。FROM t a JOIN t b ON b.revenue >= a.revenue)や複数のサブクエリが必要になり、パフォーマンスが大幅に悪化します。累積集計はウィンドウ関数が第一選択です。cumulative_share_pct と直前行の値を使って到達境界を判定します。