1. 为什么 CASE WHEN 值得你专门花时间搞透
写 SQL 写了几年之后,你会慢慢发现一个规律:真正卡住你的往往不是那些复杂到离谱的关联查询,而是看起来特别基础、却暗藏玄机的小表达式。CASE WHEN 就是其中最典型的一个。它出现频率极高——报表取数、数据清洗、字段重分类、行转列、权限控制,几乎每个数据岗位的日常都会跟它打交道。但我也见过不少写了三五年 SQL 的人,对它的理解停留在“二选一”的层面,遇到嵌套条件、聚合场景或者 NULL 陷阱就抓瞎。
这个表达式本质上解决的是“对数据进行条件映射”的问题。你手头有一堆明细数据,想按照业务规则给每一条记录打上一个标签、归入一个分组、或者做一次数值换算,这就是 CASE WHEN 的用武之地。它不像 JOIN 那样负责把表连起来,也不像 GROUP BY 那样负责汇总整组数据,它的核心身份是“逐行计算的条件表达式”——你可以把它放在 SELECT 列表里作为输出字段,也可以放在 WHERE 子句里作为过滤条件,还可以放在 ORDER BY 里做自定义排序,甚至可以嵌进聚合函数里做条件计数。
我个人的建议是,不要把它当成一个“知识点”来学,而是当成一个“思维工具”来用。当你形成了“任何条件映射都可以用 CASE WHEN 表达”的思维习惯,很多复杂的 SQL 场景会瞬间变得清晰。这篇文章我会从语法细节讲到实际案例,再把我踩过的坑和总结的优化经验一并分享出来,希望能帮你彻底吃透这个表达式。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 两种写法:简单函数与搜索函数,用错场景会出大事
CASE WHEN 在 SQL 标准里其实有两种形态,几乎所有主流数据库(MySQL、PostgreSQL、SQL Server、Oracle、SQLite)都支持。它们各有适用场景,不能随便混用。
2.1 简单函数写法
第一种叫简单函数(simple case),语法是这样的:
sql复制CASE column_name
WHEN value1 THEN result1
WHEN value2 THEN result2
ELSE resultN
END
它做的事情很直白:拿 column_name 的值去和每个 WHEN 后面的 value 做等值比较,匹配上了就返回对应的 THEN 结果,全部匹配不上就返回 ELSE 里的值。
举个例子,假设订单表 orders 里有一个状态字段 status,1 代表待支付、2 代表已支付、3 代表已发货、4 代表已完成,你现在要把它转成业务人员能看懂的中文描述:
sql复制SELECT
order_id,
CASE status
WHEN 1 THEN '待支付'
WHEN 2 THEN '已支付'
WHEN 3 THEN '已发货'
WHEN 4 THEN '已完成'
ELSE '未知状态'
END AS status_name
FROM orders;
这种写法的优点是结构紧凑、阅读性好,一眼就能看出是“同一个字段的离散值映射”。但它有一个硬性限制:只能做等值比较。你没法在简单函数里直接写 WHEN status > 2 THEN ... 这种范围判断,一旦出现这种需求,就得切换到第二种写法。
2.2 搜索函数写法
第二种叫搜索函数(searched case),语法是这样的:
sql复制CASE
WHEN condition1 THEN result1
WHEN condition2 THEN result2
ELSE resultN
END
注意,CASE 关键字后面不跟表达式,WHEN 后面直接跟完整的条件判断,每个条件都可以是独立的逻辑表达式,支持 =、>、<、>=、<=、<>、LIKE、IN、BETWEEN、IS NULL 等任意运算,多个条件还可以用 AND、OR 组合。
继续用订单表来举例,现在要根据订单金额做等级划分:金额大于 5000 的算高价值订单,1000 到 5000 的算中价值订单,小于 1000 的算普通订单,另外还需要把金额为空的订单单独标记出来:
sql复制SELECT
order_id,
amount,
CASE
WHEN amount > 5000 THEN '高价值'
WHEN amount >= 1000 THEN '中价值'
WHEN amount IS NULL THEN '金额缺失'
ELSE '普通'
END AS order_grade
FROM orders;
这种写法是日常开发中使用频率最高的,自由度高,组合能力强,几乎所有复杂的条件映射都是用它实现的。
2.3 关键区别与易错点
这两种写法背后有一个特别容易被忽略的语义差异:搜索函数是“自上而下逐一判断条件,一旦命中就立即返回,后面不再执行”的。这意味着你把条件顺序写反了,结果就会出错。
我举个例子你就明白了。上面那个等级划分,如果有两个条件:
sql复制WHEN amount > 1000 THEN '中价值'
WHEN amount > 5000 THEN '高价值'
假如某条记录的金额是 8000,它先命中了 amount > 1000,直接返回“中价值”,后面的 amount > 5000 永远没机会执行。结果是高价值订单被错分到中价值里。这类错误在代码 review 时非常难发现,因为语法完全正确,数据量少时也不太容易暴露,一旦数据量大了,报表里的分类就会莫名其妙出错。
另外还有一个需要特别注意的地方:CASE WHEN 的匹配逻辑对 NULL 的处理。在简单函数写法下,CASE column WHEN NULL THEN ... 是永远匹配不上的,因为 SQL 里的 NULL 不等于任何值,包括 NULL 本身。你必须写 WHEN column IS NULL,或者改用搜索函数里的 CASE WHEN column IS NULL THEN ...。这是我觉得初学者最容易踩的坑,没有之一。
简单总结一下两种写法的差异,我用一张表帮你理清思路:
| 对比维度 | 简单函数 | 搜索函数 |
|---|---|---|
| 语法形式 | CASE 字段 WHEN 值 THEN 结果 | CASE WHEN 条件 THEN 结果 |
| 支持的操作 | 仅等值比较 | 任意逻辑表达式 |
| NULL 处理 | 不可直接判断,需配合 IS NULL | 可直接用 IS NULL |
| 适用场景 | 单字段离散值映射 | 范围判断、多条件组合、复杂规则 |
| 判断顺序 | 按书写顺序,命中即返回 | 按书写顺序,命中即返回 |
看到没有,两种写法在“顺序敏感性”上是一致的,都是命中即返回。这一点后面我还会在踩坑章节里详细展开。
3. 实战案例:行转列、分段统计、报表字段扩展是怎么用 CASE WHEN 起飞的
理解了语法还不够,真正体现 CASE WHEN 价值的是它在实战场景中的灵活运用。我挑了几个出镜率最高的场景,给你拆解一下具体的写法思路。
3.1 “行转列”的通用套路
行转列(长表转宽表)是数据处理中一块难啃的骨头,但 CASE WHEN 配合聚合函数可以轻松搞定。先看一个经典的场景。
假设有一张学生成绩表 student_scores,字段包括 student_id(学号)、subject(科目)、score(分数),数据长这样:
| student_id | subject | score |
|---|---|---|
| 1001 | 语文 | 88 |
| 1001 | 数学 | 92 |
| 1001 | 英语 | 85 |
| 1002 | 语文 | 76 |
| 1002 | 数学 | 81 |
| 1002 | 英语 | 79 |
现在想把它转成“每个学生一行,语文、数学、英语各占一列”的宽表,SQL 可以这样写:
sql复制SELECT
student_id,
MAX(CASE WHEN subject = '语文' THEN score END) AS chinese_score,
MAX(CASE WHEN subject = '数学' THEN score END) AS math_score,
MAX(CASE WHEN subject = '英语' THEN score END) AS english_score
FROM student_scores
GROUP BY student_id;
这段 SQL 的思路分两层理解:内层的 CASE WHEN subject = '语文' THEN score 把语文成绩提取出来,其他科目则为 NULL;外层的 MAX 在这个分组内把唯一的非 NULL 值取出来。由于 GROUP BY student_id 后每个学生只有一条语文记录,MAX 会把那条记录的值捞出来。
我把这个写法称为“通用行转列模板”,因为你只需要替换科目名和目标字段名称,就能适配大量场景。MySQL 里有一种更简洁的写法叫 IF(subject = '语文', score, NULL),效果和这里的 CASE WHEN 是完全一样的,原理也相同,但 CASE WHEN 是 SQL 标准语法,跨数据库迁移时不会被卡住,我建议你在正式项目里优先用 CASE WHEN。
3.2 分段统计:分组计数和汇总的灵活高效写法
行转列只是开胃菜,CASE WHEN 配合聚合函数做分段统计才是报表开发中的主力场景。比如你需要统计一个电商平台的订单金额分布:0-100 元、100-500 元、500-1000 元、1000 元以上各有几单,还要算每个区间的订单总额和平均客单价。
一个干净的写法是把“区间映射”写在聚合函数内部:
sql复制SELECT
CASE
WHEN amount < 100 THEN '0-100'
WHEN amount < 500 THEN '100-500'
WHEN amount < 1000 THEN '500-1000'
ELSE '1000以上'
END AS amount_range,
COUNT(*) AS order_count,
COALESCE(SUM(amount), 0) AS total_amount,
AVG(amount) AS avg_amount
FROM orders
WHERE pay_status = 'paid'
GROUP BY
CASE
WHEN amount < 100 THEN '0-100'
WHEN amount < 500 THEN '100-500'
WHEN amount < 1000 THEN '500-1000'
ELSE '1000以上'
END
ORDER BY MIN(amount);
有两个细节值得注意。
第一个是 GROUP BY 后面没有直接写别名,而是把整个 CASE WHEN 表达式又写了一遍。这是因为 SQL 的执行顺序是:先 FROM,再 WHERE,再 GROUP BY,再 SELECT,最后 ORDER BY。GROUP BY 执行时,SELECT 里定义的别名还不能引用,所以必须重复写表达式。不过这也有个衍生的优化方案——你可以把映射逻辑嵌套在子查询里,最外层再做分组,逻辑会更清晰:
sql复制SELECT
amount_range,
COUNT(*) AS order_count,
SUM(amount) AS total_amount
FROM (
SELECT
amount,
CASE
WHEN amount < 100 THEN '0-100'
WHEN amount < 500 THEN '100-500'
WHEN amount < 1000 THEN '500-1000'
ELSE '1000以上'
END AS amount_range
FROM orders
WHERE pay_status = 'paid'
) t
GROUP BY amount_range;
第二种做法我更喜欢,原因有两个:一是外层分组可以直接写别名,逻辑更顺;二是如果后面要加过滤条件(比如只统计某个区间的订单),可以直接在外层对 amount_range 加 WHERE,不需要重复 CASE WHEN 表达式。你想想看,当项目里有人改了一个区间阈值,第一种写法需要同步改 SELECT 和 GROUP BY 两处,漏改一处就出问题,第二种只改内层子查询一处,这个价值在日常维护中特别明显。
第三个细节是关于 COUNT 的。很多人习惯写 COUNT(CASE WHEN xxx THEN 1 END) 来做条件计数,这是完全正确的,因为它统计的是“非 NULL 记录数”,条件不满足时返回 NULL,NULL 不会被 COUNT 计入。如果你写 COUNT(CASE WHEN xxx THEN 1 ELSE 0 END) 做条件计数,结果会变成统计全表记录数,因为 ELSE 0 导致每条记录都有值,条件不满足的那些也被算进去了。这个常识很多人知道,但实际写的时候还是会顺手写错,我建议你每次写完都回头检查一下有没有这个隐患。
3.3 在 WHERE、ORDER BY 里给查询加“软逻辑”
CASE WHEN 不只是 SELECT 列表里的输出工具,它在 WHERE 和 ORDER BY 里同样能玩出花样。
场景一:按条件动态过滤。 比如前端页面传了两个参数:start_date 和 end_date,当 end_date 为空时,只查 start_date 当天及之后的数据;当 end_date 不为空时,查两个日期之间的数据。虽然这类参数拼接通常由后端代码处理,但有些场景直接把逻辑写进 SQL 反而更稳定:
sql复制SELECT order_id, create_date, amount
FROM orders
WHERE create_date >= start_date
AND (
end_date IS NULL
OR create_date <= end_date
);
这个写法实际上就是想表达“end_date 为空则不过滤上限”。用 CASE WHEN 也可以写成:
sql复制WHERE create_date >= start_date
AND create_date <= CASE WHEN end_date IS NULL THEN create_date ELSE end_date END;
不过我自己更倾向于 OR 的写法,因为 CASE WHEN 写在 WHERE 里会破坏字段上的索引利用,OR 的写法配合适当索引有时还能走索引。这里没有标准答案,建议你根据实际执行计划来选择。
场景二:自定义排序规则。 ORDER BY 加上 CASE WHEN 能让你完全掌控结果的排列顺序。举个真实的例子:公司状态字段有“停用”“试用”“正式”“注销”四个取值,你希望正式排在前面,试用排第二,停用排第三,注销排最后,而它们不是按字母序也不是按主键序排的:
sql复制SELECT company_id, company_name, status
FROM companies
ORDER BY
CASE status
WHEN '正式' THEN 1
WHEN '试用' THEN 2
WHEN '停用' THEN 3
WHEN '注销' THEN 4
ELSE 99
END;
这种写法的妙处在于,它可以把业务优先级注入 SQL 的排序逻辑中,报表前端不需要做额外处理。你甚至可以把它和字段值组合排序,比如先按状态排序,再按注册时间倒序,只需要在 ORDER BY 后面加一个逗号再接别的字段就行。
4. 嵌套与进阶技巧:当单层 CASE WHEN 不够用的时候
4.1 嵌套表达的精度问题
有些业务规则比较复杂,单层 CASE WHEN 很难优雅表达。比如某电商公司的佣金计算规则:订单已支付且金额大于 1000 时,佣金比例按 2% 计算;如果商品品类是“数码”,则比例提高到 3%;如果客户等级是“VIP”,再额外多加 1 个百分点。这种多条件叠加的场景,就需要嵌套:
sql复制SELECT
order_id,
amount,
CASE
WHEN pay_status = 'paid' THEN
CASE
WHEN category = '数码' THEN amount * 0.03
WHEN vip_flag = 1 THEN amount * 0.02
ELSE amount * 0.02
END
ELSE 0
END AS commission
FROM orders;
嵌套能让规则更精准,但我不建议为了嵌套而嵌套。嵌套超过两层之后,代码的可读性会急剧下降,review 的人和后来的维护者都会想骂人。我的经验是:如果业务规则没复杂到必须嵌套,优先用逻辑运算符(AND/OR)把条件合并在同一个 WHEN 后面,比如:
sql复制WHEN pay_status = 'paid' AND category = '数码' THEN amount * 0.03
WHEN pay_status = 'paid' AND vip_flag = 1 THEN amount * 0.02
WHEN pay_status = 'paid' THEN amount * 0.02
这样处理既保留了规则,又不需要嵌套。相比之下,我真心建议你养成的习惯是:永远用“多条件 + AND/OR”来横向简化,只有当条件之间真的存在“层级归属”关系(比如第一层判断订单有没有支付,没支付就不需要再判断任何东西,支付了才需要进入第二层细分)时,嵌套才是更清晰的选择。判断依据就一句话——把嵌套改成并列的 AND/OR 之后,如果不影响逻辑且可读性更好,就果断别嵌套;如果有歧义或者规则本身就是分层的,再嵌套。
4.2 使用表达式做动态计算
CASE WHEN 的 THEN 和 ELSE 部分不限于返回固定字符串或数值,它可以返回几乎任何合法的 SQL 表达式,包括算术运算、函数调用、子查询等。利用这个特性,你可以很方便地在 SQL 里做更多动态计算。
一个很常见的需求是“同比环比计算”。假设 sales 表里有月份和销售额,你要计算每个月的环比增长率(当月销售额相对上月的增幅),CASE WHEN 配合 LAG 窗口函数可以这么写:
sql复制SELECT
month_id,
sales_amount,
LAG(sales_amount) OVER (ORDER BY month_id) AS prev_month_amount,
CASE
WHEN LAG(sales_amount) OVER (ORDER BY month_id) IS NULL THEN NULL
WHEN LAG(sales_amount) OVER (ORDER BY month_id) = 0 THEN NULL
ELSE ROUND(
(sales_amount - LAG(sales_amount) OVER (ORDER BY month_id))
/ LAG(sales_amount) OVER (ORDER BY month_id)
* 100,
2
)
END AS growth_rate
FROM monthly_sales;
这里的 CASE WHEN 处理了两个边界:上月没有数据(第一行)时,以及上月销售额为 0 时,都不做除法,避免出现除以零的错误。这两个边界判断在实际业务数据里经常遇到,如果不主动处理,SQL 执行到一半就会报错,或者在可视化报表里显示一个让人摸不着头脑的无穷大。这个写法本身不难,难的是你能否在写 SQL 的一开始就意识到这些边界分支的存在。
4.3 CASE WHEN 与聚合函数组合的奇技淫巧
前面提到了 COUNT(CASE WHEN ... THEN 1 END) 的条件计数,其实 CASE WHEN 还能和 SUM、AVG、MIN、MAX 等各类聚合函数组合出许多实用的技巧。
技巧一:多条件去重计数。 统计 2024 年每个品类下有付费行为的用户数,同时要区分新用户和回访用户。可以直接在 COUNT 里放 DISTINCT:
sql复制SELECT
category,
COUNT(DISTINCT CASE WHEN user_type = 'new' THEN user_id END) AS new_user_cnt,
COUNT(DISTINCT CASE WHEN user_type = 'return' THEN user_id END) AS return_user_cnt
FROM orders
WHERE pay_status = 'paid'
AND order_date BETWEEN '2024-01-01' AND '2024-12-31'
GROUP BY category;
这里的原理是:COUNT(DISTINCT) 会忽略 NULL,CASE WHEN 条件不满足时返回 NULL,所以只有满足条件的 user_id 参与了去重计数。这个组合既能去重又能加条件,报表场景里非常实用,比先 WHERE 过滤再 GROUP BY 再分开统计灵活得多。
技巧二:用 MIN/MAX 求条件极值。 假设你想知道每个客户的第一笔订单金额和最大一笔订单金额,可以在聚合函数里加条件:
sql复制SELECT
customer_id,
MIN(CASE WHEN order_seq = 1 THEN amount END) AS first_order_amount,
MAX(amount) AS max_order_amount
FROM (
SELECT
customer_id,
amount,
ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY order_date) AS order_seq
FROM orders
) t
GROUP BY customer_id;
这个写法在订单分析里很常用,内层子查询用窗口函数给每个客户的订单按时间排了序号,外层再用 CASE WHEN 把第一笔订单的金额挑出来。需要注意的是,如果某个客户恰好第一笔订单金额就是最大值,两个字段显示同值,这不代表写错了,而是真实业务就是这样。
技巧三:先聚合再分支。 有时候先做聚合,再把聚合结果拿到 CASE WHEN 里做分类,比把 CASE WHEN 放在聚合内部更符合直觉。比如你要按客户统计总消费额,然后给客户打上“高、中、低”的标签:
sql复制SELECT
customer_id,
total_amount,
CASE
WHEN total_amount >= 10000 THEN '高消费'
WHEN total_amount >= 5000 THEN '中消费'
ELSE '低消费'
END AS customer_level
FROM (
SELECT customer_id, SUM(amount) AS total_amount
FROM orders
WHERE pay_status = 'paid'
GROUP BY customer_id
) t;
这里如果把 CASE WHEN 放在聚合函数内部写,会非常别扭,但放在外层做就顺理成章。这也印证了一个经验:不要执着于用 CASE WHEN 做所有事,它在聚合子查询之外做分类往往是更清晰的方案。
5. 我踩过的坑:NULL 判断、ELSE 缺失、类型不一致和性能误判
写 CASE WHEN 这么多年,我踩过的坑也算攒了一堆。这些坑单看都不大,但一旦碰到,排查起来极其耗时。我把几个典型的坑连同排查思路整理出来,希望你看了之后能少走弯路。
5.1 坑一:简单函数模式下的 NULL 判断永远不命中
这是个非常隐蔽的坑。有人想判断某字段是否为 NULL,图省事写了这样的 SQL:
sql复制SELECT
customer_id,
CASE phone
WHEN NULL THEN '无手机号'
ELSE '有手机号'
END AS phone_status
FROM customers;
结果是,所有客户都返回了“有手机号”——哪怕该客户 phone 字段确实是 NULL。原因我在前面提过:SQL 里 NULL 不等于任何值,包括 NULL 本身。简单函数模式的 CASE phone WHEN NULL 实际上是做了 phone = NULL 的比较,这个比较的结果既不是 TRUE 也不是 FALSE,而是 UNKNOWN,WHEN 判断要求结果是 TRUE 才命中,所以统一落入 ELSE。
正确写法是:
sql复制CASE
WHEN phone IS NULL THEN '无手机号'
ELSE '有手机号'
END
或者简单函数与搜索函数结合:
sql复制CASE
WHEN phone IS NULL THEN '无手机号'
ELSE '有手机号'
END
5.2 坑二:ELSE 缺失导致大量 NULL
CASE WHEN 如果没写 ELSE,那么在没有任何 WHEN 条件命中时,表达式返回 NULL。这在某些计算场景下会产生连锁反应。
举一个真实例子。某次我写一个佣金报表 SQL,按订单金额判断佣金费率,当时不知道哪个环节少写了 ELSE:
sql复制CASE
WHEN amount > 5000 THEN 0.05
WHEN amount > 1000 THEN 0.03
END
金额小于等于 1000 的订单,佣金费率全是 NULL,算出来的佣金也全是 NULL,报表里一堆空白。更麻烦的是,如果之后还有 JOIN 或子查询,NULL 会继续蔓延,甚至影响后续的聚合统计。排查这个过程很折磨人,因为 SQL 语法没问题,数据库中也有数据,只是因为阈值没覆盖全。
所以我现在写 CASE WHEN 有个铁律:只要拿不准所有取值是否都被覆盖,就必须写 ELSE。 即使你觉得业务上不可能出现其他值,写一个 ELSE 兜底(比如 ELSE 0 或 ELSE '其他')也不会有什么事,反而能排除一大类隐患。真要说代价,最多多一行代码,但换来的却是结果的可预期性。
5.3 坑三:THEN 返回值类型不一致导致隐式转换
这个坑在跨数据库迁移或者字段类型设计不合理时容易被触发。比如某个 CASE WHEN 的 THEN 分支里,有的返回字符串,有的返回数值:
sql复制CASE
WHEN flag = 1 THEN '启用'
WHEN flag = 0 THEN 0
ELSE '未知'
END
有些数据库会执行隐式类型转换,把这个表达式整体转成字符串,输出结果变成 '0' 而不是数字 0。还有一些数据库直接报类型不匹配错误。这种错误在写 SQL 时很难发现,往往要等数据跑出来、前端展示异常时才会被察觉。
我遇到过一种更隐蔽的类型问题:THEN 返回的是小数点位数不同的数值。比如 THEN 1 和 THEN 1.00,在不同数据库里可能会被解析成 INTEGER 和 NUMERIC 两种类型。你用 GROUP BY 对这个表达式分组时,数据库可能把它们当成不同的分组键,导致本应聚合在一起的数据被拆成两组,结果完全错乱。
排查思路: 当你在 GROUP BY 或 DISTINCT 结果里发现“同样的逻辑值被分成多行”,先检查 CASE WHEN 各分支的返回类型是不是一致。检查方式很简单,把 THEN 的结果类型统一成同一类型,比如都转成字符串或都保留相同的小数位。
5.4 坑四:把条件字段套函数,导致索引失效
很多人知道不能在 WHERE 的字段上套函数,但一旦换成 CASE WHEN 就忘记了。例如:
sql复制SELECT *
FROM orders
WHERE CASE WHEN create_date >= '2024-01-01' THEN 1 ELSE 0 END = 1;
这个写法逻辑上没错,但它让 create_date 上的索引彻底失效了,因为数据库优化器无法对这个表达式做常规的索引范围扫描。实测下来,数据量大时查询速度会慢好几倍甚至十几倍。
排查思路: 写完 SQL 用 EXPLAIN 看执行计划,留意有没有出现全表扫描(Seq Scan / Table Scan)。如果扫描行数和总行数一样,就说明条件无法利用索引。这时不如直接去掉 CASE WHEN,把条件重新写成常规 WHERE 形式:
sql复制SELECT *
FROM orders
WHERE create_date >= '2024-01-01';
这里的教训是:CASE WHEN 是很强大的工具,但它不是万能的。在 WHERE 子句里,能用普通条件表达式说清楚的事,就不要用 CASE WHEN 包一层,后者基本都会让优化器失去对索引的利用能力,得不偿失。
5.5 坑五:性能上盲目使用大 CASE WHEN 批量转换
有一种场景我见过很多人踩:在报表查询里写一个超级长的 CASE WHEN,比如把几百个编码映射成中文名称。这种映射表类的逻辑,如果直接写在 SQL 里,会带来两个问题:一是 SQL 文本变得极长,解析开销上升,动态执行计划困难;二是每次要改映射关系都得改 SQL 发布,运维成本高。
更合理的做法通常是准备一张维表,把编码和中文名称的映射关系存在表里,用 JOIN 去关联。如果实在没有维表权限,或者只是临时取数,用 CASE WHEN 也没问题,但要注意控制规模,超过二三十个分支时建议换个方案。
我个人的经验标准是:五六个分支以内,CASE WHEN 很合适;超过十个分支,我会强烈建议维表方案。这个阈值不绝对,但可以作为你判断的第一参考。
6. 从易读到可维护:让 CASE WHEN 代码变得优雅的几个习惯
写 CASE WHEN 不只是把功能跑通就完事了,代码的可读性和可维护性同样重要。尤其是团队协作的项目里,你的 SQL 很可能被其他同事 review、修改或复用。我建议你养成下面这几个习惯。
6.1 每个 WHEN 配一个缩进级别,别挤成一行
我见过一些“一行流”的写法:
sql复制SELECT CASE WHEN a=1 THEN 1 WHEN a=2 THEN 2 END FROM t;
这种东西自己写完可能转眼就忘了,更别说别人来 review。多分支的 CASE WHEN 建议展开成多行,每个 WHEN 独立一行,THEN 对齐缩进,这样检查条件逻辑时一目了然。
sql复制SELECT
CASE
WHEN a = 1 THEN '一'
WHEN a = 2 THEN '二'
ELSE '其他'
END AS num_text
FROM t;
6.2 给每个分支写注释,特别是业务口径复杂的地方
如果分支含义不是一眼能看明白的(比如 WHEN amount * 0.8 > 5000 这种业务规则),建议在分支行尾加注释,写清楚这个条件的业务含义。我自己写 SQL 时有个习惯:凡是涉及业务口径的分支,注释里会把口径来源写明,这样三个月后回来维护时就不需要再翻需求文档了。
sql复制SELECT
order_id,
CASE
-- 满减活动后实付金额仍大于5000的,属于高净值订单
WHEN amount - discount_amount > 5000 THEN '高净值'
ELSE '普通'
END AS order_level
FROM orders;
6.3 把复杂的 CASE WHEN 抽到子查询或视图里
如果一个查询里同时出现多个复杂的 CASE WHEN,不仅代码冗长,优化器也容易被折腾。我的建议是,把复杂的映射逻辑先抽到一个子查询(或 WITH 临时表)里,外层再基于它做聚合和过滤。这样有几个好处:一是子查询和外层职责分离,读起来更清晰;二是外层可以直接引用子查询算好的字段名,不用重复写表达式;三是如果映射逻辑修改,只需要改一处。
这个思路和我前面举的“分段统计”例子一脉相承。如果你意识到同一个 CASE WHEN 表达式在 SELECT 和 GROUP BY 里出现多次,那么几乎就意味着你应该把它抽到子查询里了。
6.4 优先使用 COALESCE 处理 NULL,而不是用 CASE WHEN
CASE WHEN 和 COALESCE 在功能上有重叠——它们都可以处理 NULL 的默认值问题。最简单的场景下,用 COALESCE 明显更简洁:
sql复制-- 普通
SELECT COALESCE(phone, '暂无') FROM customers;
-- 绕远路
SELECT CASE WHEN phone IS NULL THEN '暂无' ELSE phone END FROM customers;
COALESCE 的语义是“按参数顺序返回第一个非 NULL 值”,这个逻辑在很多场景下比 CASE WHEN 更直观,而且性能上通常也更有优势(数据库对 COALESCE 有专门优化)。所以遇到“把字段为空时替换成默认值”这类需求,别再用 CASE WHEN 了,让 COALESCE 干活就好了。
7. 实测对比:不同数据库下的行为差异
CASE WHEN 是 SQL 标准中的表达式,主流数据库基本都支持,但在一些边缘细节上还是存在差异。我把我在实际项目中遇到的差异整理出来,你在跨数据库写 SQL 时可以参考。
| 数据库 | 简单函数是否支持 | 搜索函数是否支持 | 特殊行为 |
|---|---|---|---|
| MySQL | 支持 | 支持 | 类型比较宽松,字符串和数字会做隐式转换 |
| PostgreSQL | 支持 | 支持 | 类型比较严格,隐式转换会报错 |
| SQL Server | 支持 | 支持 | 支持 ISNULL 函数,但 COALESCE 更标准 |
| Oracle | 支持 | 支持 | 支持 NVL、DECODE,DECODE 类似简单函数 |
| SQLite | 支持 | 支持 | 类型动态,宽松 |
PostgreSQL 对类型的严格性是我特别想强调的。比如你在 PostgreSQL 里写:
sql复制CASE
WHEN amount > 100 THEN '高'
WHEN amount > 50 THEN 1
ELSE '低'
END
它很可能直接报错“CASE types text and integer cannot be matched”,因为各分支返回类型不一致。而在 MySQL 里,这个 SQL 通常能运行,返回的类型由数据库推断。这个差异不是说谁好谁坏,而是提醒你:写 CASE WHEN 前先想清楚每个分支返回的数据类型是否一致,这在跨数据库迁移时尤其重要。
另外,Oracle 里那个 DECODE 函数也值得一提。它的作用和简单函数类似,但语法更别扭。如果你在 Oracle 项目里看到 DECODE,也别慌,本质上它和 CASE 字段 WHEN 值 THEN 结果 END 是同一个思路,只是写法上靠的是逗号分隔参数。我这个人在 Oracle 里一般还是用标准 CASE WHEN,因为 DECODE 有几个限制(不能做范围判断,NULL 处理需要额外技巧),而 CASE WHEN 是完整的搜索函数,能处理的情况更广。
8. 一些实际项目中总结出来 CASE WHEN 与窗口函数搭配的经验
CASE WHEN 和窗口函数(ROW_NUMBER、RANK、SUM OVER 等)搭配使用,能解决很多分析场景里的痛点。这里分享几个我最常用的组合。
8.1 分组内取“第一个满足条件的记录”
业务上经常有这样的需求:按用户分组,找到每个用户第一笔满足特定条件的订单。比如找出每个客户第一笔超过 1000 元的订单信息,这个需求看起来要过滤、要排序、要分组,好像很麻烦,但用窗口函数和 CASE WHEN 可以这样写:
sql复制SELECT customer_id, order_id, amount, order_date
FROM (
SELECT
customer_id,
order_id,
amount,
order_date,
ROW_NUMBER() OVER (
PARTITION BY customer_id
ORDER BY order_date
) AS rn
FROM orders
WHERE amount > 1000 AND pay_status = 'paid'
) t
WHERE rn = 1;
这里没有直接用到 CASE WHEN,但它的外层判断 rn = 1 本质上就是在做“条件满足才取第一条”的过滤。如果你还想额外标记“是否是首单”,可以在外层用 CASE WHEN 加一个字段:
sql复制SELECT
*,
CASE
WHEN rn = 1 THEN '首单'
ELSE '非首单'
END AS is_first
FROM (
-- 上面那个子查询
) t;
8.2 条件累计和:按条件过滤后再做窗口累加
“按条件过滤后再做累加”这个需求如果用 JOIN 会很笨重,但用 CASE WHEN 套窗口函数就非常轻巧。
比如你想看每个用户只统计“已支付”订单的消费累计趋势:
sql复制SELECT
customer_id,
order_date,
amount,
SUM(CASE WHEN pay_status = 'paid' THEN amount ELSE 0 END) OVER (
PARTITION BY customer_id
ORDER BY order_date
) AS paid_amount_cumulative
FROM orders;
这里 CASE WHEN 把未支付订单的金额映射为 0,窗口函数的累加就只累加已支付金额。外层跑出来的每一行都是该用户截至当前订单日期已支付金额的累计值,拿来画折线图刚好。
8.3 用 CASE WHEN 做排名分桶
排名分桶是另一个经典应用。比如你想按每个用户的消费排名,把前 10% 标记为“头部用户”,后 30% 标记为“腰部用户”,其余标记为“长尾用户”。可以先算排名百分比,再用 CASE WHEN 分桶:
sql复制SELECT
customer_id,
total_amount,
CASE
WHEN percent_rank() OVER (ORDER BY total_amount DESC) <= 0.1 THEN '头部'
WHEN percent_rank() OVER (ORDER BY total_amount DESC) <= 0.4 THEN '腰部'
ELSE '长尾'
END AS user_bucket
FROM (
SELECT customer_id, SUM(amount) AS total_amount
FROM orders
WHERE pay_status = 'paid'
GROUP BY customer_id
) t;
这里面 percent_rank() 返回的是 0 到 1 之间的排名比例。我特意把计算逻辑写在子查询之外,就是为了让 CASE WHEN 读起来更清晰,分桶规则一眼就能看懂。如果哪天运营说“头部用户改成 15%”,只需要改第一个数字即可,维护成本比写一堆嵌套条件低得多。
9. 写在最后:我个人的实践判断标准
回到标题本身——SQL 之 CASE WHEN 用法详解。写了这么多,其实真正想表达的是:CASE WHEN 的语法很简单,翻文档五分钟就能看完,难的是在真实业务里形成“用它来解决问题”的判断力。
我自己在实践中积累了一套判断标准,每次写 CASE WHEN 时都会先过一遍:
- 能用简单函数就不要用搜索函数:等值映射时简单函数更紧凑、更清晰;
- 能拼条件就不要嵌套:同一个层级里能用 AND/OR 连接,就先不嵌套;
- 能写 ELSE 就写 ELSE:除非你 100% 确定所有取值都覆盖到了;
- 能让 COALESCE 干的活就别让 CASE WHEN 干:默认值替换用 COALESCE 更简洁;
- WHERE 里能不套 CASE WHEN 就不套:避免索引失效;
- 同一个表达式出现两次以上,就要考虑抽子查询:不重复自己;
- 字段分支类型必须一致:跨数据库场景尤其要确认;
- CASE WHEN 本质是表达式,不是语句:它能用在 SELECT、WHERE、ORDER BY、GROUP BY、HAVING 以及各种函数内部,想清楚这一点,你会发现它的用武之地比想象中大得多。
最后再分享一个小技巧:遇到特别复杂的 CASE WHEN,我习惯先在草稿纸上把条件分支画成表格,确认没有遗漏、没有冲突、顺序正确,再翻译成 SQL 代码。这个习惯帮我避开了大量“顺序写反导致分类错误”的坑,也推荐给你。
