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

【SQL IN 子句入门】智能处理多重条件!用 IN 和 NOT IN 改变 WHERE 子句的写法

在开发网站时,我们经常会遇到需要从多个选项中查找匹配任意一个的数据的场景,比如“只想列出特定类别的商品”或“只想给北京、上海、广州的用户发送活动通知”。

这时候,你会怎么写 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集线器', '电脑配件', 4000, 101, NULL),
(6, '传统毛笔', '文具', 1200, 104, '工匠手工制作'),
(7, '大阪混合果汁', '食品', 350, NULL, '供应商不明');

这样,用于按各种条件提取数据的准备工作就完成了。


【基础】使用 `IN` 子句指定多个选项

`IN` 子句的基本语法非常简单。

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

只需在 `()` 中放入想查找的值的列表即可。这表示“只要列的值与此列表中的任何一个值匹配即可”。开头那个使用了很多 `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集线器  | 电脑配件 | 4000  | 101         | NULL

只需在 `IN` 前面加上 `NOT`,就能轻松指定否定条件。这也是一个非常方便的用法。


【应用】`IN` 子句与子查询的结合

`IN` 子句的真正价值在于与子查询(Subquery)的结合。子查询是指嵌套在 SQL 语句中的另一个 `SELECT` 语句。

使用它,`IN` 后面的列表就不用写成固定的值,而是可以**使用从其他表查询出的结果作为列表**。

场景:“想查找所有由位于关东地区的供应商供应的商品”

这个查询可以分为两个步骤来考虑:

  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集线器  | 电脑配件 | 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` 与子查询一起使用时,一个基本原则是在子查询中加入 `WHERE id IS NOT NULL` 这样的条件,确保子查询绝不返回 `NULL`,或者使用 `NOT EXISTS` 等其他语法。


【体验环节】在浏览器中运行SQL,熟练使用IN子句!

现在,是时候把知识转化为技能了!请将下面的HTML代码完整复制,保存为 `sql_in_test.html` 这样的文件名,然后在浏览器中打开它。一个专属于你的SQL执行环境将会启动,并且已经为你准备好了本文中使用的 `products` 和 `suppliers` 表。

试着改变 `IN` 或 `NOT IN` 列表中的内容,或者改变子查询的条件,看看结果会如何变化吧!

<!DOCTYPE html>
<html lang="zh">
<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 = '数据库准备中...';
      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集线器', '电脑配件', 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技能更上一层楼!