EXCEPT(片方から他方を引く)の使い方
shain給与 50 万以上
| name |
|---|
| 中村 |
| 井上 |
| 山本 |
| … |
shain在籍中
| name |
|---|
| 中村 |
| 井上 |
| 吉田 |
| … |
EXCEPT左にしかいない人
| name |
|---|
| 山本 |
- 給与 50 万以上 10 行 − 在籍中 16 行 → 左にしかいない人 1 行
- 残るのは給与 50 万以上にしかない行です。給与 50 万以上の 10 行のうち、在籍中にもある 9 行が引かれます
10 人から在籍中の 9 人が引かれて 1 人だけ残ります。給与 50 万以上なのに在籍していない人、つまり退職した山本です。
EXCEPT は 1 つ目の結果から 2 つ目の結果を引きます。 残るのは「左にはあるが右には無い行」です。UNION や INTERSECT と違い、左右を入れ替えると別の結果になります。「登録されているのに使われていないもの」を探すときの定番です。
未確認の製品があります そのまま使える: PostgreSQL / MySQL / MariaDB / SQLite / SQL Server / Oracle Database
未確認: IBM Db2
このページの実行結果と図は、すべて本物のデータベースに同じ SQL を流して得たものです。手で書いた「たぶんこうなる」ではありません。
01左にあって右に無い行だけが残る
結論: EXCEPT は引き算です。 左の一覧から、右の一覧に出てくる行を取り除きます。
右にしか無い行は結果に出てきません。引かれる側なので、右に何行あっても結果が増えることはありません。
言い換えると、結果の件数は必ず左の件数以下になります。増えていたら書き方を間違えています。
⚠️ 比べるのは行まるごとです。選んでいる列の値がすべて一致したときだけ「同じ行」として引かれます。
SELECT name FROM shain引かれる側(左)WHERE salary >= 500000給与 50 万以上(10 人)EXCEPT★左から右を引くSELECT name FROM shain引く側(右)WHERE taishoku_on IS NULL在籍中(16 人)ORDER BY 1;並べ替えは最後に 1 つだけ
右の 16 人のうち、左にも出てくる 9 人だけが引かれます。右にしかいない 7 人は結果に影響しません。
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 は左右を入れ替えると結果が変わります。
UNION と INTERSECT は入れ替えても同じ結果でしたが、引き算はそうなりません。どちらを引かれる側にするかを必ず意識してください。
上の例を入れ替えると「在籍中なのに給与 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 でも書けます。どれが読みやすいかは場合によるので、両方書いて比べてください。
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 と同じ結果になります。
SELECT busho_name FROM busho
WHERE busho_id NOT IN (SELECT busho_id FROM shain);| busho_name |
|---|
(0 行)
0 行です。エラーは出ません。社員表の busho_id に NULL が含まれているせいで、すべての行が unknown になっています。
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 つ出てきません。
UNION や INTERSECT と同じで、集合演算は既定で重複を消します。左に同じ値が何行あっても、結果は 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「相手がいない行」を外部結合で探す書き方です。
根拠(一次情報)
- PostgreSQL 18 マニュアル: 問い合わせの結合(UNION / INTERSECT / EXCEPT)一次情報・確認 2026-08-11
- PostgreSQL 18 マニュアル: 副問い合わせ式(NOT IN と NULL)一次情報・確認 2026-08-11