EP04. "进阶查询语法:EXISTS / ANY / ALL / CASE"
🔒 登录后可标记已读- 「找出至少下过一次单的用户」「找出比某个分类里所有商品都贵的商品」——这类判断句式,直接翻译成 SQL 就是 EXISTS / ANY / ALL 的用武之地。
- EP02 的语法速查表已经提过一行
EXISTS (subquery)和COALESCE(col, default),这篇把两者真正展开讲清楚怎么写、什么时候比 JOIN/IN 更合适。 - CASE 表达式是 SQL 里的 if-else,能把判断逻辑直接写进查询结果的栏位。
- SELECT INTO 和 INSERT INTO SELECT 是两种「复制数据到别的表」的写法,一个负责建新表、一个负责塞进已存在的表,容易搞混。
- 最后补 NULL 值替代函数在不同数据库的写法差异——COALESCE、IFNULL、ISNULL、NVL 名字都不一样,但做的事一样。
重点内容
EXISTS:只看「有没有」,不看「是什么」
EXISTS 后面接一个子查询,只判断子查询有没有返回至少一行,不关心返回的具体内容是什么,成立就是 TRUE。因为不需要真的把子查询的值取出来比对,条件符合的当下数据库就能提前停止往下找,这也是它常常比同等效果的 JOIN 更省资源的原因。
-- 找出至少下过一次订单的用户
SELECT u.id, u.name
FROM users u
WHERE EXISTS (
SELECT 1 FROM orders o WHERE o.user_id = u.id
);
-- 反过来:找出从来没下过单的用户(NOT EXISTS)
SELECT u.id, u.name
FROM users u
WHERE NOT EXISTS (
SELECT 1 FROM orders o WHERE o.user_id = u.id
);
上面这种子查询里的条件(o.user_id = u.id)引用了外层查询的栏位,叫「关联子查询」(correlated subquery)——子查询会针对外层的每一行各跑一次,专门配合 EXISTS/NOT EXISTS 使用。
📌 为什么很多人放弃 NOT IN、改用 NOT EXISTS:如果子查询结果集里混进一个 NULL,NOT IN 的比对会整个失效、直接返回空结果——这是子查询栏位可能为 NULL 时的经典陷阱。NOT EXISTS 不会受 NULL 影响,正式环境要判断"不存在",优先用 NOT EXISTS 而不是 NOT IN。
ANY / ALL:拿单一值跟一整组值比较
ANY 和 ALL 都是搭配比较运算符(=、>、<、>=、<=、<>)跟一个子查询的结果集比较,差别在于门槛松紧:
- ANY:子查询结果里只要有一个值满足条件就算成立
- ALL:子查询结果里每一个值都要满足条件才算成立
-- ANY:找出比「电子产品」分类里最便宜商品还贵的商品(价格 > 该分类中任一个价格)
SELECT name, price
FROM products
WHERE price > ANY (
SELECT price FROM products WHERE category_id = 3
);
-- ALL:找出比「电子产品」分类里最贵商品还贵的商品(价格要大于该分类全部价格)
SELECT name, price
FROM products
WHERE price > ALL (
SELECT price FROM products WHERE category_id = 3
);
💡 > ANY (subquery) 等价于 > (SELECT MIN(...) FROM subquery),> ALL (subquery) 等价于 > (SELECT MAX(...) FROM subquery)——看不懂 ANY/ALL 的时候,换成 MIN()/MAX() 的写法想一遍会更直觉。另外 = ANY (subquery) 效果等同于 IN (subquery),两种写法可以互换。
CASE:把判断逻辑写进查询结果
CASE 表达式依序检查每个 WHEN 条件,碰到第一个成立的条件就停下、返回对应的 THEN 结果,全部都不成立就用 ELSE 的值兜底(没写 ELSE 则返回 NULL,不是报错)。
-- 把订单按金额分级,直接在查询结果多一栏「订单等级」
SELECT
id,
total_amount,
CASE
WHEN total_amount >= 500 THEN '大额订单'
WHEN total_amount >= 100 THEN '中额订单'
ELSE '小额订单'
END AS 订单等级
FROM orders;
CASE 也能配合聚合函数做「条件式计数」,一次统计出各等级各自有几笔,不用分开跑三次查询:
SELECT
COUNT(CASE WHEN total_amount >= 500 THEN 1 END) AS 大额订单数,
COUNT(CASE WHEN total_amount >= 100 AND total_amount < 500 THEN 1 END) AS 中额订单数,
COUNT(CASE WHEN total_amount < 100 THEN 1 END) AS 小额订单数
FROM orders;
SELECT INTO 与 INSERT INTO SELECT:复制数据到别的表
两者都是「用查询结果去建立/填充另一张表」,但目标表的状态不一样:
| 语法 | 目标表 | 用途 |
|---|---|---|
SELECT ... INTO new_table FROM ... | 全新的表(自动建立) | 备份、建临时分析表 |
INSERT INTO existing_table SELECT ... FROM ... | 已存在的表 | 把数据追加进现有表,原有数据不受影响 |
-- INSERT INTO SELECT:把一年前已完成的订单搬进历史归档表(orders_archive 要事先建好)
INSERT INTO orders_archive (id, user_id, status, total_amount, created_at)
SELECT id, user_id, status, total_amount, created_at
FROM orders
WHERE status = 'completed'
AND created_at < NOW() - INTERVAL '1 year';
📌 SELECT INTO 在 PostgreSQL / MySQL 上不是这样用的:SELECT ... INTO new_table 是 SQL Server / MS Access 的写法。PostgreSQL 要做「建一张新表塞进查询结果」这件事,对应语法是 CREATE TABLE ... AS SELECT(MySQL 也支持同样写法):
-- PostgreSQL / MySQL:建新表并塞进查询结果,效果等同 SQL Server 的 SELECT INTO
CREATE TABLE orders_2025_backup AS
SELECT * FROM orders WHERE created_at < '2026-01-01';
| 数据库 | 建新表并复制数据的写法 |
|---|---|
| SQL Server / MS Access | SELECT * INTO new_table FROM old_table |
| PostgreSQL | CREATE TABLE new_table AS SELECT * FROM old_table |
| MySQL | CREATE TABLE new_table AS SELECT * FROM old_table |
NULL 值替代函数:COALESCE 与各数据库写法
EP02 提过 COALESCE(col, default) 能把 NULL 换成指定的默认值,这里展开讲:COALESCE 其实可以传两个以上参数,会依序找第一个不是 NULL 的值,适合做「多层退回」。
-- 显示名称优先用 nickname,没填就退回用 name,两个都没有才显示 'Guest'
SELECT id, COALESCE(nickname, name, 'Guest') AS display_name
FROM users;
不同数据库处理 NULL 替代的函数名不一样,功能是一样的:
| 数据库 | 函数 | 语法 | 备注 |
|---|---|---|---|
| PostgreSQL / SQL Server / Oracle | COALESCE | COALESCE(val1, val2, ..., valN) | ANSI 标准写法,可以传 2 个以上参数 |
| MySQL | IFNULL | IFNULL(expr, alt) | 只能传 2 个参数;MySQL 也支持 COALESCE |
| SQL Server | ISNULL | ISNULL(expr, alt) | 只能传 2 个参数;SQL Server 也支持 COALESCE |
| Oracle | NVL | NVL(expr, alt) | 只能传 2 个参数 |
📌 PostgreSQL 没有 IFNULL、也没有 ISNULL,只有 COALESCE——从 MySQL 或 SQL Server 转来 PostgreSQL 最容易踩这个坑,写 IFNULL(...) 会直接报函数不存在。
语法速查
| 任务 | 语法 |
|---|---|
| 判断子查询有没有结果 | WHERE EXISTS (subquery) |
| 判断子查询完全没有结果 | WHERE NOT EXISTS (subquery) |
| 跟子查询任一值比较 | WHERE col > ANY (subquery) |
| 跟子查询全部值比较 | WHERE col > ALL (subquery) |
| 条件判断出结果栏位 | CASE WHEN cond THEN val ELSE default END |
| 建新表塞进查询结果(PostgreSQL) | CREATE TABLE t AS SELECT ... |
| 塞进已存在的表 | INSERT INTO t SELECT ... |
| Null 值替代(标准写法) | COALESCE(col, default) |
适用版本
内容以 PostgreSQL 语法为主。EXISTS / ANY / ALL / CASE 在 PostgreSQL、MySQL、SQL Server 上写法一致;SELECT INTO 和 NULL 替代函数在不同数据库上差异较大,已在上面各自的对照表列出,实际使用前建议对照所用数据库的官方文档确认。
常见错误
- ❌ 子查询结果可能含 NULL 时还是用
NOT IN判断「不存在」——NULL 会让NOT IN整个失效返回空结果,改用NOT EXISTS更安全 - ❌ 看到 ANY/ALL 就死记语法,没意识到
> ANY等于跟子查询最小值比、> ALL等于跟子查询最大值比——想不通的时候换成 MIN()/MAX() 改写会更好懂 - ❌ CASE 没写 ELSE,以为条件都不满足时会报错——实际上会静默返回 NULL,报表里容易出现看不出原因的空白栏位
- ❌ 在 PostgreSQL 或 MySQL 里照抄网上教程的
SELECT ... INTO new_table语法建表——两者都不支持这种写法,要改用CREATE TABLE ... AS SELECT - ❌ 从 MySQL/SQL Server 转到 PostgreSQL,还在用
IFNULL()/ISNULL()——PostgreSQL 只认COALESCE()
Sources
Blog / Website: