【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` 后面的列表就不用写成固定的值,而是可以**使用从其他表查询出的结果作为列表**。
场景:“想查找所有由位于关东地区的供应商供应的商品”
这个查询可以分为两个步骤来考虑:
- 首先,从 `suppliers` 表中获取 `region` 为“关东”的供应商ID列表。
- 然后,使用该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` 子句: 可以将 `WHERE A = 1 OR A = 2 OR A = 3` 这样的冗长条件,简洁地写成 `WHERE A IN (1, 2, 3)`。可读性得到极大提升。
- `NOT IN` 子句: 与 `IN` 相反,用于提取不包含在列表中的内容。
- 与子查询的结合: `IN` 的列表部分可以由另一个 `SELECT` 语句的结果动态生成,功能非常强大。
- `NULL` 的陷阱: 一个重要的注意事项是,如果 `NOT IN` 的列表中包含 `NULL`,结果将为0条。使用子查询时需要特别小心。
`IN` 子句是使代码变得简洁易读的基本技巧。特别是与子查询的结合,是在提取复杂条件数据时不可或缺的方法。而 `NOT IN` 与 `NULL` 的关系,是否了解将极大地影响调试时间,是一项重要的知识。请务必熟练掌握本次内容,让你的SQL技能更上一层楼!