コマンド道場

EXCEPT(片方から他方を引く)の使い方

左から右を引く

shain給与 50 万以上

name
中村
井上
山本

shain在籍中

name
中村
井上
吉田

EXCEPT左にしかいない人

name
山本
  • 給与 50 万以上 10 在籍中 16左にしかいない人 1
  • 残るのは給与 50 万以上にしかない行です。給与 50 万以上10 行のうち、在籍中にもある 9 行が引かれます

10 人から在籍中の 9 人が引かれて 1 人だけ残ります。給与 50 万以上なのに在籍していない人、つまり退職した山本です。

EXCEPT は 1 つ目の結果から 2 つ目の結果を引きます。 残るのは「左にはあるが右には無い行」です。UNIONINTERSECT と違い、左右を入れ替えると別の結果になります。「登録されているのに使われていないもの」を探すときの定番です。

未確認の製品があります そのまま使える: PostgreSQL / MySQL / MariaDB / SQLite / SQL Server / Oracle Database

未確認: IBM Db2

製品ごとの対応表(根拠つき)を見る

このページの実行結果と図は、すべて本物のデータベースに同じ SQL を流して得たものです。手で書いた「たぶんこうなる」ではありません。

01左にあって右に無い行だけが残る

結論: EXCEPT は引き算です。 左の一覧から、右の一覧に出てくる行を取り除きます。

右にしか無い行は結果に出てきません。引かれる側なので、右に何行あっても結果が増えることはありません。

言い換えると、結果の件数は必ず左の件数以下になります。増えていたら書き方を間違えています。

⚠️ 比べるのは行まるごとです。選んでいる列の値がすべて一致したときだけ「同じ行」として引かれます。

1 行ずつ、何をしているか
  1. SELECT name FROM shain引かれる側(左)
  2. WHERE salary >= 500000給与 50 万以上(10 人)
  3. EXCEPT★左から右を引く
  4. SELECT name FROM shain引く側(右)
  5. WHERE taishoku_on IS NULL在籍中(16 人)
  6. ORDER BY 1;並べ替えは最後に 1 つだけ

右の 16 人のうち、左にも出てくる 9 人だけが引かれます。右にしかいない 7 人は結果に影響しません。

給与 50 万以上なのに在籍していない人
SELECT name FROM shain WHERE salary >= 500000
EXCEPT
SELECT name FROM shain WHERE taishoku_on IS NULL
ORDER BY 1;
name
山本

1

1 人です。給与 50 万以上の 10 人のうち、退職している山本だけが残りました。

02⚠️ よくある間違い:左右を入れ替えると別物になる

結論: EXCEPT は左右を入れ替えると結果が変わります。

UNIONINTERSECT は入れ替えても同じ結果でしたが、引き算はそうなりません。どちらを引かれる側にするかを必ず意識してください。

上の例を入れ替えると「在籍中なのに給与 50 万に届かない人」になり、件数も顔ぶれもまったく変わります。

日本語にしてから書くのが確実です。 「A にはあるが B には無いもの」と言えたなら、A が左、B が右です。

左右を入れ替えた場合
SELECT name FROM shain WHERE taishoku_on IS NULL
EXCEPT
SELECT name FROM shain WHERE salary >= 500000
ORDER BY 1;
name
吉田
大野
小林
斎藤
木村
松本
渡辺

7

7 人になります。上の例は 1 人でした。同じ 2 つの一覧を使っているのに、引く向きが変わるだけで別の問いになっています。

03使われていないものを探す

結論: 「マスタにあるのに、明細に出てこないもの」を探すのが定番の使い道です。

部署表には登録されているのに、その部署に所属する社員が 1 人もいない、という部署を探してみます。busho から shain に出てくる部署番号を引けば残ります。

部署番号のままでは分かりにくいので、名前で引き直すと読みやすくなります。

⚠️ 同じことは外部結合や NOT EXISTS でも書けます。どれが読みやすいかは場合によるので、両方書いて比べてください。

社員が 1 人もいない部署の番号
SELECT busho_id FROM busho
EXCEPT
SELECT busho_id FROM shain
ORDER BY 1;
busho_id
6

1

1 行です。6 番だけが残りました。部署表には 7 部署ありますが、社員表に出てこないのは 1 つだけです。

番号ではなく名前で出す
SELECT busho_name FROM busho
WHERE busho_id IN (SELECT busho_id FROM busho
                   EXCEPT
                   SELECT busho_id FROM shain)
ORDER BY 1;
busho_name
法務

1

法務です。引き算の結果を IN の中に入れて、部署名を引き直しています。

04⚠️ よくある間違い:NOT IN が 0 行になる場面でも動く

結論: NOT IN の右側に NULL が 1 つでもあると、結果は 1 行も返りません。 EXCEPT にはこの落とし穴がありません。

社員表の busho_id には所属未設定の NULL が入っています。この列をそのまま NOT IN の中に入れると、エラーも警告も出ないまま 0 行になります。

理由は三値論理です。6 NOT IN (1, 2, NULL) は「6 は NULL と違うか」が unknown になるため、全体も unknown になり、真になりません。

NOT IN を使うなら、右側から NULL を除くことを習慣にしてください。 除けば EXCEPT と同じ結果になります。

NOT IN をそのまま使う
SELECT busho_name FROM busho
WHERE busho_id NOT IN (SELECT busho_id FROM shain);
busho_name

0

0 行です。エラーは出ません。社員表の busho_id に NULL が含まれているせいで、すべての行が unknown になっています。

右側から NULL を除く
SELECT busho_id FROM busho
WHERE busho_id NOT IN (SELECT busho_id FROM shain
                       WHERE busho_id IS NOT NULL)
ORDER BY 1;
busho_id
6

1

1 行です。6 番だけが残り、EXCEPT で書いたときと同じ結果になりました。

05重複は 1 つにまとめられる

結論: EXCEPT の結果に同じ行は 2 つ出てきません。

UNIONINTERSECT と同じで、集合演算は既定で重複を消します。左に同じ値が何行あっても、結果は 1 行です。

件数を数える目的には使えません。 「引いた残りが何件か」を知りたい場合、その件数は重複を除いた種類の数であることに注意してください。

左に 8 行あっても結果は 1 行
SELECT busho_id FROM shain WHERE busho_id IN (2, 3)
EXCEPT
SELECT busho_id FROM shain WHERE busho_id = 3;
busho_id
2

1

1 行です。左は開発 4 人と営業 4 人で 8 行返しますが、営業(3)が引かれ、残った開発(2)は 1 行にまとめられます。

自分で打ってみる

このページの例と同じデータが入った本物のデータベースを、この場(あなたのブラウザの中)で起動できます。打った SQL がサーバーへ送られることはありません。

押すと初回だけ約 9MB を読み込みます(2 回目以降はキャッシュされます)。

解いてみる

読んだだけでは書けるようになりません。実行結果で採点します。

関連するトピック

  • UNION / UNION ALLどちらかにあれば残す集合演算です。
  • INTERSECT両方にある行だけを残す集合演算です。
  • IN / EXISTSNOT IN と NOT EXISTS の違いはこちらで詳しく扱っています。
  • IS NULLNULL と三値論理の話はこちらです。
  • LEFT / RIGHT OUTER JOIN「相手がいない行」を外部結合で探す書き方です。

根拠(一次情報)