この内容は古いバージョンです。最新バージョンを表示するには、戻るボタンを押してください。
バージョン:7
ページ更新者:T
更新日時:2026-06-11 07:07:02

タイトル: WHERE句
SEOタイトル: SQL WHERE 句完全ガイド

この記事の要点
  • WHERE 句SELECT/UPDATE/DELETE の対象行を絞り込む
  • 比較演算子 =, <>, <, >, <=, >= と論理 AND/OR/NOT が基本
  • パターン: LIKE (%/_)、INBETWEENIS NULLEXISTS
  • 性能: SARGable な書き方(WHERE date >= ?)でインデックスを効かせる
  • SQL Injection 対策に Prepared Statement / バインドパラメータ必須

WHERE 句とは

WHERE 句は SQL の SELECT / UPDATE / DELETE 文で対象行を絞り込むための条件指定です。条件は各行に対して評価され、true となった行だけが結果に含まれます。

基本構文

SELECT カラム FROM テーブル WHERE 条件;
UPDATE テーブル SET カラム = 値 WHERE 条件;
DELETE FROM テーブル WHERE 条件;

-- 例
SELECT * FROM users WHERE id = 1;
SELECT id, name FROM users WHERE age >= 20 AND status = 'active';
UPDATE users SET status = 'inactive' WHERE last_login < '2023-01-01';
DELETE FROM logs WHERE created_at < '2024-01-01';

比較演算子

演算子意味
=等しいid = 1
<> / !=等しくないstatus <> 'deleted'
<未満age < 18
>より大きいscore > 80
<=以下price <= 1000
>=以上created_at >= '2024-01-01'

論理演算子 (AND / OR / NOT)

-- AND: 両方真
SELECT * FROM users WHERE age >= 20 AND status = 'active';

-- OR: いずれか真
SELECT * FROM users WHERE role = 'admin' OR role = 'editor';

-- NOT: 否定
SELECT * FROM users WHERE NOT (status = 'deleted');

-- 優先順位: NOT > AND > OR → 括弧で明示推奨
SELECT * FROM products
WHERE (category = 'food' OR category = 'drink')
  AND price < 1000;

-- 括弧無しだと意図と異なる
WHERE category = 'food' OR category = 'drink' AND price < 1000
-- ⇔ WHERE category = 'food' OR (category = 'drink' AND price < 1000)

LIKE (パターンマッチ)

ワイルドカード意味
%0 文字以上の任意の文字列
_任意の 1 文字
-- 前方一致 (インデックス効く)
WHERE name LIKE 'Yam%'      -- Yamada, Yamaguchi, Yamamoto

-- 後方一致 (インデックス効かない)
WHERE name LIKE '%hi'

-- 部分一致 (インデックス効かない)
WHERE name LIKE '%am%'

-- 任意 1 文字
WHERE code LIKE 'A_01'      -- AB01, AC01, A001 等

-- エスケープ
WHERE name LIKE '50\%引%' ESCAPE '\'   -- "50%引" で始まる
WHERE name LIKE '50!%引%' ESCAPE '!'   -- 同上 (ESCAPE 文字を ! に)

-- 大文字小文字
WHERE name LIKE 'yam%'      -- MySQL/MariaDB: utf8_general_ci なら大小区別なし
WHERE LOWER(name) LIKE 'yam%'  -- 明示的に小文字化 (インデックス効かない)

IN / NOT IN

-- リストに含まれる
WHERE status IN ('active', 'pending')
WHERE id IN (1, 2, 3, 4, 5)

-- リストに含まれない
WHERE status NOT IN ('deleted', 'banned')

-- サブクエリ
WHERE user_id IN (
    SELECT id FROM users WHERE status = 'active'
)

-- ★ NULL の罠
WHERE id NOT IN (1, 2, NULL)
-- → 常に何もマッチしない (NULL との比較は UNKNOWN)
-- 対処: NULL を含まないようにする or NOT EXISTS を使う

BETWEEN

-- 範囲指定 (両端含む)
WHERE price BETWEEN 1000 AND 5000
-- ⇔ WHERE price >= 1000 AND price <= 5000

WHERE created_at BETWEEN '2024-01-01' AND '2024-12-31'

-- 否定
WHERE price NOT BETWEEN 1000 AND 5000

-- ★ 日付の罠: BETWEEN は 23:59:59.999 を含まない
WHERE created_at BETWEEN '2024-01-01' AND '2024-12-31'
-- → 2024-12-31 00:00:00 までしか含まれない!
-- 正しくは:
WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01'

IS NULL / IS NOT NULL

NULL は = NULL では判定できません。必ず IS NULL / IS NOT NULL:

-- ❌ 動かない (常に false)
WHERE email = NULL
WHERE email <> NULL

-- ✅ 正しい
WHERE email IS NULL
WHERE email IS NOT NULL

-- NULL を別の値として扱う
WHERE COALESCE(email, '') = ''   -- NULL または空文字
WHERE IFNULL(email, '') = ''     -- MySQL
WHERE NVL(email, '') IS NULL     -- Oracle

EXISTS / NOT EXISTS

-- 注文を 1 件以上持つユーザー
SELECT * FROM users u
WHERE EXISTS (
    SELECT 1 FROM orders o WHERE o.user_id = u.id
);

-- 注文を持たないユーザー
SELECT * FROM users u
WHERE NOT EXISTS (
    SELECT 1 FROM orders o WHERE o.user_id = u.id
);

-- IN との違い: EXISTS は 1 件見つけた時点で終わる → 大規模データで速い
-- IN は全件展開してから判定 → 小規模リスト向き

CASE WHEN (条件分岐)

-- SELECT で使うのが一般的だが WHERE でも使える
SELECT id,
    CASE
        WHEN age < 20 THEN '未成年'
        WHEN age < 65 THEN '成人'
        ELSE '高齢者'
    END AS age_group
FROM users;

-- WHERE で動的フィルタ
WHERE
    CASE WHEN @flag = 1 THEN status END = 'active'

SARGable: インデックスが効く書き方

SARGable (Search ARGument able) = インデックスが使える形の WHERE 句。カラムをそのまま比較するのが基本:

-- ❌ Non-SARGable: 関数でラップ → インデックス使えない
WHERE YEAR(created_at) = 2024
WHERE UPPER(email) = 'FOO@BAR.COM'
WHERE created_at + INTERVAL 1 DAY > NOW()
WHERE name LIKE '%abc%'              -- 先頭ワイルドカード
WHERE price * 1.1 > 1000

-- ✅ SARGable: カラムは素のまま、定数側で計算
WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01'
WHERE email = LOWER('Foo@Bar.com')   -- email は CI 照合順序前提
WHERE created_at > NOW() - INTERVAL 1 DAY
WHERE name LIKE 'abc%'
WHERE price > 1000 / 1.1

Prepared Statement / SQL Injection 対策

WHERE 句にユーザー入力を直接埋め込むと SQL Injection 脆弱性になります。必ずバインドパラメータを使ってください:

// ❌ 危険: SQL Injection
$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = $id";
// /?id=1 OR 1=1 → 全件取得される

// ✅ 安全: PDO の Prepared Statement
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([$_GET['id']]);
$users = $stmt->fetchAll(PDO::FETCH_ASSOC);

// ✅ Laravel Query Builder
$users = DB::table('users')->where('id', $request->id)->get();

// ✅ 名前付きバインド
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email AND status = :status');
$stmt->execute([':email' => $email, ':status' => 'active']);

UPDATE / DELETE の WHERE 抜け事故防止

-- ❌ WHERE 抜け = 全件更新・全件削除
UPDATE users SET status = 'inactive';   -- 全ユーザーが inactive に!
DELETE FROM users;                       -- 全ユーザーが消えた!

-- ✅ 事前に SELECT で対象確認
SELECT * FROM users WHERE last_login < '2023-01-01';
-- → 対象 OK と確認してから
UPDATE users SET status = 'inactive' WHERE last_login < '2023-01-01';

-- ✅ MySQL の safe mode (本番推奨)
-- my.cnf: sql_safe_updates = 1
-- → WHERE 無しの UPDATE/DELETE をエラーに

FAQ

Q: AND と OR、どっちが先に評価される?
A: AND が先(優先順位が高い)。意図通りにするため括弧で明示するのが安全です。

Q: WHERE 1=1 をよく見るのはなぜ?
A: 動的 SQL 構築時、後続の AND ... を機械的に追加できるテクニックです。例: SELECT * FROM t WHERE 1=1 に条件を増やしていく。

Q: NOT IN と NOT EXISTS どちらを使うべき?
A: NULL を含む可能性があるなら必ず NOT EXISTSNOT IN (..., NULL) は 1 件も返さない罠があります。