NOT IN と NULL サブクエリの罠 — サブクエリに NULL が 1 つあるだけで全行がUNKNOWNで消える
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)
employees テーブルから、管理対象外の部署(managed_depts に存在しない dept_id)の従業員を抽出してください。出力列は emp_id, name, dept_id、emp_id 昇順で返してください。
| emp_id | name | dept_id |
|---|---|---|
| 1 | 田中 | 10 |
| 2 | 鈴木 | 20 |
| 3 | 佐藤 | 30 |
| 4 | 伊藤 | 40 |
| 5 | 山田 | NULL |
| 6 | 高橋 | 20 |
| dept_id | dept_name |
|---|---|
| 10 | 営業 |
| 30 | 人事 |
| NULL | 未定義 |
| emp_id | name | dept_id |
|---|---|---|
| 2 | 鈴木 | 20 |
| 4 | 伊藤 | 40 |
| 5 | 山田 | NULL |
| 6 | 高橋 | 20 |
集計関数と NULL の落とし穴 — COUNT(*) / COUNT(列) / AVG の分母が知らず縮む
集計関数は 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))
sales_results テーブルから、部署(dept)ごとの担当者数・実績あり担当者数・実績ありの平均売上・0実績を含む全員平均を集計してください。出力列は dept, head_count, active_reps, avg_amount, true_avg、dept 昇順で返してください。
| rep_id | dept | amount |
|---|---|---|
| 1 | 東京 | 250000 |
| 2 | 東京 | 300000 |
| 3 | 東京 | NULL |
| 4 | 東京 | NULL |
| 5 | 大阪 | 150000 |
| 6 | 大阪 | 200000 |
| 7 | 大阪 | 180000 |
| 8 | 大阪 | NULL |
| dept | head_count | active_reps | avg_amount | true_avg |
|---|---|---|---|---|
| 大阪 | 4 | 3 | 176667 | 132500 |
| 東京 | 4 | 2 | 275000 | 137500 |
LEFT JOIN の ON 句フィルタ — 非マッチ行を 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
customers テーブルの全顧客について、金額が 20,000 円を超える注文(大口注文)の最大金額(large_order)を表示してください。大口注文がない顧客または注文自体がない顧客は large_order を NULL で表示してください。出力列は customer_id, customer_name, large_order、customer_id 昇順で返してください。
| customer_id | customer_name |
|---|---|
| 1 | 田中商事 |
| 2 | 鈴木物産 |
| 3 | 佐藤商会 |
| 4 | 新規顧客 |
| order_id | customer_id | amount |
|---|---|---|
| 1 | 1 | 10000 |
| 2 | 1 | 35000 |
| 3 | 1 | 40000 |
| 4 | 2 | 28000 |
| 5 | 2 | 8000 |
| 6 | 3 | 5000 |
| 7 | 3 | 12000 |
| customer_id | customer_name | large_order |
|---|---|---|
| 1 | 田中商事 | 40000 |
| 2 | 鈴木物産 | 28000 |
| 3 | 佐藤商会 | NULL |
| 4 | 新規顧客 | NULL |
NULLIF でゼロ除算を NULL に変える — NULL を防御的に武器として使う
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)
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 昇順で返してください。
| date | campaign | clicks | conversions | revenue |
|---|---|---|---|---|
| 2024-03-01 | CP_A | 1000 | 50 | 200000 |
| 2024-03-01 | CP_B | 0 | 0 | 0 |
| 2024-03-02 | CP_A | 800 | 32 | 160000 |
| 2024-03-02 | CP_B | 500 | 20 | 100000 |
| 2024-03-03 | CP_A | 0 | 0 | 0 |
| 2024-03-03 | CP_B | 600 | 24 | 120000 |
| date | campaign | clicks | conv_rate | rev_per_click |
|---|---|---|---|---|
| 2024-03-01 | CP_A | 1000 | 0.0500 | 200.0000 |
| 2024-03-01 | CP_B | 0 | NULL | NULL |
| 2024-03-02 | CP_A | 800 | 0.0400 | 200.0000 |
| 2024-03-02 | CP_B | 500 | 0.0400 | 200.0000 |
| 2024-03-03 | CP_A | 0 | NULL | NULL |
| 2024-03-03 | CP_B | 600 | 0.0400 | 200.0000 |
ウィンドウ関数と NULL 伝播 — LAG の境界 NULL と前月比の連鎖 NULL
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)
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 昇順で返してください。
| month | revenue |
|---|---|
| 2024-01 | 1000000 |
| 2024-02 | 1200000 |
| 2024-03 | NULL |
| 2024-04 | 1100000 |
| 2024-05 | 1400000 |
| 2024-06 | 1350000 |
| month | revenue | prev_revenue | mom_growth |
|---|---|---|---|
| 2024-01 | 1000000 | 0 | NULL |
| 2024-02 | 1200000 | 1000000 | 0.2000 |
| 2024-03 | 0 | 1200000 | -1.0000 |
| 2024-04 | 1100000 | 0 | NULL |
| 2024-05 | 1400000 | 1100000 | 0.2727 |
| 2024-06 | 1350000 | 1400000 | -0.0357 |