SQL NULL — 集計NULL・NULLIF・ゼロ除算の応用

応用NULL応用NOT IN / NOT EXISTSCOUNT / AVG 分母問題LEFT JOIN 暗黙変換NULLIF / ゼロ除算PostgreSQL対応全5問
1 / 5 · 学習中 0 / 5 問完了
QUESTION 6

NOT IN と NULL サブクエリの罠 — サブクエリに NULL が 1 つあるだけで全行がUNKNOWNで消える

NOT IN / NOT EXISTS相関サブクエリUNKNOWN 全除外NULL安全比較
前提知識

col NOT IN (a, b, c) は内部的に col <> a AND col <> b AND col <> c と等価です。リストに NULL が 1 つ含まれるだけで、col <> NULL → UNKNOWN となり AND チェーンが UNKNOWN に伝播して、全行が除外されます

-- リストに NULL があると UNKNOWN が伝播し、全行が除外される
col NOT IN (10, 30, NULL)
-- 内部展開: col <> 10 AND col <> 30 AND col <> NULL (← UNKNOWN)

-- 解決策①: NOT EXISTS (NULL の影響を受けない)
WHERE NOT EXISTS (SELECT 1 FROM sub WHERE sub.col = t.col)

-- 解決策②: サブクエリから NULL を除外
WHERE col NOT IN (SELECT col FROM sub WHERE col IS NOT NULL)
NULL 安全な除外の原則:サブクエリを使う NOT IN では、サブクエリが NULL を返す可能性がないか必ず確認してください。NOT EXISTS は相関サブクエリが「行を返さないかどうか」を判定するため NULL の影響を受けません。データが増えて NULL が混入した瞬間に全件消える「タイムボム」になります。
問題

employees テーブルから、管理対象外の部署(managed_depts に存在しない dept_id)の従業員を抽出してください。出力列は emp_id, name, dept_id、emp_id 昇順で返してください。

使用テーブル
► employees(6行)
emp_idnamedept_id
1田中10
2鈴木20
3佐藤30
4伊藤40
5山田NULL
6高橋20
► managed_depts(3行)
dept_iddept_name
10営業
30人事
NULL未定義
期待出力
emp_idnamedept_id
2鈴木20
4伊藤40
5山田NULL
6高橋20
QUESTION 7

集計関数と NULL の落とし穴 — COUNT(*) / COUNT(列) / AVG の分母が知らず縮む

COUNT / AVGNULL集計除外分母縮小問題COALESCE補正
前提知識

集計関数は NULL を自動的に無視します。COUNT(*) だけが全行をカウントし、COUNT(col)SUM(col)AVG(col) は NULL の行を除外します。特に AVG の分母縮小は「実績あり担当者だけの平均」が「全員平均」として報告される静かなバグになります。

-- 挙動の違い
COUNT(*)                  -- 全行カウント(NULL含む)
COUNT(amount)             -- 非NULL行のみカウント

-- AVGは NULL が除外され分母が縮む (SUM / COUNT(amount) と等価)
AVG(amount)

-- NULLを0として扱う全員平均
AVG(COALESCE(amount, 0))
AVG の「見えない分母縮小」:amount=NULL(実績なし)を AVG が除外すると、「実績ある担当者だけの平均」が「全担当者の平均」として報告されてしまいます。NULL が「0(実績なし)」を意味するなら COALESCE で補正した true_avg を、「欠損(集計不能)」を意味するなら AVG のまま使うか明示的に件数を表示してください。
問題

sales_results テーブルから、部署(dept)ごとの担当者数・実績あり担当者数・実績ありの平均売上・0実績を含む全員平均を集計してください。出力列は dept, head_count, active_reps, avg_amount, true_avg、dept 昇順で返してください。

使用テーブル
► sales_results(8行)
rep_iddeptamount
1東京250000
2東京300000
3東京NULL
4東京NULL
5大阪150000
6大阪200000
7大阪180000
8大阪NULL
期待出力
depthead_countactive_repsavg_amounttrue_avg
大阪43176667132500
東京42275000137500
QUESTION 8

LEFT JOIN の ON 句フィルタ — 非マッチ行を NULL で保持する挙動

LEFT JOINON 句フィルタ結合時の条件評価非マッチ行の NULL 保持
前提知識

LEFT JOIN の目的は「左テーブルの全行を保持すること」です。右テーブルの列に対する条件(例:特定の金額以上の注文など)を指定しつつ、条件に合わない左テーブルの行も NULL として保持したい場合は、ON 句にフィルタ条件を記述します。

-- ON 句に書けば、非マッチ行も NULL として保持される
FROM customers c
LEFT JOIN orders o
       ON c.customer_id = o.customer_id
      AND o.amount > 20000
ON 句の評価タイミング:ON 句は結合時に評価され、条件を満たさない行は結合されず、左テーブルの行が NULL と結合されて保持されます。右テーブルをフィルタして「条件に合わない行も NULL で表示したい」場合は ON 句に条件を書いてください。
問題

customers テーブルの全顧客について、金額が 20,000 円を超える注文(大口注文)の最大金額(large_order)を表示してください。大口注文がない顧客または注文自体がない顧客は large_order を NULL で表示してください。出力列は customer_id, customer_name, large_order、customer_id 昇順で返してください。

使用テーブル
► customers(4行)
customer_idcustomer_name
1田中商事
2鈴木物産
3佐藤商会
4新規顧客
► orders(7行)
order_idcustomer_idamount
1110000
2135000
3140000
4228000
528000
635000
7312000
期待出力
customer_idcustomer_namelarge_order
1田中商事40000
2鈴木物産28000
3佐藤商会NULL
4新規顧客NULL
QUESTION 9

NULLIF でゼロ除算を NULL に変える — NULL を防御的に武器として使う

NULLIFゼロ除算防止NULL を正常値として活用COALESCE との組み合わせ
前提知識

NULLIF(expr, value)expr = value のとき NULL を返し、そうでなければ expr をそのまま返す関数です。ゼロ除算防止に使うことで、エラーではなく NULL を安全に返せます。

-- 基本: a = b なら NULL、それ以外は a を返す
NULLIF(a, b)

-- ゼロ除算防止 (units=0 の場合 NULL を返し、結果も NULL になる)
revenue / NULLIF(units, 0)

-- ゼロ除算時に 0 を返したい場合
COALESCE(revenue / NULLIF(units, 0), 0)
NULL を武器として使う:NULL は「不明」を意味しますが、NULLIF でゼロを NULL に変換することで「ゼロ除算を安全にスキップする」という積極的な活用法があります。NULL が伝播して結果も NULL になることを「意図的に利用する」設計パターンです。
問題

campaign_daily テーブルの各行(日次・キャンペーン単位のレコード)について、クリック数・コンバージョン率(conv_rate = conversions / clicks)・クリック単価(rev_per_click = revenue / clicks)を計算して出力してください。clicks が 0 の行は conv_rate / rev_per_click を NULL で表示してください(ゼロ除算を NULLIF で防ぐこと)。出力列は date, campaign, clicks, conv_rate, rev_per_click、date・campaign 昇順で返してください。

使用テーブル
► campaign_daily(6行)
datecampaignclicksconversionsrevenue
2024-03-01CP_A100050200000
2024-03-01CP_B000
2024-03-02CP_A80032160000
2024-03-02CP_B50020100000
2024-03-03CP_A000
2024-03-03CP_B60024120000
期待出力
datecampaignclicksconv_raterev_per_click
2024-03-01CP_A10000.0500200.0000
2024-03-01CP_B0NULLNULL
2024-03-02CP_A8000.0400200.0000
2024-03-02CP_B5000.0400200.0000
2024-03-03CP_A0NULLNULL
2024-03-03CP_B6000.0400200.0000
QUESTION 10

ウィンドウ関数と NULL 伝播 — LAG の境界 NULL と前月比の連鎖 NULL

LAG / ウィンドウ関数NULL 伝播チェーンCTE パターン前月比・時系列分析
前提知識

LAG(col, offset, default) は前の行の値を返しますが、指定した列自体が NULL の場合と、最初の行で前の行が存在しない場合(境界 NULL)の2つの NULL の発生源があります。これらを区別し、意図しない NULL 伝播を防ぐ設計が必要です。

-- ① 境界 NULL のみ防ぐ(最初の行は 0 になるが、前行が NULL の場合は NULL が返る)
LAG(revenue, 1, 0) OVER (ORDER BY month)

-- ② 境界 NULL とデータ自体の NULL の両方を完全に 0 に防ぐ(実務パターン)
LAG(COALESCE(revenue, 0), 1, 0) OVER (ORDER BY month)
2つの NULL の防御:LAG の第3引数は「前の行が存在しない場合(最初の行など)」にのみ適用される値です。「前の行は存在するが、その値自体が NULL の場合」は第3引数では防げず、NULL がそのまま返ってしまいます。実務では 対象列を COALESCE で保護し、さらに LAG の第3引数も指定する ことで、どんな状況でも確実なデフォルト値(0など)を取得して計算を行うのが鉄則です。
問題

monthly_revenue テーブルから、月ごとの売上(revenue)・前月売上(prev_revenue)・前月比成長率(mom_growth)を計算してください。
データ未収集で revenue が NULL の月は、売上 0 として扱ってください。また、LAG() を使って前月売上を取得する際も、最初の月や前月が売上 0(NULL を補正したものを含む)の場合は 0 として取得してください。
mom_growth = ROUND((当月 - 前月) / 前月, 4) で計算し、前月が 0 で計算できない場合(ゼロ除算)は NULL で表示してください。出力列は month, revenue, prev_revenue, mom_growth、month 昇順で返してください。

使用テーブル
► monthly_revenue(6行)
monthrevenue
2024-011000000
2024-021200000
2024-03NULL
2024-041100000
2024-051400000
2024-061350000
期待出力
monthrevenueprev_revenuemom_growth
2024-0110000000NULL
2024-02120000010000000.2000
2024-0301200000-1.0000
2024-0411000000NULL
2024-05140000011000000.2727
2024-0613500001400000-0.0357