SQLの結合と集計

技術の仕組み | データベースの基礎

← 技術の仕組み

SQLの穴埋めは、暗記ではなく「答えとして出したい1行が何を表すか」を先に決めると解けます。1行=1件の明細なら結合だけ、1行=顧客ごとの合計ならGROUP BY、実績ゼロの相手も出したいなら外部結合。この順で決めると、書くべき句がほぼ自動的に決まります。

先に押さえる

SQLは書いた順ではなくFROM(結合)→ WHERE(行の絞り込み)→ GROUP BY(まとめる)→ HAVING(集計後の絞り込み)→ SELECT → ORDER BYの順に処理されます。この処理順を頭に入れておくと、「なぜ WHERE に SUM が書けないのか」「なぜ SELECT の別名が WHERE で使えないのか」が説明できます。

題材にする2つの表

顧客表
顧客番号顧客名
C01青木
C02井上
C03上田
受注表
受注番号顧客番号金額
J1C01300
J2C01200
J3C02500

上田(C03)は受注が1件もない顧客です。この1人が、内部結合と外部結合の違いをそのまま見せてくれます。

内部結合と外部結合

SELECT 顧客.顧客名, 受注.金額 FROM 顧客 INNER JOIN 受注 ON 顧客.顧客番号 = 受注.顧客番号
内部結合の結果
顧客名金額
青木300
青木200
井上500
上田(消える)

両方の表に相手がいる行だけが残る。上田は受注がないので結果から消える

SELECT 顧客.顧客名, 受注.金額 FROM 顧客 LEFT OUTER JOIN 受注 ON 顧客.顧客番号 = 受注.顧客番号
左外部結合の結果
顧客名金額
青木300
青木200
井上500
上田NULL

左の表(顧客)の行は全部残る。相手がいない列はNULLで埋まる。

見抜きどころ

判断軸はただ1つ、「一致しない行を残したいか」です。試験で外部結合を選ぶ理由は、ほぼ例外なく実績ゼロの行も一覧に出したいから。設問文に「受注のない顧客も含めて」「売上が0の商品も表示する」「取引実績の有無にかかわらず全ての取引先を」といった語があれば、そこが外部結合の合図です。逆に「実績のある顧客だけ」なら内部結合で足ります。LEFTFROM に先に書いた表(残したい側)を全部残す、と覚えます。

つまずきやすい

外部結合を書いたのに、WHERE に右側の表の条件を書くと内部結合に戻ってしまいます。右側が NULL の行は「受注.金額 >= 100」のような条件で必ず偽になり、消えるからです。残したい側の相手にかける条件は ON に、結合後の全体にかける条件は WHERE に。この置き場所の違いは、穴埋めでも狙われます。

GROUP BY と HAVING

集計するときの合言葉は「1行=何の単位か」。それがそのまま GROUP BY に並べる列になります。顧客ごとの合計を出したいなら単位は顧客なので GROUP BY 顧客.顧客番号, 顧客.顧客名

SELECT 顧客.顧客名, SUM(受注.金額) AS 合計 FROM 顧客 INNER JOIN 受注 ON 顧客.顧客番号 = 受注.顧客番号 WHERE 受注.金額 >= 100 -- 集計する前に、対象の行を絞る GROUP BY 顧客.顧客番号, 顧客.顧客名 -- ここが「1行の単位」 HAVING SUM(受注.金額) >= 400 -- 集計した結果を絞る ORDER BY 合計 DESC

WHERE=集計前

  • まとめるの個々の行に効く
  • 集計関数(SUM・COUNT等)は書けない
  • 例:「取消でない受注だけを対象に」

HAVING=集計後

  • まとめたのグループに効く
  • 集計関数を書ける/書くのが普通
  • 例:「合計が400以上の顧客だけ」
注記

SELECT に書ける列は、原則GROUP BY に並べた列集計関数だけです。穴埋めで「SELECT に顧客名があるのに GROUP BY に顧客番号しかない」形を見たら、顧客名も GROUP BY に足すのが定番の解答になります。

副問合せと EXISTS / NOT EXISTS

〜が1件も存在しない行」を出すのが NOT EXISTS の定番の型です。外部結合+NULL 判定でも同じことができますが、設問が副問合せの形で穴埋めしてくるときは EXISTS を使います。

-- 受注が1件もない顧客を求める SELECT 顧客.顧客名 FROM 顧客 WHERE NOT EXISTS ( SELECT 1 FROM 受注 WHERE 受注.顧客番号 = 顧客.顧客番号 -- ← 外側の表を参照=相関副問合せ )

相関副問合せの読み方は「外側の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でも空文字でもないため、扱いを間違えると結果が変わります。

-- 受注ゼロの顧客も、合計0として一覧に出す SELECT 顧客.顧客名, COALESCE(SUM(受注.金額), 0) AS 合計 FROM 顧客 LEFT OUTER JOIN 受注 ON 顧客.顧客番号 = 受注.顧客番号 GROUP BY 顧客.顧客番号, 顧客.顧客名
結果(COALESCE あり)
顧客名合計COUNT(*)COUNT(受注.受注番号)
青木50022
井上50011
上田010
見抜きどころ

外部結合と 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が出るか → 出るなら COALESCECOUNT(列名) で始末する。
この①〜⑤を順に埋めれば、空欄に入る句は消去法でほぼ1つに絞れます。

この仕組みを使う設問