🇯🇵 日本語 | 🇺🇸 English | 🇪🇸 Español | 🇵🇹 Português | 🇹🇭 ไทย | 🇨🇳 中文

【SQL IN句入門】複数条件をスマートに!INとNOT IN句で変わるWHERE句の書き方

Webサイトを開発していると、「特定のカテゴリの商品だけを一覧表示したい」「東京、神奈川、千葉のユーザーにだけキャンペーンのお知らせを送りたい」といった、複数の選択肢の中からいずれかに合致するデータを探したい場面が頻繁に訪れます。

こんなとき、あなたならWHERE句をどう書きますか?

WHERE category = '文房具' OR category = '家電' OR category = '食品'

もちろん、これでも正しく動作します。しかし、選択肢が5個、10個と増えていったらどうでしょう? `OR`が延々と続く、非常に読みにくく、メンテナンスしづらいコードになってしまいます。こんな悩みを一瞬で解決してくれるのが、今回主役の`IN`句です。

この記事では、`IN`句と、その逆の条件を指定する`NOT IN`句の基本的な使い方から、サブクエリと組み合わせた応用テクニック、そして多くの人がハマる「NULLの罠」まで、コピペで動くコードと共に徹底解説します。`IN`句をマスターして、あなたのSQLをよりスマートで読みやすく進化させましょう!


準備:検索対象の商品データを用意しよう

操作を試すには、まずデータが必要です。今回は、シンプルな商品リストを管理するproductsテーブルと、その商品を供給するサプライヤーの情報を管理するsuppliersテーブルを作成します。カテゴリや仕入先が複数あることで、`IN`句の便利さが実感しやすくなります。

-- もしテーブルが存在すれば削除(繰り返し試せるように)
DROP TABLE IF EXISTS products;
DROP TABLE IF EXISTS suppliers;

-- サプライヤーテーブルを作成
CREATE TABLE suppliers (
  id INTEGER PRIMARY KEY,
  supplier_name TEXT NOT NULL,
  region TEXT NOT NULL -- '関東', '関西'
);

-- 商品テーブルを作成
CREATE TABLE products (
  id INTEGER PRIMARY KEY,
  product_name TEXT NOT NULL,
  category TEXT NOT NULL,
  price INTEGER,
  supplier_id INTEGER,
  notes TEXT -- NULLを許可する備考欄
);

-- サプライヤーデータを挿入
INSERT INTO suppliers (id, supplier_name, region) VALUES
(101, '東京オフィスサプライ', '関東'),
(102, '大阪キッチンウェア', '関西'),
(103, '千葉フードサービス', '関東'),
(104, '京都伝統文具', '関西');

-- 商品データを挿入
INSERT INTO products (id, product_name, category, price, supplier_id, notes) VALUES
(1, 'すごいボールペン', '文房具', 150, 101, '書きやすい'),
(2, '魔法のノート', '文房具', 300, 104, NULL),
(3, '究極の炊飯器', '家電', 25000, 102, '大人気商品'),
(4, '奇跡のリンゴ', '食品', 500, 103, '数量限定'),
(5, 'USB-Cハブ', 'PC周辺機器', 4000, 101, NULL),
(6, '伝統の筆', '文房具', 1200, 104, '職人手作り'),
(7, '大阪ミックスジュース', '食品', 350, NULL, 'サプライヤー不明');

これで、様々な条件でデータを抽出する準備が整いました。


【基本】`IN`句で複数の選択肢を指定する

`IN`句の基本的な構文はとてもシンプルです。

WHERE 列名 IN (値1, 値2, 値3, ...);

()の中に、探したい値のリストを入れるだけです。これは「列の値が、このリストの中のどれか一つにでも合致すればOK」という意味になります。冒頭の`OR`をたくさん使った例も、`IN`句を使えば次のようにスッキリ書けます。

シナリオ:「カテゴリが『文房具』または『食品』である商品をすべて探したい」

SELECT * FROM products
WHERE category IN ('文房具', '食品');

実行結果:

id | product_name     | category | price | supplier_id | notes
---|--------------------|----------|-------|-------------|----------
1  | すごいボールペン   | 文房具     | 150   | 101         | 書きやすい
2  | 魔法のノート       | 文房具     | 300   | 104         | NULL
4  | 奇跡のリンゴ       | 食品       | 500   | 103         | 数量限定
6  | 伝統の筆         | 文房具     | 1200  | 104         | 職人手作り
7  | 大阪ミックスジュース | 食品       | 350   | NULL        | サプライヤー不明

どうでしょう? `OR`をいくつも繋げるより、ずっと意図が分かりやすいと思いませんか? この「リスト」の考え方が`IN`句の基本です。


【逆の条件】`NOT IN`句でリスト以外のものを指定する

もちろん、`IN`句の逆、「このリストに含まれて**いない**もの」を探したい場合もあります。その時に使うのが`NOT IN`句です。

シナリオ:「カテゴリが『文房具』と『食品』**以外**の商品をすべて探したい」

SELECT * FROM products
WHERE category NOT IN ('文房具', '食品');

実行結果:

id | product_name | category  | price | supplier_id | notes
---|--------------|-----------|-------|-------------|----------
3  | 究極の炊飯器   | 家電        | 25000 | 102         | 大人気商品
5  | USB-Cハブ    | PC周辺機器  | 4000  | 101         | NULL

`IN`の前に`NOT`を付けるだけで、簡単に否定条件を指定できました。これも非常に便利な使い方です。


【応用】`IN`句とサブクエリを組み合わせる

`IN`句の真価が発揮されるのが、サブクエリ(副問い合わせ)との組み合わせです。サブクエリとは、SQL文の中に入れ子で記述される別の`SELECT`文のことです。

これを使うと、`IN`の後のリストを固定の値で書くのではなく、**別のテーブルから取得した結果をリストとして使う**ことができます。

シナリオ:「関東地方にあるサプライヤー(仕入先)が供給している商品をすべて探したい」

この検索は2段階のステップで考えられます。

  1. まず、`suppliers`テーブルから`region`が「関東」であるサプライヤーのIDリストを取得する。
  2. 次に、そのIDリストを使って`products`テーブルから該当する商品を探す。

このステップ1の部分をサブクエリとして`IN`句の中に埋め込むのです。

SELECT * FROM products
WHERE supplier_id IN (SELECT id FROM suppliers WHERE region = '関東');

実行結果:

id | product_name   | category  | price | supplier_id | notes
---|----------------|-----------|-------|-------------|----------
1  | すごいボールペン | 文房具      | 150   | 101         | 書きやすい
4  | 奇跡のリンゴ     | 食品        | 500   | 103         | 数量限定
5  | USB-Cハブ      | PC周辺機器  | 4000  | 101         | NULL

データベースはまず()の中のサブクエリSELECT id FROM suppliers WHERE region = '関東'を実行し、IDのリスト(この場合は `(101, 103)`)を取得します。その後、外側のクエリは実質的にWHERE supplier_id IN (101, 103)として実行されます。このように、動的に条件リストを生成できるのが、サブクエリと組み合わせる最大のメリットです。


最重要の注意点! `NOT IN`と`NULL`の恐ろしい罠

`IN`句は直感的で使いやすいですが、`NOT IN`句には、多くの開発者が一度はハマる、非常に厄介な「罠」が存在します。それは、リストの中に`NULL`が含まれている場合です。

結論から言うと、`NOT IN`のリストに一つでも`NULL`が含まれていると、そのクエリは一件もデータを返さなくなります。

なぜでしょう? SQLの`NULL`は「値がない」ことを表す特殊な存在で、`NULL`との比較(例: `id = NULL`や`id <> NULL`)の結果は、`TRUE`でも`FALSE`でもなく、常に`UNKNOWN`(不明)になります。

WHERE id NOT IN (1, 2, NULL)という条件は、内部的にWHERE id <> 1 AND id <> 2 AND id <> NULLとほぼ同じように評価されます。レコードの`id`が`3`だったとしても、最後の`3 <> NULL`が`UNKNOWN`になるため、全体の条件式が`TRUE`になることはなく、結果としてどの行もマッチしなくなってしまうのです。

特に危険なのが、先ほど使ったサブクエリとの組み合わせです。

シナリオ:「不明なサプライヤー(supplier_idがNULL)が供給している商品**以外**をすべて探したい」

一見、これで良さそうに見えますが…

-- 【罠!】このクエリは意図通りに動作しません!
SELECT * FROM products
WHERE supplier_id NOT IN (SELECT id FROM suppliers WHERE region = '関西');

-- 上記のサブクエリは(102, 104)を返すが、もしsuppliersテーブルに
-- idがNULLの行があった場合、(102, 104, NULL)となり、
-- このクエリは0件の結果を返します。

もしサブクエリの結果に`NULL`が含まれてしまう可能性がある場合、結果は0件になってしまいます。私たちのサンプルデータには`id`が`NULL`のサプライヤーはいませんが、`products`テーブルの`supplier_id`が`NULL`のレコード(ID 7の商品)は存在します。このレコードがNOT INでどう扱われるか見てみましょう。

対処法: `NOT IN`をサブクエリと使う場合は、サブクエリが決して`NULL`を返さないように`WHERE id IS NOT NULL`のような条件をサブクエリ内に加えるか、NOT EXISTSなどの別の構文を使うのが鉄則です。


【体験コーナー】ブラウザでSQLを動かし、IN句を使いこなそう!

さあ、知識をスキルに変える時間です! 以下のHTMLコードを丸ごとコピーして、sql_in_test.htmlのようなファイル名で保存し、ブラウザで開いてみてください。あなた専用のSQL実行環境が立ち上がり、この記事で使っているproductssuppliersテーブルが用意されています。

`IN`や`NOT IN`のリストの中身を変えてみたり、サブクエリの条件を変えてみたりして、結果がどう変わるか色々試してみてください!

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>SQL IN句 オンライン実行環境</title>
  <style>
    body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; line-height: 1.7; color: #333; max-width: 800px; margin: 2rem auto; padding: 0 1rem; }
    h1 { color: #8e44ad; }
    textarea { width: 100%; height: 180px; font-family: "SF Mono", "Consolas", monospace; font-size: 16px; padding: 12px; border: 1px solid #ccc; border-radius: 6px; box-sizing: border-box; margin-bottom: 1rem; }
    button { background-color: #9b59b6; color: white; border: none; padding: 12px 22px; font-size: 16px; border-radius: 6px; cursor: pointer; transition: background-color 0.2s; }
    button:hover { background-color: #8e44ad; }
    button:disabled { background-color: #bdc3c7; cursor: not-allowed; }
    #result-container { margin-top: 2rem; border: 1px solid #ddd; padding: 1rem; border-radius: 6px; background: #fdfdfd; min-height: 50px; }
    #error-message { color: #e74c3c; font-weight: bold; }
    table { border-collapse: collapse; width: 100%; margin-top: 1rem; }
    th, td { border: 1px solid #ddd; padding: 10px; text-align: left; }
    th { background-color: #f2f2f2; }
    tr:nth-child(even) { background-color: #f9f9f9; }
  </style>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.10.3/sql-wasm.js"></script>
</head>
<body>

  <h1>SQLを試してみよう!</h1>
  <p>下のテキストエリアにSQL文を入力して「実行」ボタンを押してください。色々な条件を試してみましょう!</p>

  <textarea id="sql-input">-- 関西地方のサプライヤーが供給する商品を探してみよう
SELECT * FROM products
WHERE supplier_id IN (SELECT id FROM suppliers WHERE region = '関西');
  </textarea>
  
  <button id="execute-btn">実行</button>
  
  <div id="result-container">
    <p id="error-message"></p>
    <div id="result-output"></div>
  </div>

  <script>
    const sqlInput = document.getElementById('sql-input');
    const executeBtn = document.getElementById('execute-btn');
    const errorMsg = document.getElementById('error-message');
    const resultOutput = document.getElementById('result-output');

    let db;

    async function initDb() {
      executeBtn.disabled = true;
      executeBtn.textContent = 'DB準備中...';
      try {
        const SQL = await initSqlJs({
          locateFile: file => `https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.10.3/${file}`
        });
        db = new SQL.Database();
        
        const setupSql = `
          DROP TABLE IF EXISTS products;
          DROP TABLE IF EXISTS suppliers;

          CREATE TABLE suppliers (id INTEGER PRIMARY KEY, supplier_name TEXT NOT NULL, region TEXT NOT NULL);
          CREATE TABLE products (id INTEGER PRIMARY KEY, product_name TEXT NOT NULL, category TEXT NOT NULL, price INTEGER, supplier_id INTEGER, notes TEXT);

          INSERT INTO suppliers (id, supplier_name, region) VALUES (101, '東京オフィスサプライ', '関東'), (102, '大阪キッチンウェア', '関西'), (103, '千葉フードサービス', '関東'), (104, '京都伝統文具', '関西');
          INSERT INTO products (id, product_name, category, price, supplier_id, notes) VALUES (1, 'すごいボールペン', '文房具', 150, 101, '書きやすい'), (2, '魔法のノート', '文房具', 300, 104, NULL), (3, '究極の炊飯器', '家電', 25000, 102, '大人気商品'), (4, '奇跡のリンゴ', '食品', 500, 103, '数量限定'), (5, 'USB-Cハブ', 'PC周辺機器', 4000, 101, NULL), (6, '伝統の筆', '文房具', 1200, 104, '職人手作り'), (7, '大阪ミックスジュース', '食品', 350, NULL, 'サプライヤー不明');
        `;
        db.run(setupSql);
        
        executeBtn.disabled = false;
        executeBtn.textContent = '実行';
        resultOutput.innerHTML = '<p>準備完了!自由にSQLを試してみてください。</p>';

      } catch (err) {
        errorMsg.textContent = 'データベースの初期化に失敗しました: ' + err.message;
        console.error(err);
      }
    }

    function executeSql() {
      if (!db) return;
      
      const sql = sqlInput.value;
      errorMsg.textContent = '';
      resultOutput.innerHTML = '';

      try {
        const results = db.exec(sql);
        if (results.length === 0) {
          resultOutput.innerHTML = '<p>クエリは成功しましたが、結果セットは返されませんでした。</p>';
          return;
        }
        
        results.forEach(result => {
          const table = document.createElement('table');
          const thead = document.createElement('thead');
          const tbody = document.createElement('tbody');
          
          const headerRow = document.createElement('tr');
          result.columns.forEach(colName => {
            const th = document.createElement('th');
            th.textContent = colName;
            headerRow.appendChild(th);
          });
          thead.appendChild(headerRow);
          
          result.values.forEach(row => {
            const bodyRow = document.createElement('tr');
            row.forEach(cellValue => {
              const td = document.createElement('td');
              td.textContent = cellValue === null ? 'NULL' : cellValue;
              bodyRow.appendChild(td);
            });
            tbody.appendChild(bodyRow);
          });
          
          table.appendChild(thead);
          table.appendChild(tbody);
          resultOutput.appendChild(table);
        });

      } catch (err) {
        errorMsg.textContent = 'SQLエラー: ' + err.message;
        console.error(err);
      }
    }

    executeBtn.addEventListener('click', executeSql);
    
    initDb();
  </script>
</body>
</html>

まとめ

今回は、複数の条件をスマートにまとめるIN句とNOT IN句について学びました。

`IN`句は、コードをシンプルで読みやすくするための基本的なテクニックです。特にサブクエリとの組み合わせは、複雑な条件のデータを抽出する際に避けては通れない道です。そして、`NOT IN`と`NULL`の関係は、知っているかいないかでデバッグの時間が大きく変わる重要な知識です。ぜひ、今回の内容をしっかりマスターして、あなたのSQLスキルを一段階レベルアップさせてください!