SQL 述語(Predicate) — INサブクエリ・ANY/ALLの応用

応用述語 (Predicate)EXISTS / NOT EXISTSIN サブクエリANY / ALLIS DISTINCT FROMPostgreSQL対応全5問
1 / 5 · 学習中 0 / 5 問完了
QUESTION 1

EXISTS述語 — 相関サブクエリで「1件でも存在するか」を判定する

EXISTS相関サブクエリ存在チェックセミジョイン
前提知識

EXISTS 述語はサブクエリが1行以上の結果を返せば TRUE、0件なら FALSE を返します。通常、外側クエリの列を内側サブクエリに持ち込む相関サブクエリ(Correlated Subquery)と組み合わせて使います。

WHERE EXISTS (
  SELECT 1               -- SELECT の中身は評価されない(1 / * / NULL でも同じ)
  FROM   orders o
  WHERE  o.user_id = u.user_id  -- 外側の u を内側に持ち込む「相関条件」
    AND  o.status  = 'completed'
)
短絡評価(Short-circuit):EXISTS は1行ヒットした時点で即 TRUE を返して停止します。全件カウントする COUNT(*) > 0 より大幅に効率的です。
問題

users テーブルから、completed(完了済み)の注文を 1 件以上持つユーザーを取得してください。取得列は user_id, name, plan、user_id 昇順で返してください。

使用テーブル
▸ users
user_idnameplan
1田中太郎premium
2佐藤花子free
3鈴木一郎premium
4山田次郎free
5高橋三郎free
▸ orders
order_iduser_idamountstatusordered_at
101115000completed2024-05-01
10218000cancelled2024-05-10
10325000pending2024-05-15
104322000completed2024-05-20
10543000cancelled2024-05-22
10639000completed2024-05-25
107512000completed2024-05-28
期待出力
user_idnameplan
1田中太郎premium
3鈴木一郎premium
5高橋三郎free
模範解答コード
SELECT
  u.user_id, u.name, u.plan
FROM   users u
WHERE  EXISTS (
  SELECT 1                        -- SELECT * / 'x' / NULL でも動作は同じ
  FROM   orders o
  WHERE  o.user_id = u.user_id   -- 相関条件: 外側の u を内側に持ち込む
    AND  o.status  = 'completed'
)
ORDER BY u.user_id;

/*
  実行順序(SQLの論理的な評価順):
  1. FROM users u                      → 外側 5行をスキャン
  2. [各行に対して] EXISTS 相関サブクエリ           → completed 注文を短絡評価
  3. WHERE EXISTS (...)                → TRUE の行を通過
  4. SELECT u.user_id, u.name, u.plan  → 3列を選択
  5. ORDER BY u.user_id                → user_id 昇順
  */
解説(テーブル変化・ポイント)
SELECT u.user_id, u.name, u.plan FROM users u WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.user_id = u.user_id AND o.status = 'completed' ) ORDER BY u.user_id;
LEGEND
データ取得・読込対象
① FROM users u
FROM users uusersテーブル全5行を外側テーブルとして読み込みます。この後、各ユーザー行に対してEXISTSの相関サブクエリが個別に実行されます。
1 / 4
user_idnameplan
1田中太郎premium
2佐藤花子free
3鈴木一郎premium
4山田次郎free
5高橋三郎free
外側テーブル: 5行読込
学習ポイント
短絡評価(Short-circuit):EXISTS は内側のサブクエリが1行ヒットした時点で即 TRUE を返して停止します。全件スキャンして結果をカウントする COUNT(*) > 0 より大幅に効率的です。
SELECT の中身は何でもよい:EXISTS (SELECT 1 ...) の SELECT 句の値は一切評価されません。SELECT * / SELECT NULL / SELECT 'x' でも動作は完全に同じです。慣習として SELECT 1 が最もよく使われます。
相関条件の役割:WHERE o.user_id = u.user_id が「外側テーブルの列を内側サブクエリに持ち込む相関条件」です。この条件がないと内側クエリが全行を走査して常に TRUE になり、すべての外側行が返ってしまいます。
EXISTS vs IN:非相関サブクエリなら user_id IN (SELECT user_id FROM ...) と EXISTS は等価です。現代のオプティマイザはどちらも同様の実行計画に変換することが多く、相関条件がある場合は EXISTS の方が自然な表現です(Q3参照)。
アンチパターン
EXISTS(SELECT COUNT(*) ...) > 0:EXISTS は結果の件数を返しません。サブクエリ内で COUNT をとってから比較するのは二度手間で、最初の1件を見つけた後も全件スキャンが走ります。WHERE EXISTS (...) と書くだけで十分です。
相関条件の書き忘れ:WHERE EXISTS (SELECT 1 FROM orders WHERE status = 'completed') は orders テーブルに完了注文が1件でもあれば全ユーザーが返るバグです。必ず o.user_id = u.user_id のような相関条件を追加してください。
実務コラム:EXISTS とセミジョインの違い
EXISTS は内部的にセミジョイン(Semi-Join)として実行されます。INNER JOIN との違いは重複の扱いで、1ユーザーが複数の注文を持つ場合 JOIN だと同じユーザーが複数行返ってしまいます(DISTINCT が必要)。EXISTS は重複を自動的に排除するため、「〜を持つユーザー」「〜が存在する行を取得する」には常に EXISTS が JOIN より適切です。
QUESTION 2

NOT EXISTS述語 — アンチジョインパターンと NOT IN の NULL 罠

NOT EXISTSアンチジョイン非存在チェックNOT IN 危険
前提知識

NOT EXISTS は「対応する行が1件も存在しない」行を返します。INNER JOIN の逆にあたるアンチジョイン(Anti-Join)パターンで使われます。

WHERE NOT EXISTS (
  SELECT 1
  FROM   order_items oi
  WHERE  oi.product_id = p.product_id  -- 1件でもあれば FALSE、なければ TRUE
)
NOT IN + NULL = 全件 0 件(致命的バグ):NOT IN のリストに NULL が1件でも含まれると x <> NULL = UNKNOWN となり、AND 全体が UNKNOWN → 全行除外されます。NOT EXISTS はこの罠を回避します。
問題

products テーブルから、一度も注文されたことがない商品を取得してください。order_items テーブルとの照合に NOT EXISTS を使い、取得列は product_id, name, category, price、product_id 昇順で返してください。

使用テーブル
▸ products
product_idnamecategoryprice
1スマートフォン Xスマートフォン89800
2スマートブック ProPC128000
3タブレット Airタブレット64800
4ワイヤレスイヤホンアクセサリ12800
5USBケーブルアクセサリ980
6ゲーミングPC ProPC198000
▸ order_items
item_idorder_idproduct_idquantity
110111
210142
310421
410631
510711
6105NULL1
期待出力
product_idnamecategoryprice
5USBケーブルアクセサリ980
6ゲーミングPC ProPC198000
模範解答コード
SELECT
  p.product_id, p.name, p.category, p.price
FROM   products p
WHERE  NOT EXISTS (
  SELECT 1
  FROM   order_items oi
  WHERE  oi.product_id = p.product_id  -- 相関条件: 1件でもあれば FALSE
)
ORDER BY p.product_id;

-- ✗ 誤った書き方(order_items.product_id に NULL が含まれると 0件になる):
-- WHERE p.product_id NOT IN (SELECT product_id FROM order_items)

/*
  実行順序(SQLの論理的な評価順):
  1. FROM products p                     → 外側: 6行スキャン
  2. [各行に対して] NOT EXISTS 相関サブクエリ:
       FROM order_items oi
       WHERE oi.product_id = p.product_id → 1件でもヒット → FALSE(除外)
                                            0件             → TRUE(通過)
  3. WHERE NOT EXISTS (...)              → TRUE の 2行を通過(product 5, 6)
  4. SELECT p.product_id, p.name, ...   → 4列を選択
  5. ORDER BY p.product_id              → ID 昇順
*/
解説(テーブル変化・ポイント)
SELECT p.product_id, p.name, p.category, p.price FROM products p WHERE NOT EXISTS ( SELECT 1 FROM order_items oi WHERE oi.product_id = p.product_id ) ORDER BY p.product_id;
LEGEND
データ取得・読込対象
① FROM products p
FROM products pproductsテーブル全6行を外側テーブルとして読み込みます。一度も注文されていない商品を見つけます。
1 / 4
product_idnamecategoryprice
1スマートフォン Xスマートフォン89800
2スマートブック ProPC128000
3タブレット Airタブレット64800
4ワイヤレスイヤホンアクセサリ12800
5USBケーブルアクセサリ980
6ゲーミングPC ProPC198000
外側テーブル: 6行読込
学習ポイント
NOT EXISTS はアンチジョインの最安全実装:「対応する行が1件も存在しない」行を返すパターンをアンチジョイン(Anti-Join)と呼びます。NOT EXISTS は NULL が存在しても正しく動作し、安全性が最も高いです。
NOT IN + NULL = 全件 0 件(致命的バグ):x NOT IN (..., NULL)x <> a AND x <> b AND ... AND x <> NULL に展開されます。x <> NULL は常に UNKNOWN → AND 全体が UNKNOWN → 全行除外。これは最も見逃されやすい実務バグのひとつです。
3種のアンチジョインパターン:NOT EXISTS (...)、②NOT IN (SELECT ... WHERE col IS NOT NULL)(NULL除外が必須)、③LEFT JOIN ... WHERE right_key IS NULL、の3つは同じ結果を返します。推奨は可読性と安全性の高い NOT EXISTS です。
アンチパターン
NOT IN に nullable カラムを持つサブクエリを使う:WHERE product_id NOT IN (SELECT product_id FROM order_items)product_id に NULL が1行でも含まれると全件 0件になります。常に NOT EXISTS か、内側に WHERE col IS NOT NULL を追加した NOT IN を使いましょう。
「このカラムに NULL は入らない」という前提のコード:スキーマ制約やアプリバリデーションで NULL 防いでいても、データ移行・バグ・DB設定変更で NULL が混入することがあります。アンチジョインには常に NOT EXISTS を選ぶ習慣が重要です。
実務コラム:3パターンのアンチジョインとパフォーマンス
PostgreSQL では NOT EXISTS と LEFT JOIN IS NULL はほぼ同じ実行計画(Anti Merge Join / Anti Hash Join)に変換されます。EXPLAIN ANALYZE で確認し「Anti Join」が出ていれば最適です。NOT IN (サブクエリ) は NULL が保証されていない限り使わないようにしましょう。大量テーブルでは NOT EXISTS + 外部キーのインデックスが最も効率的なケースが多いです。
QUESTION 3

IN(サブクエリ)述語 — 動的集合によるセミジョインと NOT IN の NULL 罠

IN サブクエリセミジョイン動的リストNOT IN 危険
前提知識

IN (サブクエリ) はサブクエリが返す集合に列の値が含まれるかを評価します。固定リストの代わりにテーブルの現在状態から動的に集合を生成できるため、実務で頻繁に使われます。

WHERE o.user_id IN (
  SELECT user_id        -- サブクエリで「プレミアム会員のIDリスト」を動的生成
  FROM   users
  WHERE  plan = 'premium'
)
-- ↑ 非相関サブクエリ: 1回だけ評価されて集合を展開、EXISTS と等価になることが多い
NOT IN + NULL 罠(再掲):サブクエリが NULL を返す場合、NOT IN は全行 UNKNOWN → 0件になります。NOT IN は必ず NOT EXISTSWHERE col IS NOT NULL 付きで使いましょう。
問題

orders テーブルから、プレミアム会員(plan = 'premium')が行った注文を取得してください。users テーブルへのサブクエリを使い、取得列は order_id, user_id, amount, status, ordered_at、ordered_at 昇順で返してください。

使用テーブル
▸ orders
order_iduser_idamountstatusordered_at
101115000completed2024-05-01
10218000cancelled2024-05-10
10325000pending2024-05-15
104322000completed2024-05-20
10543000cancelled2024-05-22
10639000completed2024-05-25
107512000completed2024-05-28
▸ users
user_idnameplan
1田中太郎premium
2佐藤花子free
3鈴木一郎premium
4山田次郎free
5高橋三郎free
期待出力
order_iduser_idamountstatusordered_at
101115000completed2024-05-01
10218000cancelled2024-05-10
104322000completed2024-05-20
10639000completed2024-05-25
模範解答コード
SELECT
  o.order_id, o.user_id, o.amount, o.status, o.ordered_at
FROM   orders o
WHERE  o.user_id IN (
  SELECT user_id          -- プレミアム会員の user_id を動的に取得
  FROM   users
  WHERE  plan = 'premium'  -- サブクエリ結果: {1, 3}
)
ORDER BY o.ordered_at;

/*
  実行順序(SQLの論理的な評価順):
  1. サブクエリ評価(非相関)                       → premium ユーザーを1回だけ取得
  2. FROM orders o                      → 7行読み込み
  3. WHERE o.user_id IN (...)           → 該当ユーザーの注文に絞り込み
  4. SELECT o.order_id, o.user_id, ...  → 5列を選択
  5. ORDER BY o.ordered_at              → 日付昇順
  */
解説(テーブル変化・ポイント)
SELECT o.order_id, o.user_id, o.amount, o.status, o.ordered_at FROM orders o WHERE o.user_id IN ( SELECT user_id FROM users WHERE plan = 'premium' ) ORDER BY o.ordered_at;
LEGEND
評価対象の列・キー
除外・非表示データ
✓ 通過
✗ 除外
① サブクエリ実行(動的集合の生成)
SELECT user_id FROM users WHERE plan = 'premium'INの内部にある非相関サブクエリがまず1回だけ実行され、プレミアム会員のIDリスト(動的集合)を生成します。
1 / 4
user_idnameplanIN集合の要素
1田中太郎premium✓ 追加 (1)
2佐藤花子free✗ 除外
3鈴木一郎premium✓ 追加 (3)
4山田次郎free✗ 除外
5高橋三郎free✗ 除外
生成された集合: {1, 3}
学習ポイント
IN(サブクエリ)= セミジョイン:非相関サブクエリの IN (SELECT ...) は内側を1回だけ評価して集合を生成し、外側クエリの各行と照合します。実行計画上では Hash Semi Join / Merge Semi Join として現れることが多いです。
IN vs EXISTS の等価性:非相関サブクエリの場合 WHERE user_id IN (...)WHERE EXISTS (...) は等価です。現代のオプティマイザは相互変換を行うため、どちらも同様の実行計画になることが多く、可読性で選んで問題ありません。
相関 vs 非相関:IN のサブクエリが外側列を参照しない(非相関)なら1回だけ実行されます。相関する IN は外側の各行ごとに実行されるため、EXISTS との等価性が成り立たないケースもあり、その場合は EXISTS の方が意図が明確です。
アンチパターン
NOT IN + NULL 罠(再掲):通り、NOT IN (SELECT nullable_col ...) は NULL が含まれると全件 0件になります。動的なサブクエリでも固定リストでも同じ罠があります。NOT IN を使う場合は必ず WHERE col IS NOT NULL を内側に追加するか、NOT EXISTS に切り替えましょう。
アプリ側で大量 ID を IN に渡す:バックエンドで数千〜数万件の ID を配列に集めて WHERE id IN (1, 2, ..., 50000) と埋め込むと、SQL のパース・プランニングコストが激増します。一時テーブル / UNNEST / JOIN など、大量集合に特化した設計に切り替えましょう。
実務コラム:アプリから動的に IN を組み立てる
PostgreSQL では WHERE user_id = ANY($1::int[]) に配列を1つのバインド変数として渡せます(任意の長さで安全)。Node.js / Prisma / Drizzle などの ORM も内部でこのパターンを使用します。固定リストの手書き IN はアプリの状態と乖離しやすいため、テーブルの現在状態に基づく IN (subquery) パターンへの切り替えを検討しましょう。
QUESTION 4

ALL / ANY述語 — サブクエリ結果の全体・部分との比較(∀ と ∃)

ALLANY集合比較∀ / ∃ 量化子
前提知識

ANY(= SOME)はサブクエリの集合の少なくとも1件に対して比較が真、ALL はすべての件に対して比較が真のときに TRUE を返します。

WHERE price > ANY (SELECT price FROM products WHERE category = 'アクセサリ')
-- ↑ price > MIN(アクセサリ価格) と等価: 「いずれかより高い」= 集合内の最小値より大きい

WHERE price > ALL (SELECT price FROM products WHERE category = 'アクセサリ')
-- ↑ price > MAX(アクセサリ価格) と等価: 「すべてより高い」= 集合内の最大値より大きい
= ANY は IN と等価:category = ANY (ARRAY['PC', 'タブレット'])IN ('PC', 'タブレット') と完全に等価です。比較演算子に >< を使った場合が ANY / ALL の真価です。
問題

products テーブルから、アクセサリカテゴリのすべての商品より高額な商品ALL 述語を使って取得してください。取得列は product_id, name, category, price、price 昇順で返してください。

使用テーブル
▸ products
product_idnamecategorypricestock
1スマートフォン Xスマートフォン8980050
2スマートブック ProPC1280000
3タブレット Airタブレット6480030
4ワイヤレスイヤホンアクセサリ12800100
5USBケーブルアクセサリ980200
6ゲーミングPC ProPC1980005
期待出力
product_idnamecategoryprice
3タブレット Airタブレット64800
1スマートフォン Xスマートフォン89800
2スマートブック ProPC128000
6ゲーミングPC ProPC198000
模範解答コード
SELECT
  product_id, name, category, price
FROM   products
WHERE  price > ALL (
  SELECT price                       -- アクセサリ全商品の価格リスト: {12800, 980}
  FROM   products
  WHERE  category = 'アクセサリ'
)
ORDER BY price;                      -- ↑ price > MAX(アクセサリ価格) = price > 12800 と等価

/*
  実行順序(SQLの論理的な評価順):
  1. サブクエリ評価                       → アクセサリの価格集合を生成
  2. FROM products                 → 6行読み込み
  3. WHERE price > ALL (...)       → 最大値超で絞り込み
  4. SELECT product_id, name, ...  → 4列を選択
  5. ORDER BY price                → 価格昇順
  */
解説(テーブル変化・ポイント)
SELECT product_id, name, category, price FROM products WHERE price > ALL ( SELECT price FROM products WHERE category = 'アクセサリ' ) ORDER BY price;
LEGEND
評価対象の列・キー
① サブクエリ実行(比較用集合の生成)
SELECT price FROM products WHERE category = 'アクセサリ'サブクエリを実行して、比較対象となる「アクセサリ」の価格集合を取得します。
1 / 4
product_idnamecategoryprice(集合に追加)
4ワイヤレスイヤホンアクセサリ12800
5USBケーブルアクセサリ980
サブクエリ結果: {12800, 980}
学習ポイント
ANY は「∃(存在量化子)」:price > ANY (集合) は「集合の中の少なくとも1つより price が大きい」→ price > MIN(集合) と等価です。また = ANYIN と完全に等価です。
ALL は「∀(全称量化子)」:price > ALL (集合) は「集合のすべての要素より price が大きい」→ price > MAX(集合) と等価です。また <> ALL は NULL なしの前提で NOT IN と等価です。
空集合に対する真理値:ANY(空集合) は常に FALSE(比較できる要素がない)、ALL(空集合) は常に TRUE(空虚な真: Vacuous Truth)です。ALL のサブクエリが空になりうる場合は注意が必要です。
アンチパターン
ALL に空集合が返る設計:アクセサリが 0件の場合 price > ALL(空集合) は常に TRUE となり全商品が返ります。WHERE 条件が意図と逆になるバグです。サブクエリが空になりうる場合は EXISTS で事前チェックするか、COALESCE でデフォルト値を設けましょう。
ANY / ALL より MAX / MIN の方が最適化されやすい:price > ANY (...)price > (SELECT MIN(price) ...) と等価ですが、データベースによってはスカラーサブクエリの方がインデックス活用の最適化が容易です。EXPLAIN で確認して遅い場合は MIN / MAX への書き換えを検討しましょう。
実務コラム:= ANY と IN の使い分け
PostgreSQL では col = ANY($1::int[]) で配列型パラメータを直接渡せます(IN のリストを配列で渡す最もクリーンな方法)。一方 = ANY(サブクエリ)IN(サブクエリ) はオプティマイザが同一視するため、可読性の高い IN を使うのが一般的です。> ANY / > ALL のような比較演算子との組み合わせは、MIN / MAX サブクエリへ明示的に書き直すとクエリの意図が伝わりやすくなります。
QUESTION 5

IS DISTINCT FROM述語 — NULL安全な等価比較と <> の落とし穴

IS DISTINCT FROMNULL安全比較NULL処理<> の落とし穴
前提知識

IS DISTINCT FROM は NULL を含む等価比較を安全に行う述語です。通常の <> は NULL との比較が UNKNOWN になり行が除外されますが、IS DISTINCT FROM は NULL を「明確に異なる値」として TRUE を返します。

-- a IS DISTINCT FROM b の真理値表
'WINTER20' IS DISTINCT FROM 'SUMMER10'  -- → TRUE  (値が異なる)
NULL       IS DISTINCT FROM 'SUMMER10'  -- → TRUE  (NULL は「異なる」扱い)
'SUMMER10' IS DISTINCT FROM 'SUMMER10'  -- → FALSE (同じ値)
NULL       IS DISTINCT FROM NULL        -- → FALSE (両方 NULL は「同じ」扱い)
<> の落とし穴:coupon_code <> 'SUMMER10' では coupon_code が NULL の行は UNKNOWN → 除外されます。「クーポン未使用(NULL)の行も含めたい」場合は IS DISTINCT FROM を使います。
問題

orders テーブルから、coupon_code が 'SUMMER10' とは異なる注文(クーポン未使用の NULL 行も含む)を取得してください。IS DISTINCT FROM を使い、取得列は order_id, user_id, amount, coupon_code, ordered_at、order_id 昇順で返してください。

使用テーブル
▸ orders
order_iduser_idamountcoupon_codeordered_at
101115000SUMMER102024-05-01
10218000NULL2024-05-10
10325000NULL2024-05-15
104322000WINTER202024-05-20
10543000NULL2024-05-22
10639000NULL2024-05-25
107512000SUMMER102024-05-28
期待出力
order_iduser_idamountcoupon_codeordered_at
10218000NULL2024-05-10
10325000NULL2024-05-15
104322000WINTER202024-05-20
10543000NULL2024-05-22
10639000NULL2024-05-25
模範解答コード
SELECT
  order_id, user_id, amount, coupon_code, ordered_at
FROM   orders
WHERE  coupon_code IS DISTINCT FROM 'SUMMER10'
--              ↑ NULL安全比較: NULL は「SUMMER10 と異なる」= TRUE として扱う
--   ✗ coupon_code <> 'SUMMER10' はNULL行がUNKNOWN→除外されてしまう
ORDER BY order_id;

/*
  実行順序(SQLの論理的な評価順):
  1. FROM orders                                    → 7行読み込み
  2. WHERE coupon_code IS DISTINCT FROM 'SUMMER10'  → 一致以外を通過
  3. SELECT order_id, user_id, amount, ...          → 5列を選択
  4. ORDER BY order_id                              → order_id 昇順

  ▸ IS DISTINCT FROM 真理値表(再掲):
      a = b             → FALSE  (同値)
      a ≠ b             → TRUE   (異なる値)
      NULL vs 非NULL    → TRUE   (NULLは「異なる」)
      NULL vs NULL      → FALSE  (両方NULLは「同じ」)
*/
解説(テーブル変化・ポイント)
SELECT order_id, user_id, amount, coupon_code, ordered_at FROM orders WHERE coupon_code IS DISTINCT FROM 'SUMMER10' ORDER BY order_id;
LEGEND
データ取得・読込対象
① FROM orders
FROM ordersordersテーブル全7行を読み込みます。coupon_code列にはNULL(クーポン未使用)が含まれています。
1 / 3
order_iduser_idcoupon_codeordered_at
1011SUMMER102024-05-01
1021NULL2024-05-10
1032NULL2024-05-15
1043WINTER202024-05-20
1054NULL2024-05-22
1063NULL2024-05-25
1075SUMMER102024-05-28
全 7行 読込
学習ポイント
IS DISTINCT FROM の真理値表:① 同一値 → FALSE、② 異なる値 → TRUE、③ NULL vs 非 NULL → TRUE(NULL は「異なる」)、④ NULL vs NULL → FALSE(両方 NULL は「同じ」)。<> と違い、2値(TRUE/FALSE)を常に返します。
<> との根本的な違い:NULL <> 'SUMMER10' は UNKNOWN を返し WHERE 句で除外されます(三値論理 基礎編Q4参照)。NULL IS DISTINCT FROM 'SUMMER10' は TRUE を返しその行を含めます。NULL を「値が設定されていない = 比較対象と異なる」とみなしたい場合は IS DISTINCT FROM を使います。
COALESCE との使い分け:COALESCE(coupon_code, '') <> 'SUMMER10' でも NULL 行を通過させられますが、デフォルト値が比較値と同じ場合に意図しない結果になるリスクがあります。IS DISTINCT FROM の方が汎用的で安全です。
アンチパターン
col <> 'value' で NULL 行が漏れる:nullable なカラムに <> を使うと NULL 行は UNKNOWN となりフィルタから漏れます。「X でもなく NULL でもない行を取得」のつもりが NULL 行が除外される、というのは本番環境で最も発見されにくいバグのひとつです。
COALESCE のデフォルト値が比較値と衝突する:COALESCE(coupon_code, 'SUMMER10') <> 'SUMMER10' とすると、NULL 行が 'SUMMER10' に変換されて除外されてしまいます(意図と逆)。COALESCE を使う場合はデフォルト値を比較値と衝突しない値にする必要があります。
実務コラム:IS DISTINCT FROM の DBMS 別サポートと代替構文
IS DISTINCT FROM は SQL:1999 標準で、PostgreSQL・DuckDB・CockroachDB などで直接使用できます。MySQL 8.0 以降には <=>(NULL-safe equal)があり NOT (a <=> b) が IS DISTINCT FROM と等価です。SQL Server 2022 以降は IS DISTINCT FROM が使えますが、それ以前は CASE WHEN a = b OR (a IS NULL AND b IS NULL) THEN 0 ELSE 1 END = 1 のような冗長な書き方が必要でした。対象 DB に合わせて適切な代替構文を用意しておきましょう。