SQL でデータを絞り込む WHERE 句は、AND・OR・NOT を組み合わせることで複雑な条件を表現できます。しかし「AND と OR が混在すると期待通りに絞り込めない」「NOT IN に NULL が含まれると全件ゼロになる」など、知らないと踏んでしまう落とし穴が存在します。
この記事では複数条件の組み合わせに必要な論理・優先順位・パフォーマンスのポイントを体系的に解説します。各演算子(IN・BETWEEN・LIKE・EXISTS)の詳細は専門記事へ誘導します。
-- orders テーブル(注文) -- id | customer | status | amount | category | order_date -- 1 | 田中 | shipped | 12000 | 家電 | 2024-01-15 -- 2 | 鈴木 | pending | 3500 | 食品 | 2024-02-01 -- 3 | 高橋 | canceled | 85000 | 家電 | 2024-02-20 -- 4 | 田中 | shipped | 2800 | 食品 | 2024-03-05 -- 5 | 伊藤 | pending | 45000 | 家電 | 2024-03-10 -- 6 | 渡辺 | shipped | 9500 | 衣類 | 2024-04-01 -- 7 | 田中 | pending | 600 | 食品 | 2024-04-15 -- 8 | 高橋 | shipped | NULL | 衣類 | 2024-04-20
WHERE 句の役割と書き方の基本
WHERE 句は FROM・JOIN の後に処理され、個々の行を条件で絞り込みます。複数の条件は AND・OR・NOT で組み合わせます。
-- 単一条件 SELECT * FROM orders WHERE status = 'shipped'; -- 複数条件(AND: 両方を満たす行のみ) SELECT * FROM orders WHERE status = 'shipped' AND amount >= 10000; -- 複数条件(OR: どちらかを満たす行) SELECT * FROM orders WHERE category = '家電' OR category = '食品'; -- 否定(NOT: 条件を満たさない行) SELECT * FROM orders WHERE NOT status = 'canceled'; -- 同じ意味: WHERE status <> 'canceled'
WHERE は
FROM/JOIN(①)→ WHERE(②)→ GROUP BY(③)→ HAVING(④)→ SELECT(⑤) の順で処理されます。そのため WHERE 句では SELECT で定義したエイリアスは使えません。実行順序の詳細はSELECT 文完全ガイドを参照してください。AND・OR・NOT の論理と NULL(三値論理)
SQL では TRUE・FALSE の 2 値ではなく、TRUE・FALSE・UNKNOWN の三値論理を使います。WHERE 条件が UNKNOWN の行は結果から除外されます。NULL との比較が絡むと予期しない動作の原因になります。
| A | B | A AND B | A OR B | NOT A |
|---|---|---|---|---|
| TRUE | TRUE | TRUE | TRUE | FALSE |
| TRUE | FALSE | FALSE | TRUE | FALSE |
| TRUE | UNKNOWN | UNKNOWN | TRUE | FALSE |
| FALSE | FALSE | FALSE | FALSE | TRUE |
| FALSE | UNKNOWN | FALSE | UNKNOWN | TRUE |
| UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN |
amount = NULL も amount <> NULL も結果は UNKNOWN(≠ FALSE)です。UNKNOWN の行は WHERE で除外されるため 0 件になります。NULL の検索には必ず IS NULL / IS NOT NULL を使ってください。NULL を含む列の絞り込みパターンはNULL を含む列の絞り込み完全ガイドで詳しく解説しています。AND と OR の優先順位と括弧による制御
AND は OR より優先度が高いため、混在した条件は意図と異なる結果になりやすいです。括弧 () で明示的にグループ化する習慣をつけてください。
-- 「家電 または(食品 かつ amount >= 5000)」と解釈される -- → 家電は全件(amount不問)、食品は5000以上のみ SELECT * FROM orders WHERE category = '家電' OR category = '食品' AND amount >= 5000; -- 等価な書き方(明示的に AND を先にまとめる) SELECT * FROM orders WHERE category = '家電' OR (category = '食品' AND amount >= 5000); -- 意図が「(家電 または 食品)かつ amount >= 5000」なら括弧が必要 SELECT * FROM orders WHERE (category = '家電' OR category = '食品') AND amount >= 5000;
| 演算子 | 優先度 | 補足 |
|---|---|---|
NOT |
高(1位) | 単一条件の否定。AND・OR より先に評価される |
AND |
中(2位) | 両条件が TRUE の行のみ |
OR |
低(3位) | どちらかが TRUE の行 |
() |
最高 | 括弧内を先に評価。優先順位を明示したいときは常に使う |
- AND と OR が混在するときは、必ず括弧でグループ化する
- 「A または B、かつ C」の場合:
(A OR B) AND Cと括弧を明示する - 条件が多くなるほど意図が分かりにくくなるため、コメント(
-- ここは配送済みかつ高額)を添えるとよい
WHERE で使える絞り込み演算子の一覧
| 演算子 | 用途 | 例 | 詳細 |
|---|---|---|---|
= / <> / != |
等値・不等値の比較 | status = 'shipped' |
— |
> / < / >= / <= |
大小比較 | amount >= 10000 |
— |
BETWEEN A AND B |
範囲(A 以上 B 以下) | amount BETWEEN 1000 AND 50000 |
IN/BETWEEN ガイド |
IN (値1, 値2, ...) |
リストのいずれかと一致 | category IN ('家電', '衣類') |
IN/BETWEEN ガイド |
LIKE 'パターン' |
文字列パターン一致(% と _) | customer LIKE '田%' |
— |
IS NULL / IS NOT NULL |
NULL かどうか | amount IS NOT NULL |
NULL フィルタガイド |
NOT IN / NOT LIKE |
否定条件 | status NOT IN ('canceled') |
NOT 条件ガイド |
EXISTS (サブクエリ) |
サブクエリの結果が存在するか | EXISTS (SELECT 1 FROM ...) |
IN/BETWEEN ガイド |
実務でよく使う複数条件パターン集
-- shipped かつ 1万円以上の注文 SELECT id, customer, amount, order_date FROM orders WHERE status = 'shipped' AND amount >= 10000 ORDER BY amount DESC; -- 結果: id=1(田中, 12000円)のみ -- キャンセル以外かつ 1 万円未満 SELECT id, customer, amount, status FROM orders WHERE status <> 'canceled' AND amount < 10000;
-- 2024年2月〜3月の家電・衣類注文
SELECT id, customer, category, amount, order_date
FROM orders
WHERE category IN ('家電', '衣類')
AND order_date BETWEEN '2024-02-01' AND '2024-03-31'
ORDER BY order_date;
-- 「出荷済み または キャンセル」で 2024年1月以降
SELECT id, customer, status, order_date
FROM orders
WHERE (status = 'shipped' OR status = 'canceled')
AND order_date >= '2024-01-01'
ORDER BY order_date;
-- amount が NULL または 1000 円未満の注文(要注意パターン) SELECT id, customer, amount FROM orders WHERE amount IS NULL OR amount < 1000; -- 結果: id=7(600円)と id=8(NULL) -- amount が入力済みでかつ 5000 円以上 SELECT id, customer, amount FROM orders WHERE amount IS NOT NULL AND amount >= 5000;
WHERE amount < 1000 だけでは NULL の行は除外されます(NULL < 1000 = UNKNOWN)。NULL の行も含めたい場合は WHERE amount IS NULL OR amount < 1000 と明示してください。-- 田中さんの注文で amount が田中さんの平均以上のもの
SELECT id, customer, amount
FROM orders
WHERE customer = '田中'
AND amount >= (
SELECT AVG(amount)
FROM orders
WHERE customer = '田中'
);
-- 1 件でも家電注文がある顧客の全注文(EXISTS)
SELECT o.id, o.customer, o.category
FROM orders AS o
WHERE EXISTS (
SELECT 1 FROM orders AS o2
WHERE o2.customer = o.customer
AND o2.category = '家電'
)
ORDER BY o.customer, o.id;
NOT IN の NULL 問題(重大な落とし穴)
NOT IN のリストや サブクエリの結果に NULL が 1 つでも含まれると全件 0 件になります。これは三値論理の仕様によるもので、非常に多くの開発者が踏む落とし穴です。
-- NG: amount に NULL が含まれるリストで NOT IN を使うと 0 件になる
SELECT * FROM orders
WHERE id NOT IN (1, 2, NULL);
-- 解説: id <> 1 AND id <> 2 AND id <> NULL
-- id <> NULL は UNKNOWN → 全条件が UNKNOWN → 全件除外 → 0件!
-- OK: NULL を除外した明示的なリストを使う
SELECT * FROM orders
WHERE id NOT IN (1, 2); -- NULL を含まないリスト
-- サブクエリで NULL が混入するケース(より危険)
-- amount が NULL の行を持つテーブルをサブクエリにすると全件 0 件
SELECT * FROM orders
WHERE id NOT IN (
SELECT id FROM orders WHERE status = 'canceled'
);
-- ↑ このサブクエリが NULL を返す可能性があれば 0 件になる
-- 安全な代替: NOT EXISTS を使う(NULL に強い)
SELECT * FROM orders AS o
WHERE NOT EXISTS (
SELECT 1 FROM orders AS o2
WHERE o2.id = o.id
AND o2.status = 'canceled'
);
NOT IN: リストが確実に NULL を含まない場合は使える(シンプルで読みやすい)NOT EXISTS: サブクエリが NULL を返す可能性がある場合は常に安全LEFT JOIN + IS NULL: 大量データでのパフォーマンスが NOT EXISTS より優れる場合がある- NOT 条件全般の詳細は一致しないデータを抽出する方法を参照
HAVING と WHERE の使い分け(集計後の絞り込み)
WHERE は個々の行を集計前に絞り込む、HAVING はGROUP BY 後のグループを集計結果で絞り込むという違いがあります。
-- WHERE: 集計前の行絞り込み(status が shipped の行だけで集計) SELECT customer, COUNT(*) AS 件数, SUM(amount) AS 合計 FROM orders WHERE status = 'shipped' -- ← 先に行を絞り込む GROUP BY customer; -- HAVING: 集計後のグループ絞り込み(合計が 10000 以上のグループのみ) SELECT customer, COUNT(*) AS 件数, SUM(amount) AS 合計 FROM orders GROUP BY customer HAVING SUM(amount) >= 10000; -- ← 集計結果で絞り込む -- 両方の組み合わせ(shipped のみを集計し、合計 5000 以上のグループを返す) SELECT customer, COUNT(*) AS 件数, SUM(amount) AS 合計 FROM orders WHERE status = 'shipped' -- ① 先に shipped に絞る GROUP BY customer HAVING SUM(amount) >= 5000; -- ② 集計後にさらに絞る
| WHERE | HAVING | |
|---|---|---|
| 処理タイミング | GROUP BY より前(行単位) | GROUP BY より後(グループ単位) |
| 集計関数(SUM・COUNT等) | 使えない | 使える |
| インデックス活用 | 効く(行を早期に絞り込む) | 効きにくい(集計後の絞り込み) |
| 典型的な用途 | 特定ステータス・期間の絞り込み | 件数・合計値が閾値以上のグループ |
- WHERE でできる絞り込みは WHERE でやる(集計対象を事前に減らすと GROUP BY が速くなる)
- HAVING に書けるものを WHERE に書いてしまうとインデックスが使われない場合がある
HAVING COUNT(*) >= 2など集計関数を使った条件のみ HAVING に書く
SARGable 条件とインデックスの活用
インデックスが効く条件の書き方を SARGable(Search ARGument ABLE)条件と呼びます。WHERE 句の書き方次第でインデックスが使われなくなり、全件スキャンになることがあります。
| 状況 | SARGable でない書き方(遅い) | SARGable な書き方(速い) |
|---|---|---|
| 列に関数を適用 | YEAR(order_date) = 2024 |
order_date BETWEEN '2024-01-01' AND '2024-12-31' |
| 計算式を列側に書く | amount * 1.1 >= 11000 |
amount >= 10000 |
| 暗黙の型変換 | id = '100'(id が INT 型) |
id = 100(型を合わせる) |
| LIKE の前方ワイルドカード | LIKE '%田' |
LIKE '田%'(前方一致のみインデックス有効) |
| OR で異なる列を使う | col1 = 'A' OR col2 = 'B' |
UNION で分けるか複合インデックスを検討 |
-- NG: 列に関数をかけるとインデックスが使われない SELECT * FROM orders WHERE YEAR(order_date) = 2024 AND MONTH(order_date) = 3; -- OK: 範囲比較に書き直す SELECT * FROM orders WHERE order_date >= '2024-03-01' AND order_date < '2024-04-01'; -- NG: 計算式を列側に書くとインデックスが使われない SELECT * FROM orders WHERE amount / 2 >= 5000; -- OK: 定数側を計算する SELECT * FROM orders WHERE amount >= 10000; -- EXPLAIN で実行計画を確認して type が ALL かどうかチェックする EXPLAIN SELECT * FROM orders WHERE order_date >= '2024-03-01';
複数条件が意図通りか確認するデバッグテクニック
-- 複雑な条件を段階的に確認する手順
-- ステップ1: 件数だけ確認(全件)
SELECT COUNT(*) FROM orders; -- 8件
-- ステップ2: 条件Aだけ確認
SELECT COUNT(*) FROM orders WHERE status = 'shipped'; -- 4件
-- ステップ3: 条件Aとの AND を確認
SELECT COUNT(*) FROM orders
WHERE status = 'shipped' AND amount >= 10000; -- 1件
-- ステップ4: 期待件数と合っているか確認
-- → 多すぎ/少なすぎなら括弧の位置や OR/AND の見直し
-- テクニック: 条件ごとにフラグを出して可視化
SELECT
id,
customer,
status,
amount,
CASE WHEN status = 'shipped' THEN '✓' ELSE '✗' END AS 配送済み,
CASE WHEN amount >= 10000 THEN '✓' ELSE '✗' END AS 高額
FROM orders;
SELECT COUNT(*), COUNT(列名) FROM テーブル で NULL の件数を把握してから条件を組む。COUNT(*) は全行数、COUNT(列名) は NULL 除外の件数を返すので、差が NULL 件数です。よくある質問(FAQ)
A OR B AND C は A OR (B AND C) と解釈されます。意図した通りにグループ化するには括弧 () を使ってください。例えば「(家電 または 食品)かつ 1 万円以上」は (category = '家電' OR category = '食品') AND amount >= 10000 と書きます。EXPLAIN で実行計画を確認しながら条件をステップごとに確認するのが効果的です。NULL != 'canceled')は TRUE でも FALSE でもなく UNKNOWN になります。UNKNOWN の行は WHERE で除外されるため、NULL 行も消えてしまいます。NULL の行を含めるには WHERE status != 'canceled' OR status IS NULL と明示的に条件を追加してください。x <> NULL = UNKNOWN で全条件が UNKNOWN になる)。安全な代替として NOT EXISTS を使うか、サブクエリに WHERE 列 IS NOT NULL を加えて NULL を除外してください。HAVING を使ってください。例: GROUP BY customer HAVING SUM(amount) >= 10000。-- shipped かつ高額)を添えて意図を明記する。条件が 5 つ以上になる場合は複数の CTE に分割することで可読性が大幅に上がります。まとめ
| やりたいこと | 書き方・ポイント |
|---|---|
| 両方の条件を満たす行 | WHERE A AND B |
| どちらかの条件を満たす行 | WHERE A OR B(AND との混在時は括弧必須) |
| 条件を満たさない行 | WHERE NOT A または WHERE A <> 値 |
| AND と OR の優先順位を制御 | 括弧 () で明示的にグループ化 |
| NULL の行を含める | IS NULL を OR で明示的に追加 |
| NOT IN で全件 0 件になる | リストに NULL が含まれていないか確認、または NOT EXISTS を使う |
| 集計結果で絞り込む | WHERE ではなく HAVING を使う |
| インデックスを効かせる | 列に関数や計算を適用しない(SARGable 条件) |
| 条件が意図通りか確認 | 条件を 1 つずつ段階的に追加して件数を確認 |
各演算子の詳細についてはIN・BETWEEN・EXISTS 完全ガイド・NOT 条件完全ガイド・NULL フィルタリング完全ガイドもあわせてご覧ください。

