SQLの穴埋めは、暗記ではなく「答えとして出したい1行が何を表すか」を先に決めると解けます。1行=1件の明細なら結合だけ、1行=顧客ごとの合計ならGROUP BY、実績ゼロの相手も出したいなら外部結合。この順で決めると、書くべき句がほぼ自動的に決まります。
SQLは書いた順ではなくFROM(結合)→ WHERE(行の絞り込み)→ GROUP BY(まとめる)→ HAVING(集計後の絞り込み)→ SELECT → ORDER BYの順に処理されます。この処理順を頭に入れておくと、「なぜ WHERE に SUM が書けないのか」「なぜ SELECT の別名が WHERE で使えないのか」が説明できます。
題材にする2つの表
| 顧客番号 | 顧客名 |
|---|---|
| C01 | 青木 |
| C02 | 井上 |
| C03 | 上田 |
| 受注番号 | 顧客番号 | 金額 |
|---|---|---|
| J1 | C01 | 300 |
| J2 | C01 | 200 |
| J3 | C02 | 500 |
上田(C03)は受注が1件もない顧客です。この1人が、内部結合と外部結合の違いをそのまま見せてくれます。
内部結合と外部結合
| 顧客名 | 金額 |
|---|---|
| 青木 | 300 |
| 青木 | 200 |
| 井上 | 500 |
| 上田 | (消える) |
両方の表に相手がいる行だけが残る。上田は受注がないので結果から消える。
| 顧客名 | 金額 |
|---|---|
| 青木 | 300 |
| 青木 | 200 |
| 井上 | 500 |
| 上田 | NULL |
左の表(顧客)の行は全部残る。相手がいない列はNULLで埋まる。
判断軸はただ1つ、「一致しない行を残したいか」です。試験で外部結合を選ぶ理由は、ほぼ例外なく実績ゼロの行も一覧に出したいから。設問文に「受注のない顧客も含めて」「売上が0の商品も表示する」「取引実績の有無にかかわらず全ての取引先を」といった語があれば、そこが外部結合の合図です。逆に「実績のある顧客だけ」なら内部結合で足ります。LEFTはFROM に先に書いた表(残したい側)を全部残す、と覚えます。
外部結合を書いたのに、WHERE に右側の表の条件を書くと内部結合に戻ってしまいます。右側が NULL の行は「受注.金額 >= 100」のような条件で必ず偽になり、消えるからです。残したい側の相手にかける条件は ON に、結合後の全体にかける条件は WHERE に。この置き場所の違いは、穴埋めでも狙われます。
GROUP BY と HAVING
集計するときの合言葉は「1行=何の単位か」。それがそのまま GROUP BY に並べる列になります。顧客ごとの合計を出したいなら単位は顧客なので GROUP BY 顧客.顧客番号, 顧客.顧客名。
WHERE=集計前
- まとめる前の個々の行に効く
- 集計関数(SUM・COUNT等)は書けない
- 例:「取消でない受注だけを対象に」
HAVING=集計後
- まとめた後のグループに効く
- 集計関数を書ける/書くのが普通
- 例:「合計が400以上の顧客だけ」
SELECT に書ける列は、原則GROUP BY に並べた列か集計関数だけです。穴埋めで「SELECT に顧客名があるのに GROUP BY に顧客番号しかない」形を見たら、顧客名も GROUP BY に足すのが定番の解答になります。
副問合せと EXISTS / NOT EXISTS
「〜が1件も存在しない行」を出すのが NOT EXISTS の定番の型です。外部結合+NULL 判定でも同じことができますが、設問が副問合せの形で穴埋めしてくるときは EXISTS を使います。
相関副問合せの読み方は「外側の1行を決めて、その値を内側に差し込んで内側を実行し、行が返るかどうかで外側の1行を残すか決める」。上の例なら、上田の行を持って内側を実行 → 受注が0件 → NOT EXISTS が真 → 上田が残る、という読み方になります。
同じ意味の3つの書き方
- NOT EXISTS(相関副問合せ)
- NOT IN(顧客番号の一覧と比べる)
- 左外部結合+ WHERE 受注.顧客番号 IS NULL
選び分け
- 内側で外側の列を参照しているなら相関=EXISTS系
- 内側が単独で実行できるなら IN/単純な副問合せ
- 集計値と比べるなら WHERE 金額 >= (SELECT AVG(金額) …) の型
NOT IN は、内側の結果に NULL が1つでも混じると1行も返らなくなります(NULL との比較が「不明」になり、真になれないため)。NOT EXISTS にはこの落とし穴がありません。「NOT IN で書いたら結果が空になった」という不具合の理由づけは、そのまま記述解答になります。
UNION と UNION ALL
UNION
- 2つの結果をつなげ、重複行を除く
- 重複判定のため並べ替え等の余分な処理が要る
UNION ALL
- 重複を除かずそのまま全部つなげる
- 重複が出ないと分かっているならこちらが速い
つなげる両側は列数が同じで、対応する列のデータ型が両立していることが条件。列名は左側のものが使われます。ORDER BY は全体に対して最後に1つだけ書きます(片側だけに付けることはできない)。
NULLの扱い
外部結合を使うと必ず NULL が生まれます。NULL は「値がない・不明」であって0でも空文字でもないため、扱いを間違えると結果が変わります。
| 顧客名 | 合計 | COUNT(*) | COUNT(受注.受注番号) |
|---|---|---|---|
| 青木 | 500 | 2 | 2 |
| 井上 | 500 | 1 | 1 |
| 上田 | 0 | 1 | 0 |
- 集計関数はNULLを無視する:SUM・AVG・MAX は NULL 行を数に入れない。上田の SUM は NULL になるので COALESCE で0に置換する。
- COUNT(*) は行数を数えるので、外部結合で NULL 埋めされた上田の行も1と数えてしまう。
- COUNT(列名) はその列がNULLでない行だけ数えるので、上田は0。「受注件数」を出したいならCOUNT(列名)側が正解。
- NULLの判定は
= NULLではなく IS NULL / IS NOT NULL。 - AVG も要注意:NULLを除いて平均するので、「NULLを0とみなした平均」とは値が変わる。
外部結合と COUNT はセットで問われます。「取引実績のない取引先は0件と表示する」という要件が出たら、答えは左外部結合+COUNT(相手表の列)、金額側はCOALESCE(SUM(…),0)。COUNT(*) と書くと0件のはずが1件になる、という誤りの指摘まで書けると強いです。
試験で一段深く問われる点
結合で行が増える
- 1対多の結合では、1の側の行が多の件数だけ複製される
- そこに SUM をかけると金額が二重に足される
- 対策=先に集計してから結合する(副問合せを表として結合)
3表以上の結合
- 結合条件は表の数マイナス1本が目安
- 条件を1本書き忘れると全組合せになり件数が爆発する
- 穴埋めでは「どの列とどの列を等号で結ぶか」=主キーと外部キーの対応を探す
結合する列は外部キーと、その参照先の主キーであるのが原則です。だからSQLの穴埋めが解けないときは、まずE-R図や表定義に戻ってどの列が主キーで、どの列が他の表を指しているかを確認します。結合条件は自分で考えるものではなく、表定義から読み取るものです。
急所:解く順序を固定する
SQL穴埋めは、次の順に決めると迷いません。
①求めたい1行の単位は何か(明細1件か、顧客ごとか、月ごとか)→ そのまま GROUP BY の列になる。
②その単位を作るのに、どの表が要るか → FROM に並べ、主キーと外部キーで結合条件を書く。
③実績ゼロの行も出すか → 出すなら外部結合、出さないなら内部結合。
④絞り込みは集計の前か後か → 前なら WHERE、後なら HAVING。
⑤NULLが出るか → 出るなら COALESCE/COUNT(列名) で始末する。
この①〜⑤を順に埋めれば、空欄に入る句は消去法でほぼ1つに絞れます。