📖 本篇简介:上一篇我们认识了 IF、IFNULL 这些简单的流程控制函数,但只要分支超过两层,它们就会变得难读难维护。本篇专门讲 SQL 里最强大的条件表达工具 CASE WHEN,以及它与聚合函数结合后完成"行转列"报表的经典套路。学完后你能写出多分支分类、动态排序、交叉报表等复杂查询。本示例基于 MySQL 8.0 语法。目录
- 1 条件逻辑在SQL中的地位
- 2 CASE WHEN基本语法
- 3 简单CASE与搜索CASE两种写法
- 4 IF与IFNULL与NULLIF函数
- 5 条件聚合与行转列
- 6 条件逻辑速查表
- 7 常见坑与注意事项
- 8 练习题
1 条件逻辑在SQL中的地位
编程语言里有 if/else,SQL 里对应的就是条件逻辑。它让数据库不只是"把数据捞出来",而是"按业务规则现场加工数据"。比如:
- 用户消费 1 万以上标 VIP,1 千以上标会员,其余标普通;
- 订单状态
1显示待付款、2已发货、3已完成; - 报表里把"每个月每个地区的销量"横向铺开成一行。
这些都不是单纯的字段映射,而是"判断 + 赋值"。CASE WHEN 就是 SQL 里的 if/else if/else。它可以用在 SELECT、WHERE、ORDER BY、GROUP BY 里,是把业务规则写进查询的关键。
对比一下"在应用层判断"和"在 SQL 层判断"的差异:
| 做法 | 优点 | 缺点 |
|---|---|---|
| 应用层 if/else | 业务逻辑集中、好调试 | 要把全量数据拉到应用层,网络开销大 |
| SQL 里 CASE WHEN | 数据不落地就完成加工、口径统一 | 复杂规则塞进 SQL 可读性下降 |
经验是:简单映射、分类、报表聚合放 SQL;复杂业务流程、需要事务一致性的逻辑放应用层。本篇聚焦 SQL 这一侧。
2 CASE WHEN基本语法
CASE WHEN 有两种形态,先看最通用的"搜索 CASE":
-- CASE WHEN 基本语法(MySQL 8.0)
SELECT
user_id,
total_amount,
CASE
WHEN total_amount >= 10000 THEN 'VIP' -- 满足条件返回该值
WHEN total_amount >= 1000 THEN '会员'
ELSE '普通' -- 都不满足走 ELSE
END AS level -- END 结束,起别名
FROM user_stat;
要点:
1. CASE 开头、END 结尾,缺一不可;
2. 每个 WHEN 条件 THEN 结果 是一个分支,从上往下匹配,命中即停;
3. ELSE 可选——不写的话,所有分支都不命中时返回 NULL;
4. 它本身是一个表达式,可以出现在任何表达式能出现的地方。
把状态码翻译成可读文案是最常见的用途:
-- 订单状态翻译(MySQL 8.0)
SELECT
order_id,
status,
CASE status
WHEN 1 THEN '待付款'
WHEN 2 THEN '已发货'
WHEN 3 THEN '已完成'
WHEN 4 THEN '已取消'
ELSE '未知'
END AS status_text
FROM orders;
2.1 分支里写复合条件
WHEN 后面可以是任意布尔表达式,支持 AND/OR、函数、子查询。比如"年龄在 18~60 且消费超过 500"才标活跃用户:
-- 复合条件分支(MySQL 8.0)
SELECT
user_id,
CASE
WHEN age BETWEEN 18 AND 60 AND total_amount > 500 THEN '活跃用户'
WHEN age < 18 OR age > 60 THEN '非主力人群'
ELSE '普通用户'
END AS user_tag
FROM user_info;
注意 WHEN 条件是"短路"的:从上到下依次判断,命中第一个满足的分支就停止,后面不再判断。这和大多数编程语言的 if/else if 一致。
3 简单CASE与搜索CASE两种写法
上面第二个例子就是"简单 CASE":CASE 字段 WHEN 值 THEN ...,它把字段直接写在 CASE 后面,只做等值比较。
| 写法 | 结构 | 适用 |
|---|---|---|
| 简单 CASE | CASE 字段 WHEN 常量 THEN ... | 字段与多个固定值等值比较 |
| 搜索 CASE | CASE WHEN 条件 THEN ... | 任意复杂条件(范围、多字段、表达式) |
两者对比:
-- 写法一:简单 CASE(只能等值)(MySQL 8.0)
SELECT CASE level WHEN 1 THEN '低' WHEN 2 THEN '中' WHEN 3 THEN '高' END FROM t;
-- 写法二:搜索 CASE(可写范围)(MySQL 8.0)
SELECT CASE
WHEN score >= 90 THEN '优秀'
WHEN score >= 60 THEN '及格'
ELSE '不及格'
END
FROM exam;
经验法则:能一眼看出是"同一个字段对几个固定值做映射"时用简单 CASE,代码短;只要涉及范围、多字段组合、AND/OR,就直接用搜索 CASE。混用反而增加阅读负担。
举个对比例子,同样是"按金额分级":
-- 错误示范:简单 CASE 做不到范围判断(MySQL 8.0)
SELECT CASE total_amount
WHEN 10000 THEN 'VIP' -- 这只匹配恰好等于 10000 的行
WHEN 1000 THEN '会员' -- 同样只匹配恰好 1000
ELSE '普通'
END
FROM user_stat;
上面这种写法是错的——简单 CASE 是等值比较,total_amount=10001 的用户不会被算成 VIP。范围分级必须用搜索 CASE:
-- 正确:搜索 CASE 才能表达范围(MySQL 8.0)
SELECT CASE
WHEN total_amount >= 10000 THEN 'VIP'
WHEN total_amount >= 1000 THEN '会员'
ELSE '普通'
END
FROM user_stat;
3.1 CASE 与 ORDER BY:动态排序
CASE WHEN 放在 ORDER BY 里可以实现"按业务优先级排序":把 VIP 排前面,会员次之,普通最后,同等级再按金额降序。
-- 动态排序(MySQL 8.0)
SELECT user_id, level, total_amount
FROM user_stat
ORDER BY
CASE level
WHEN 'VIP' THEN 1
WHEN '会员' THEN 2
ELSE 3
END,
total_amount DESC;
3.2 CASE 出现在其他位置
CASE 是表达式,理论上 SELECT 里能写表达式的地方都能用。几个高频位置:
-- CASE 放在 GROUP BY 里:按"评级"分组统计(MySQL 8.0)
SELECT
CASE
WHEN total_amount >= 10000 THEN 'VIP'
WHEN total_amount >= 1000 THEN '会员'
ELSE '普通'
END AS level,
COUNT(*) AS user_cnt
FROM user_stat
GROUP BY
CASE
WHEN total_amount >= 10000 THEN 'VIP'
WHEN total_amount >= 1000 THEN '会员'
ELSE '普通'
END;
GROUP BY 里重复写一遍 CASE 是因为它不能直接引用 SELECT 里的别名(部分数据库支持,但 MySQL 标准写法建议照写,或用子查询包一层)。把它放进 HAVING 也常见:
-- HAVING 里过滤(MySQL 8.0)
SELECT region, SUM(amount) AS total
FROM orders
GROUP BY region
HAVING SUM(CASE WHEN amount>=200 THEN 1 ELSE 0 END) > 0; -- 只要有大额订单的地区
4 IF与IFNULL与NULLIF函数
上一篇已介绍过这三个函数,这里集中对比,因为它们和 CASE WHEN 是"能互相替代"的关系。
-- 三个函数对比(MySQL 8.0)
SELECT
IF(score >= 60, '及格', '不及格') AS r1, -- 单条件二选一
IFNULL(phone, '未绑定') AS r2, -- 空值兜底
NULLIF(divisor, 0) AS r3; -- 相等返回 NULL
| 函数 | 等价 CASE | 用途 |
|---|---|---|
| IF(c,a,b) | CASE WHEN c THEN a ELSE b END | 单分支二选一 |
| IFNULL(a,b) | CASE WHEN a IS NULL THEN b ELSE a END | NULL 兜底 |
| NULLIF(a,b) | CASE WHEN a=b THEN NULL ELSE a END | 除零保护、空串转 NULL |
当逻辑只有一个分支时,用 IF 比 CASE WHEN 简洁;一旦出现第二个分支,就果断换成 CASE WHEN,不要再写 IF(...) 套 IF(...) 的嵌套地狱。
4.1 什么时候用哪个
| 分支数 | 推荐写法 |
|---|---|
| 只判断空值 | IFNULL |
| 单条件二选一 | IF |
| 两三个固定值映射文案 | 简单 CASE |
| 范围判断、多字段组合 | 搜索 CASE |
| 聚合里按条件统计 | SUM(CASE WHEN ... END) |
记住一个信号:当你发现自己在写 IF(a, IF(b, x, y), z) 这种两层以上嵌套时,就是该换成 CASE WHEN 的信号。
5 条件聚合与行转列
这是 CASE WHEN 最有威力的应用:把它塞进聚合函数里,按"条件"汇总,从而实现"行转列"——把原本纵向排列的多行数据,横向铺成一行多列。
5.1 基础:按条件统计
需求:统计每个地区里"大额订单(金额≥200)"和"小额订单"各有多少。
-- 条件聚合(MySQL 8.0)
SELECT
region,
COUNT(*) AS total, -- 全部订单数
SUM(CASE WHEN amount >= 200 THEN 1 ELSE 0 END) AS big_cnt, -- 大额订单数
SUM(CASE WHEN amount < 200 THEN 1 ELSE 0 END) AS small_cnt -- 小额订单数
FROM orders
GROUP BY region;
原理:CASE 在每一行上判断,命中返回 1,否则返回 0;SUM 把这些 1/0 加起来,就得到"满足条件的行数"。等价的另一种写法是 COUNT(CASE WHEN amount>=200 THEN 1 END)——注意 COUNT 里 ELSE 不写,不命中时返回 NULL,COUNT 自动跳过 NULL,效果相同。
5.2 实战:行列转换报表
经典交叉表:行是地区,列是每个月的销售额。原始数据是 orders(region, month, amount) 一行一笔订单,报表要求一行显示各月数据。
-- 行转列(MySQL 8.0)
SELECT
region,
SUM(CASE WHEN month = '2026-01' THEN amount ELSE 0 END) AS m1,
SUM(CASE WHEN month = '2026-02' THEN amount ELSE 0 END) AS m2,
SUM(CASE WHEN month = '2026-03' THEN amount ELSE 0 END) AS m3
FROM orders
GROUP BY region;
用 Python + sqlite3 实跑验证这个逻辑,确认行列转换结果:
# sqlite3 实跑验证条件聚合/行转列(Python 3.14 + sqlite3 3.53)
import sqlite3
con = sqlite3.connect(':memory:')
cur = con.cursor()
cur.executescript('''
CREATE TABLE sales(region TEXT, amount INT);
INSERT INTO sales VALUES ('East',100),('East',200),('West',150),('West',50),('East',300);
''')
rows = cur.execute('''
SELECT region,
SUM(amount) AS total,
SUM(CASE WHEN amount>=200 THEN amount ELSE 0 END) AS big_amt,
COUNT(CASE WHEN amount>=200 THEN 1 END) AS big_cnt
FROM sales GROUP BY region;
''').fetchall()
for r in rows:
print(r)
con.close()
实跑输出:
('East', 600, 500, 2)
('West', 200, 0, 0)
East 地区三笔订单 100+200+300=600,其中≥200 的是 200 和 300,合计 500、共 2 笔;West 两笔都不足 200,大额统计为 0。逻辑完全正确。
5.3 条件聚合的几个变体
| 需求 | 写法 |
|---|---|
| 统计满足条件的行数 | SUM(CASE WHEN cond THEN 1 ELSE 0 END) |
| 满足条件的金额求和 | SUM(CASE WHEN cond THEN amount END) |
| 满足条件的平均值 | AVG(CASE WHEN cond THEN score END) |
| 不命中时不参与平均 | AVG(CASE WHEN cond THEN score END)(ELSE 省略,NULL 被跳过) |
5.4 经典交叉报表完整例子
把上面所有技巧拼成一个完整报表:行是地区,列是 Q1~Q4 的销售额,最后一列合计。
-- 季度交叉报表(MySQL 8.0)
SELECT
region,
SUM(CASE WHEN quarter=1 THEN amount ELSE 0 END) AS q1,
SUM(CASE WHEN quarter=2 THEN amount ELSE 0 END) AS q2,
SUM(CASE WHEN quarter=3 THEN amount ELSE 0 END) AS q3,
SUM(CASE WHEN quarter=4 THEN amount ELSE 0 END) AS q4,
SUM(amount) AS year_total
FROM orders
WHERE year = 2026
GROUP BY region
ORDER BY year_total DESC;
这种报表在 BI 报表、月度总结里天天见。理解了"CASE 过滤行、聚合函数做汇总"这个组合,所有交叉表都是一个套路。
5.5 条件更新:一条 SQL 批量改值
CASE 还能写在 UPDATE 的 SET 里,实现"按字段值批量映射更新",避免写一堆 UPDATE ... WHERE ...:
-- 批量调整折扣(MySQL 8.0)
UPDATE goods
SET discount = CASE category
WHEN '电子' THEN 0.9
WHEN '服装' THEN 0.8
WHEN '食品' THEN 0.95
ELSE 1.0 -- 其他品类不打折
END;
注意 ELSE 1.0 一定要写,否则未命中的行 discount 会被更新成 NULL,把原来的值覆盖掉。这是条件更新最容易犯的错。
5.6 算转化率:条件聚合的延伸
运营最关心"转化率",本质就是"满足后续条件的行数 / 总行数":
-- 注册到下单转化率(MySQL 8.0)
SELECT
COUNT(*) AS reg_cnt, -- 注册用户数
SUM(CASE WHEN has_order=1 THEN 1 ELSE 0 END) AS order_cnt, -- 下过单的用户数
ROUND(SUM(CASE WHEN has_order=1 THEN 1 ELSE 0 END)/COUNT(*)*100, 2) AS conv_pct -- 转化率%
FROM user;
这类"分母是全量、分子是满足条件子集"的查询,是条件聚合最常见的业务形态。
6 条件逻辑速查表
| 场景 | 推荐写法 |
|---|---|
| 二选一 | IF(c,a,b) |
| 字段空值兜底 | IFNULL(col,'默认') |
| 多分支分类 | CASE WHEN ... THEN ... ELSE ... END |
| 字段值映射文案 | 简单 CASE col WHEN v THEN ... END |
| 动态排序 | ORDER BY CASE ... END |
| 条件计数 | SUM(CASE WHEN c THEN 1 ELSE 0 END) |
| 行转列 | SUM(CASE WHEN 维度=值 THEN 指标 END) |
| 除零保护 | 分母 / NULLIF(分母,0) |
7 常见坑与注意事项
1. 漏写 END:CASE 必须以 END 收尾,少一个就语法报错。报错信息往往指到语句末尾,回头检查 CASE 是否闭合。
2. ELSE 省略导致 NULL:忘了写 ELSE,又有不命中的行,结果列里冒出一堆 NULL。要么写 ELSE 兜底,要么业务上就接受 NULL。
3. 分支顺序敏感:CASE 从上往下匹配,命中即停。把"范围大的条件"写在前面会让后面的分支永远不命中。比如先写 WHEN score>=60 再写 WHEN score>=90,90 分也会被归到"及格"。
4. CASE 里写聚合、聚合里写 CASE 搞混:CASE 出现在聚合函数内部做条件统计,和在 HAVING 里对聚合结果再判断,是两回事。先想清楚"条件作用在行级还是组级"。
5. 行转列写死月份:报表月份是动态的,硬编码 m1/m2/m3 每加一个月就要改 SQL。生产环境常借助存储过程或应用层动态拼 SQL 生成列。
6. 用 IF 嵌套超过两层:可读性差且容易漏条件,分支一多立刻迁到 CASE WHEN。
7.1 常见错误写法对照
| 错误写法 | 问题 | 正确思路 |
|---|---|---|
| CASE score WHEN score>=60 | 简单 CASE 里又写条件,逻辑错位 | 改用搜索 CASE |
| CASE WHEN a>10 THEN '大' WHEN a>5 THEN '中' | 范围大的在前,a=8 也被归"大" | 从小到大排列分支 |
| SUM(CASE WHEN c THEN 1 ELSE 0)/COUNT(*) | 想算占比 | 写成 AVG(CASE WHEN c THEN 1 ELSE 0) 更直观 |
| CASE col WHEN NULL THEN '空' | 等值比 NULL 永远不成立 | 用 CASE WHEN col IS NULL |
最后一点很关键:NULL = NULL 在 SQL 里结果是"未知"而非真,所以简单 CASE 里 WHEN NULL 永远匹配不上,判断空值必须写 IS NULL。
8 练习题
练习 1:分数评级
表 exam(user_id, score),查出每个用户分数和评级:≥90 优秀、≥60 及格、否则不及格。
<details>
<summary>参考答案</summary>
-- 分数评级(MySQL 8.0)
SELECT user_id, score,
CASE
WHEN score >= 90 THEN '优秀'
WHEN score >= 60 THEN '及格'
ELSE '不及格'
END AS grade
FROM exam;
</details>
练习 2:统计各状态订单数
表 orders(status),一条 SQL 查出待付款、已发货、已完成各多少单。
<details>
<summary>参考答案</summary>
-- 各状态订单数(MySQL 8.0)
SELECT
SUM(CASE WHEN status=1 THEN 1 ELSE 0 END) AS unpaid,
SUM(CASE WHEN status=2 THEN 1 ELSE 0 END) AS shipped,
SUM(CASE WHEN status=3 THEN 1 ELSE 0 END) AS done
FROM orders;
</details>
练习 3:动态排序
用户表有 is_vip(0/1)和 login_time,要求 VIP 用户排前面,普通用户按最近登录时间倒序。
<details>
<summary>参考答案</summary>
-- VIP 优先排序(MySQL 8.0)
SELECT user_id, is_vip, login_time
FROM user
ORDER BY
CASE is_vip WHEN 1 THEN 0 ELSE 1 END,
login_time DESC;
</details>
练习 4:空值归类
表 orders 的 coupon_id 可能为 NULL,要求查询时把 NULL 显示为"未用券"。
<details>
<summary>参考答案</summary>
-- NULL 显示为文案(MySQL 8.0)
SELECT order_id,
IFNULL(CAST(coupon_id AS CHAR), '未用券') AS coupon
FROM orders;
</details>
练习 5:行列转换
表 sales(region, quarter, amount),查出每个地区 Q1、Q2、Q3、Q4 及全年合计。
<details>
<summary>参考答案</summary>
-- 季度交叉表(MySQL 8.0)
SELECT region,
SUM(CASE WHEN quarter=1 THEN amount ELSE 0 END) AS q1,
SUM(CASE WHEN quarter=2 THEN amount ELSE 0 END) AS q2,
SUM(CASE WHEN quarter=3 THEN amount ELSE 0 END) AS q3,
SUM(CASE WHEN quarter=4 THEN amount ELSE 0 END) AS q4,
SUM(amount) AS total
FROM sales
GROUP BY region;
</details>
本章小结
CASE WHEN 是 SQL 里少有的"越用越上瘾"的功能:一旦掌握条件聚合,很多原本要在应用层循环拼接的报表,一条 SQL 就出结果。记住两个核心:分支从上往下匹配、聚合里套 CASE 就是条件统计。下一篇进入性能优化的起点——索引。
下一篇预告:第 11 章 索引基础与创建
文章回复
0 条公开回复