SQL 述語(Predicate) — IN/NOT IN・EXISTSの基礎

基礎述語 (Predicate)BETWEEN / LIKEIS NULL / INEXISTS / NOT EXISTS複合述語PostgreSQL対応全5問
1 / 5 · 学習中 0 / 5 問完了
QUESTION 6

IN述語(サブクエリ)と NOT IN の NULL の罠 — 動的リストと三値論理の危険

IN サブクエリNOT IN動的リストNULL混入に注意
前提知識

IN の内側にサブクエリを書くと、テーブルの状態に応じて動的にリストを生成できます。また NOT IN のリストに NULL が1件でも含まれると全件0件になる「NULL の罠」が存在します。

-- IN サブクエリ:planがpremiumのuser_idを動的に生成
WHERE user_id IN (
  SELECT user_id FROM users WHERE plan = 'premium'
)

-- NOT IN の罠:user_id に NULL が1件でも含まれると全件0件になる
WHERE user_id NOT IN (
  SELECT user_id FROM orders
  WHERE  user_id IS NOT NULL  -- NULL混入防止が必須
)
NOT IN + NULL の罠:5 NOT IN (1, NULL, 3) は、各値との不一致を AND でつないだ判定と同じです。このうち 5<>NULL は真偽を確定できず UNKNOWN になります。AND 全体も UNKNOWN となり、WHERE は TRUE の行だけを残すため、この行は除外されます。リストに NULL が1件でも混入すると、どの値も同じ理由で通過できません。
問題

orders テーブルから、plan が 'premium' のユーザーの注文を IN サブクエリで取得してください。取得列は order_id, user_id, amount, status、amount 降順で返してください。

使用テーブル
▸ users
user_idnameplan
1田中 太郎premium
2佐藤 花子free
3鈴木 一郎premium
4山田 次郎standard
▸ orders
order_iduser_idamountstatus
10118000completed
102112000completed
10323500pending
10439500completed
10546000cancelled
期待出力
order_iduser_idamountstatus
102112000completed
10439500completed
10118000completed
模範解答コード
SELECT
  order_id, user_id, amount, status
FROM   orders
WHERE  user_id IN (             -- IN: サブクエリ結果リストに一致する行を残す
  SELECT user_id                -- 必ず単一列を返す(複数列はエラー)
  FROM   users
  WHERE  plan = 'premium'      -- premium の user_id リストを動的生成
)
ORDER BY amount DESC;

/*
  実行順序(SQLの論理的な評価順):
  1. サブクエリ先行評価
       → SELECT user_id FROM users WHERE plan='premium'
       → 結果リスト: [1, 3]
  2. FROM orders                   → 5行読み込み
  3. WHERE user_id IN (1, 3)       → 3行に絞り込み
  4. SELECT order_id, user_id, ... → 4列を選択
  5. ORDER BY amount DESC          → 金額降順
*/
解説(テーブル変化・ポイント)
SELECT order_id, user_id, amount, status FROM orders WHERE user_id IN ( SELECT user_id FROM users WHERE plan = 'premium' ) ORDER BY amount DESC;
LEGEND
データ取得・読込対象
① メインクエリ FROM orders
FROM orders外側のメインクエリで取得対象となる orders テーブル全5行です。
1 / 6
order_iduser_idamountstatus
10118000completed
102112000completed
10323500pending
10439500completed
10546000cancelled
全 5行 読込
学習ポイント
IN サブクエリの仕組み:IN の内側で返される「値のリスト」は外側クエリより先に確定します。固定値 IN (1, 3) と論理的に等価ですが、テーブルの状態に応じて動的に変わる点が重要です。
NOT IN の NULL の罠:NOT IN のリストに NULL が1件でも含まれると、col <> NULL が UNKNOWN になり全行が除外されます。NOT IN の内側には必ず WHERE col IS NOT NULL を追加するか、NOT EXISTS を使いましょう(Q8で詳解)。
JOIN との等価変換:WHERE user_id IN (SELECT user_id FROM users WHERE plan='premium')INNER JOIN users ON orders.user_id = users.user_id WHERE users.plan='premium' と同じ結果です。結合先の列もSELECTに出す必要がない場合は IN サブクエリのほうが意図が明確です。
アンチパターン
IN の内側で複数列を返す:SELECT user_id, name FROM users のように複数列を返すと構文エラーになります。IN の比較対象と一致する単一列だけをSELECTしましょう。
NOT IN + NULL で全件0件:外部APIやETLで取り込んだデータにNULLが混入しているケースは本番で頻繁に起きます。NOT IN を使う際は内側に WHERE col IS NOT NULL を必ず追加するか、NOT EXISTS に変更してください。
実務コラム:IN / JOIN / EXISTS の使い分け基準
3つはいずれも「別テーブルの条件で絞り込む」ために使います。IN サブクエリは内側リストが小〜中規模で意図が明確なとき。JOINは結合先の列も SELECT に出したいとき。EXISTSは「存在するかどうか」だけ確認し重複を気にしないとき、かつ NULL の影響を避けたいとき。これが実務の判断目安です。
QUESTION 7

EXISTS述語(基本)— 「行が存在するか」だけを確認する最も効率的なパターン

EXISTS相関サブクエリ存在確認アクティブ検出
前提知識

WHERE EXISTS (サブクエリ) は、サブクエリが「1行でも結果を返す場合」にその行を残します。何を返すかは関係なく「行が存在するか」だけを確認するため、内側は SELECT 1 で十分です。

SELECT * FROM users u
WHERE EXISTS (
  SELECT 1                       -- 値は何でもよい。行の存在だけを確認
  FROM   orders o
  WHERE  o.user_id = u.user_id  -- 外側列を参照する「相関サブクエリ」
);
EXISTS の特徴:内側クエリが1行でも見つかった時点で即座にTRUEを返し残りの探索をやめます(短絡評価)。NULLの影響を受けない安全な書き方です。
問題

users テーブルから、status が 'completed' の注文が1件以上あるユーザーを取得してください。取得列は user_id, name, plan、user_id 昇順で返してください。

使用テーブル
▸ users
user_idnameplan
1田中 太郎premium
2佐藤 花子free
3鈴木 一郎premium
4山田 次郎standard
5伊藤 三郎free
▸ orders
order_iduser_idstatus
1011completed
1021completed
1032pending
1043completed
1054cancelled
期待出力
user_idnameplan
1田中 太郎premium
3鈴木 一郎premium
模範解答コード
SELECT
  user_id, name, plan
FROM   users u
WHERE  EXISTS (                   -- SQが1行でも返せばTRUE(短絡評価)
  SELECT 1                       -- 何を返すかは問わない。行の存在だけを確認
  FROM   orders o
  WHERE  o.user_id = u.user_id  -- 外側の u.user_id を参照する相関条件(必須)
    AND  o.status  = 'completed'
)
ORDER BY user_id;

/*
  実行順序(SQLの論理的な評価順):
  1. FROM users u                → 1行ずつ処理
  2. EXISTS(...)                 → ユーザーごとにサブクエリを実行
  3. SELECT user_id, name, plan  → 通過した行を選択
  4. ORDER BY user_id            → user_id 昇順
  */
解説(テーブル変化・ポイント)
SELECT user_id, name, plan FROM users u WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.user_id = u.user_id AND o.status = 'completed' ) ORDER BY user_id;
LEGEND
データ取得・読込対象
① メインクエリ FROM users
FROM users uメインクエリの対象となる users テーブル全5行です。この各行に対してサブクエリが評価されます。
1 / 4
user_idnameplan
1田中 太郎premium
2佐藤 花子free
3鈴木 一郎premium
4山田 次郎standard
5伊藤 三郎free
全 5行 読込
学習ポイント
EXISTS = 「1行でも存在すれば TRUE」:サブクエリが1行でも返した時点で即座に TRUE を返し探索を終了します(短絡評価)。SELECT に何を書いてもよいため、SELECT 1 が慣用的です。
相関条件が必須:EXISTS の内側に WHERE o.user_id = u.user_id という外側クエリとの結合条件を書かないと、ordersに1件でも行があれば全ユーザーが通過します。相関条件は必須です。
NULL の影響を受けない:EXISTS は「行が存在するか」の2値評価のみ。IN と違い NULL が混入しても意図通りに動作します。NULLが混在しうる本番データでは EXISTS が安全です。
アンチパターン
相関条件を書き忘れる:WHERE EXISTS (SELECT 1 FROM orders WHERE status='completed') は「ordersにcompletedが1件でもあれば」全ユーザーが通過します。外側テーブルとの結合条件 o.user_id = u.user_id を必ず書きましょう。
EXISTS の内側に SELECT * を書く:動作上は問題ありませんが SELECT * だと「全列を取得しようとしている」と誤解を招きます。SELECT 1 を使うことで「存在確認のみ」という意図が明確になります。
実務コラム:EXISTS vs IN vs JOIN の性能比較
モダンなDB(PostgreSQL, MySQL 8.0+)のオプティマイザはEXISTS・IN・JOINを同等の実行計画に変換することが多いです。とはいえ、大量データでは EXISTS が有利なことが多い理由は短絡評価(1件見つかれば即終了)にあります。また、EXISTS はNULLの罠がなく安全です。可読性と安全性の観点から EXISTS を第一候補にする習慣を身につけましょう。
QUESTION 8

NOT EXISTS述語 — NOT IN より安全な「存在しない」の確認パターン

NOT EXISTS相関サブクエリ休眠検出NOT IN との比較
前提知識

WHERE NOT EXISTS (サブクエリ) はサブクエリが「0行を返す場合」にその行を残します。EXISTS の否定形で、「別テーブルに対応する行が存在しない」ことを確認するパターンです。

SELECT * FROM users u
WHERE NOT EXISTS (
  SELECT 1
  FROM   orders o
  WHERE  o.user_id = u.user_id   -- 相関条件
);
NOT EXISTS が NOT IN より安全な理由:NOT IN のリストに NULL が混入すると全件0件になりますが、NOT EXISTS は「行が存在するか」の2値評価のみを行うため NULL の影響を受けません。本番データでは NOT EXISTS を推奨します。
問題

users テーブルから、status が 'completed' の注文が1件もないユーザーを NOT EXISTS で取得してください。取得列は user_id, name, plan、user_id 昇順で返してください。

使用テーブル
▸ users
user_idnameplan
1田中 太郎premium
2佐藤 花子free
3鈴木 一郎premium
4山田 次郎standard
5伊藤 三郎free
▸ orders
order_iduser_idstatus
1011completed
1021completed
1032pending
1043completed
1054cancelled
期待出力
user_idnameplan
2佐藤 花子free
4山田 次郎standard
5伊藤 三郎free
模範解答コード
SELECT
  user_id, name, plan
FROM   users u
WHERE  NOT EXISTS (            -- SQが0行を返す(行が存在しない)場合に通過
  SELECT 1
  FROM   orders o
  WHERE  o.user_id = u.user_id -- 外側列を参照する相関条件(必須)
    AND  o.status  = 'completed'
)
ORDER BY user_id;

/*
  実行順序(SQLの論理的な評価順):
  1. FROM users u                → 1行ずつ処理
  2. NOT EXISTS(...)             → ユーザーごとにサブクエリを実行
  3. SELECT user_id, name, plan  → 通過行を選択
  4. ORDER BY user_id            → user_id 昇順
  */
解説(テーブル変化・ポイント)
SELECT user_id, name, plan FROM users u WHERE NOT EXISTS ( SELECT 1 FROM orders o WHERE o.user_id = u.user_id AND o.status = 'completed' ) ORDER BY user_id;
LEGEND
データ取得・読込対象
① メインクエリ FROM users
FROM users uメインクエリの対象となる users テーブル全5行です。この各行に対してNOT EXISTSサブクエリが実行されます。
1 / 4
user_idnameplan
1田中 太郎premium
2佐藤 花子free
3鈴木 一郎premium
4山田 次郎standard
5伊藤 三郎free
全 5行 読込
学習ポイント
NOT EXISTS = 「行が存在しなければ通過」:EXISTS の逆です。サブクエリが0件を返す場合にのみ TRUE となります。EXISTS と対比することで理解が深まります。
NOT IN より NOT EXISTS が安全な理由:NOT IN のリストに NULL が混入すると評価が UNKNOWN になり全行が除外されます。NOT EXISTS は「行が存在するか」の2値評価のみなので NULL の影響を受けません。本番データではNOT EXISTS を第一選択にしましょう。
LEFT JOIN + IS NULL との等価変換:LEFT JOIN orders o ON u.user_id=o.user_id AND o.status='completed' WHERE o.user_id IS NULL でも同じ結果になります。ORMが生成するクエリでよく見かけるパターンです。
アンチパターン
NOT IN で NULL 混入による全件0件:外部APIからのデータに user_id=NULL の行が混入した場合、WHERE user_id NOT IN (SELECT user_id FROM orders) は全件0件になります。NOT EXISTS に変えるだけで正しく動きます。これが実務で最もよく起きるバグのひとつです。
相関条件を忘れる:NOT EXISTS 内に WHERE o.user_id = u.user_id を書かないと、ordersに1件でも行があれば全ユーザーが除外されます。相関条件は必須です。
実務コラム:休眠ユーザーの定期バッチ検出
「直近30日間にcompletedな注文がないユーザーにリマインドメールを送る」バッチ処理は、NOT EXISTS (SELECT 1 FROM orders WHERE user_id = u.user_id AND status='completed' AND ordered_at >= NOW() - INTERVAL '30 days') のように期間条件をサブクエリ内に入れるだけで実現できます。NOT EXISTS は条件の柔軟性が高く、実務バッチクエリの定番パターンです。
QUESTION 9

EXISTS 複合条件 — 複数テーブルを横断する存在確認(商品×カテゴリ×注文)

EXISTS相関サブクエリ複数テーブル購買分析
前提知識

EXISTS の内側では複数テーブルを JOIN して、より複雑な「存在確認」ができます。「この商品カテゴリの商品を注文したユーザー」のように2段階の関係を1回のEXISTSで確認するパターンです。

WHERE EXISTS (
  SELECT 1
  FROM   orders o
  JOIN   products p ON o.product_id = p.product_id
  WHERE  o.user_id  = u.user_id           -- 外側との相関条件
    AND  p.category = 'スマートフォン'  -- カテゴリ条件
)
EXISTS内のJOIN:EXISTS の内側でJOINを行っても、EXISTS は「行が存在するか」だけを確認します。行数が増えても短絡評価により効率的に動作します。
問題

users テーブルから、カテゴリが 'PC' の商品を1件でも注文したことがあるユーザーを取得してください。取得列は user_id, name, plan、user_id 昇順で返してください。

使用テーブル
▸ users
user_idnameplan
1田中 太郎premium
2佐藤 花子free
3鈴木 一郎premium
4山田 次郎standard
▸ orders(product_id付き)
order_iduser_idproduct_idstatus
10111completed
10212completed
10323pending
10434completed
10542cancelled
▸ products(category付き)
product_idnamecategory
1スマートフォン Xスマートフォン
2スマートブック ProPC
3タブレット Airタブレット
4ワイヤレスイヤホンアクセサリ
期待出力
user_idnameplan
1田中 太郎premium
4山田 次郎standard
模範解答コード
SELECT
  user_id, name, plan
FROM   users u
WHERE  EXISTS (
  SELECT 1
  FROM   orders o
  JOIN   products p ON o.product_id = p.product_id  -- 商品情報と結合
  WHERE  o.user_id  = u.user_id               -- 外側の users との相関条件
    AND  p.category = 'PC'                   -- PCカテゴリの商品を注文しているか
)
ORDER BY user_id;

/*
  実行順序(SQLの論理的な評価順):
  1. FROM users u                → 1行ずつ処理
  2. EXISTS(...)                 → ユーザーごとに PC 注文の有無を判定
  3. SELECT user_id, name, plan  → 通過行を選択
  4. ORDER BY user_id            → user_id 昇順
  */
解説(テーブル変化・ポイント)
SELECT user_id, name, plan FROM users u WHERE EXISTS ( SELECT 1 FROM orders o JOIN products p ON o.product_id = p.product_id WHERE o.user_id = u.user_id AND p.category = 'PC' ) ORDER BY user_id;
LEGEND
データ取得・読込対象
① メインクエリ FROM users
FROM users uメインクエリの対象となる users テーブルです。
1 / 5
user_idnameplan
1田中 太郎premium
2佐藤 花子free
3鈴木 一郎premium
4山田 次郎standard
全 4行 読込
学習ポイント
EXISTS 内部でのJOIN:EXISTS のサブクエリ内でも通常のJOINが使えます。これにより「2つのテーブルを経由した存在確認」が1回のEXISTSで可能になります。行数が多くても短絡評価で効率的です。
IN サブクエリとの等価変換:この問題は WHERE user_id IN (SELECT o.user_id FROM orders o JOIN products p ON o.product_id=p.product_id WHERE p.category='PC') と書いても同じ結果です。ただし EXISTS のほうがNULLに対して安全です。
複数存在確認を AND でつなぐ:WHERE EXISTS (...) AND EXISTS (...) のように複数のEXISTSをANDで連結すると「AもBも注文したユーザー」を取得できます。INのサブクエリで同じことをしようとすると複雑なINTERSECTが必要になり、EXISTSのほうが自然に書けます。
アンチパターン
EXISTS内でSELECTした列を外側で参照しようとする:EXISTS のサブクエリ内でSELECTした列は外側クエリからは参照できません。EXISTSは「存在確認のみ」です。値を取り出したい場合はスカラーサブクエリや JOIN を使いましょう。
相関条件にテーブルエイリアスを付け忘れる:WHERE user_id = u.user_id と書いて結合が曖昧になるケースがあります。EXISTS内で使うテーブルには必ずエイリアスを付け、o.user_id = u.user_id のように明示しましょう。
実務コラム:「〜のカテゴリを購入したユーザー」の分析クエリ
ECサイトでは「特定カテゴリを買ったユーザーへの関連商品レコメンド」が頻出です。EXISTS + JOIN パターンはこの典型的なユースケースです。さらに AND NOT EXISTS (SELECT 1 FROM orders o2 WHERE o2.user_id=u.user_id AND o2.product_id=X) を追加すれば「PCを買ったが商品Xはまだ買っていないユーザー」にも絞れます。述語の組み合わせで複雑なビジネス条件を表現できます。
QUESTION 10

複合述語の総合問題 — 比較・IN・EXISTS を組み合わせた実務クエリ

複合述語EXISTS + IN実務総合購買ユーザー分析
前提知識

実務のクエリは複数の述語を組み合わせて複雑な条件を表現します。比較述語で数値範囲を絞りIN で特定カテゴリを選びEXISTS で注文の存在を確認するという3段階フィルタのパターンを学びます。

WHERE p.price       >= 50000               -- ① 比較述語:価格範囲
  AND p.category    IN ('PC', 'タブレット')  -- ② IN述語:カテゴリ選択
  AND EXISTS (                               -- ③ EXISTS述語:注文の存在確認
    SELECT 1 FROM orders o
    WHERE  o.product_id = p.product_id
      AND  o.status = 'completed'
  )
述語の評価順序とAND の短絡評価:DBのオプティマイザは最もコストの低い条件から評価します。インデックスが有効な比較述語・INを先に評価し、コストの高いEXISTSを後で評価する最適化が行われます。
問題

products テーブルから、price が 50,000 以上かつcategory が 'PC' または 'タブレット'の商品のうち、completedの注文が1件以上ある商品を取得してください。取得列は product_id, name, category, price、price 降順で返してください。

使用テーブル
▸ products
product_idnamecategorypricestock
1スマートフォン Xスマートフォン8980050
2スマートブック ProPC1280000
3タブレット Airタブレット6480030
4ワイヤレスイヤホンアクセサリ12800100
5USBケーブルアクセサリ980200
6ゲーミングPC ProPC1980005
▸ orders(product_id付き)
order_idproduct_idstatus
1011completed
1022completed
1033pending
1046completed
1052cancelled
期待出力
product_idnamecategoryprice
6ゲーミングPC ProPC198000
2スマートブック ProPC128000
模範解答コード
SELECT
  product_id, name, category, price
FROM   products p
WHERE  p.price    >= 50000              -- ① 比較述語:5万円以上(インデックス有効)
  AND  p.category IN ('PC', 'タブレット')    -- ② IN述語:対象カテゴリのみ
  AND  EXISTS (                         -- ③ EXISTS述語:completed注文が存在するか
    SELECT 1
    FROM   orders o
    WHERE  o.product_id = p.product_id  -- 外側の商品との相関条件
      AND  o.status      = 'completed'  -- 完了済み注文のみ確認
  )
ORDER BY p.price DESC;

/*
  実行順序(SQLの論理的な評価順):
  1. FROM products p                           → 6行読み込み
  2. WHERE p.price のしきい値                       → 価格で絞り込み
  3. AND p.category IN (...)                   → カテゴリで絞り込み
  4. AND EXISTS (...)                          → completed 注文ありで絞り込み
  5. SELECT product_id, name, category, price  → 4列を選択
  6. ORDER BY p.price DESC                     → 価格降順
  */
解説(テーブル変化・ポイント)
SELECT product_id, name, category, price FROM products p WHERE p.price >= 50000 AND p.category IN ('PC', 'タブレット') AND EXISTS ( SELECT 1 FROM orders o WHERE o.product_id = p.product_id AND o.status = 'completed' ) ORDER BY p.price DESC;
LEGEND
データ取得・読込対象
① メインクエリ FROM products
FROM products pメインクエリの対象となる products テーブル全6行です。3段階のフィルタを順に適用していきます。
1 / 7
product_idnamecategoryprice
1スマートフォン Xスマートフォン89800
2スマートブック ProPC128000
3タブレット Airタブレット64800
4ワイヤレスイヤホンアクセサリ12800
5USBケーブルアクセサリ980
6ゲーミングPC ProPC198000
全 6行 読込
学習ポイント
述語の組み合わせパターン:比較述語(価格絞り込み)→ IN述語(カテゴリ選択)→ EXISTS述語(注文存在確認)という段階的フィルタは実務で最も頻出の構造です。各述語の役割を明確に分離して書くことが可読性の鍵です。
評価順序とオプティマイザ:DB は AND で連結した条件をコストが低い順に評価します。インデックスが使える比較述語・IN を先に評価してから、コストが高い EXISTS を後で評価するよう最適化されます。EXPLAIN で実行計画を確認してみましょう。
AND / OR の括弧と優先順位:ANDOR より優先度が高いです。WHERE A OR B AND CWHERE A OR (B AND C) と解釈されます。意図通りに動かすために、複数の述語を組み合わせる際は括弧で明示的にグループ化する習慣が重要です。
アンチパターン
AND / OR の優先順位ミス:WHERE price >= 50000 AND category = 'PC' OR category = 'タブレット'WHERE (price >= 50000 AND category = 'PC') OR category = 'タブレット' と解釈されます。タブレットの価格条件が無効になる意図せぬ結果になります。括弧を使って category IN ('PC', 'タブレット') と書くことで解決します。
EXISTSとINを使い分けずどちらかに統一しようとする:両者はどちらも有効ですが適した場面が異なります。「存在確認だけでよい + NULLが混在しうる」場面ではEXISTS、「小規模な固定リストの一致確認」ではIN、の使い分けが実務の目安です。
実務コラム:述語を組み合わせて複雑なビジネス要件を表現する
「price帯絞り込み × カテゴリ選択 × 購買実績あり」という組み合わせは、ECの商品検索・レコメンド・在庫管理など幅広い場面で登場します。SQL述語の本質は「どの行を残すか」をDeclarative(宣言的)に記述することです。比較・IN・EXISTS・IS NULL・BETWEENという基本述語を自在に組み合わせることで、ほぼすべてのビジネスロジックをWHERE句で表現できます。これが「SQLを読める・書ける」の実務レベルです。