SQL里的CASE表达式,是很多人在学会SELECT、WHERE、GROUP BY之后,最容易卡住的一个知识点。它不像JOIN那样要理解表关系,也不像子查询那样要理清嵌套层级,但真正写业务查询时,你很快会发现:只要涉及“按条件算出一个新值”的需求,CASE表达式都是最直接的解法。几乎所有主流关系型数据库管理系统,包括MySQL、PostgreSQL、SQL Server、Oracle,都支持同一套CASE语法,学会之后不需要为某个数据库单独记一套写法。
下面不按语法手册的顺序讲,而是按实际落地顺序把CASE表达式拆开过一遍:先理解两种基础语法,再逐个跑通SELECT、WHERE、ORDER BY、GROUP BY里的用法,最后补上嵌套、聚合、批量更新、排错思路和面试题常见问法。如果你已经能写基本的查询,但对CASE表达式的印象停留在“见过但用不熟”,这一篇可以直接照着练。
1. CASE表达式到底是什么,为什么每个写SQL的人都要掌握
1.1 从一个最简单的需求说起
假设你有一张学生成绩表,结构大概是这个样子:
CREATE TABLE student_score ( student_id INT, student_name VARCHAR(50), subject VARCHAR(20), score INT );现在业务要求:60分以上显示“及格”,60分以下显示“不及格”。如果没有CASE表达式,大部分人的第一反应是把查询结果拉到程序里,再写一个for循环做if else判断。这样当然能实现,但问题也很明显:
- 数据量小的时候还好,数据量大时要把所有结果集都加载到应用内存里做二次处理,浪费带宽和内存。
- 如果报表、接口、导出文件都要用到同一个等级字段,每个地方都得复制同一段判断逻辑,很容易漏改。
- 很多分析场景里你根本拿不到程序代码,只能用SQL直接输出结果,比如BI报表、临时取数、数据库客户端查询。
用CASE表达式,这个需求在SQL内部就解决了:
SELECT student_name, score, CASE WHEN score >= 60 THEN '及格' ELSE '不及格' END AS grade_label FROM student_score;执行之后会多出一列grade_label,完全不依赖外部程序。我第一次看到这段代码时的反应是:这不就是SQL里的if else吗?理解没错,但需要补一个关键点:CASE是表达式,不是语句。它本身不修改数据,不控制流程,只负责根据条件返回一个值。所以它可以用在任何“能放表达式”的位置,包括SELECT、WHERE、ORDER BY、GROUP BY,甚至UPDATE的SET语句里。
1.2 两种语法:简单CASE和搜索CASE
CASE表达式有两种写法,很多教材会区分成“简单CASE表达式”和“搜索CASE表达式”。
简单CASE表达式的写法是:
CASE 列名 WHEN 值1 THEN 结果1 WHEN 值2 THEN 结果2 ELSE 默认结果 END它做的事情是拿“列名”和后面的每个值做等值比较。比如性别字段存的是0和1,想显示成“男”“女”:
SELECT user_name, CASE gender WHEN 1 THEN '男' WHEN 0 THEN '女' ELSE '未知' END AS gender_label FROM users;搜索CASE表达式的写法是:
CASE WHEN 条件1 THEN 结果1 WHEN 条件2 THEN 结果2 ELSE 默认结果 END搜索CASE不限定某一列,每个WHEN后面是一个完整的布尔表达式,可以做范围判断、多列组合、甚至子查询。比如按分数范围评级:
SELECT student_name, score, CASE WHEN score >= 90 THEN '优秀' WHEN score >= 80 THEN '良好' WHEN score >= 60 THEN '及格' ELSE '不及格' END AS level FROM student_score;从使用频率上看,搜索CASE比简单CASE更常用。因为等值映射在很多场景里可以用JOIN字典表替代,而范围判断、组合条件往往只能靠搜索CASE来写。
| 比较项 | 简单CASE表达式 | 搜索CASE表达式 |
|---|---|---|
| 写法位置 | CASE后紧跟列名 | CASE后直接WHEN |
| 比较方式 | 只能做等值比较 | 支持范围、多列、布尔表达式 |
| 典型场景 | 字典值映射、状态码转文字 | 成绩评级、动态条件、复杂分类 |
| 灵活性 | 较低 | 较高 |
1.3 先记住这几条判断标准
CASE表达式看着简单,真正用起来容易在“要不要用、用哪种、放在哪”上面犹豫。我给自己定的判断顺序是这样的:
- 先看这个值是不是只靠一条查询就能算出来。如果是,优先考虑CASE。
- 再看匹配条件是不是等值。固定离散值用简单CASE;范围、比较、
AND/OR组合用搜索CASE。 - 最后看计算位置。展示层翻译用
SELECT里的CASE;过滤用WHERE里的CASE;排序用ORDER BY里的CASE;分组统计用GROUP BY或聚合函数里的CASE。
还有一个容易被忽略的点:简单CASE虽然写法短,但它比较的是“等于”。如果被比较列里存在NULL,NULL = NULL不会返回真,所以任何WHEN都匹配不上,只能走到ELSE。搜索CASE可以显式写WHEN column IS NULL,把NULL单独处理。这个差别在做数据清洗时非常关键。
2. 从最简单的例子开始:把数据翻译成人话
2.1 在SELECT里做字段值映射
CASE最基础、最不容易写错的用法,就是SELECT里的字段映射。实际开发中最常见的是状态字段,数据库里存的是数字,页面上要显示中文。比如订单状态:
SELECT order_id, status, CASE status WHEN 1 THEN '待支付' WHEN 2 THEN '已支付' WHEN 3 THEN '已发货' WHEN 4 THEN '已完成' ELSE '已取消' END AS status_name FROM orders;这段代码的价值在于,业务方要的数据在数据库查询阶段就已经处理完了,不需要后端再遍历数组做二次转换。
这里我建议养成加别名的习惯,也就是END AS status_name。不加别名在大多数数据库里也能跑,但列名会变成一大段CASE表达式,在BI工具、导出Excel时非常难看。早期我也偷懒不写别名,后来报表组反复来问“这列到底是什么”,从那以后所有CASE都写别名。
2.2 搜索CASE处理范围条件
范围条件用简单CASE写不了,必须用搜索CASE。最典型的场景就是成绩等级、年龄分段、金额分层。假设要按分数显示等级,90到100优秀,80到89良好,60到79及格,其他不及格:
SELECT student_name, score, CASE WHEN score >= 90 THEN '优秀' WHEN score >= 80 THEN '良好' WHEN score >= 60 THEN '及格' ELSE '不及格' END AS level FROM student_score;这段代码看起来没问题,但有一个执行顺序容易被忽略:**CASE的WHEN是从上往下逐个判断的,碰到第一个结果为真的条件就返回,后面的WHEN不再执行。**所以写范围条件时,要按从大到小或从小到大的顺序排列,保证逻辑清晰。
如果把条件写成这样:
CASE WHEN score >= 60 THEN '及格' WHEN score >= 80 THEN '良好' WHEN score >= 90 THEN '优秀' ELSE '不及格' END结果就是90分以上的学生也会被算成“及格”。SQL不会报错,但结果完全不符合业务预期。这个问题在代码评审里出现频率非常高,排查的时候第一眼就要检查WHEN的顺序。
2.3 ELSE不写会怎样
很多人写CASE表达式时不写ELSE,默认认为“没匹配到就返回NULL也没关系”。某些场景确实没关系,但有两个地方要特别注意。
第一个是数据统计。COUNT、SUM这类聚合函数遇到NULL会直接忽略,不会报错。比如你想统计及格人数,写了这样一句:
SELECT COUNT(CASE WHEN score >= 60 THEN student_id END) AS pass_cnt, COUNT(*) AS total_cnt FROM student_score;不满足条件时返回NULL,COUNT只统计非NULL的行,那么这个数字刚好等于及格人数。这个写法是安全的,也是推荐的一种写法。
但如果你用的是SUM,并且THEN后面返回0或1,不写ELSE时,不满足条件会返回NULL,SUM遇到NULL通常会忽略,结果看起来正常。可一旦你后续拿这个字段做除法,NULL可能会导致最终结果变成NULL。我建议在SUM这种需要数值累加的场景里显式写ELSE 0,把兜底逻辑写清楚。
第二个是展示层。如果不写ELSE,当数据出现预期之外的值时,界面上会直接显示空白或“null”。用户看到这个结果不会认为“数据异常”,而会认为是系统bug。显式写ELSE '未知'至少能给下游一个明确信号:这个值不在预期范围内。
注意:CASE表达式是否写ELSE,不是语法强制要求,而是业务稳定性要求。正式报表里建议默认写ELSE,除非你非常确定不可能出现未匹配值。
3. 进阶用法:在WHERE、ORDER BY、GROUP BY里用CASE
3.1 用CASE做动态排序
ORDER BY后面也能放CASE表达式,这个技巧在日常开发中很实用。比如任务列表里有状态字段,值是“待处理”“处理中”“已完成”,产品要求固定顺序:处理中排最前面,待处理排中间,已完成排最后;同一状态内再按创建时间倒序。
不写CASE时,你只能按状态字段的原始值排序,不一定符合业务顺序。用CASE可以这样写:
SELECT task_id, title, status, created_at FROM tasks ORDER BY CASE status WHEN 'processing' THEN 1 WHEN 'pending' THEN 2 WHEN 'done' THEN 3 ELSE 4 END, created_at DESC;这里CASE返回一个数字,数字越小越靠前。多个排序条件用逗号连接,先按CASE结果排,再按创建时间排。相比在程序里排序,这种方式能直接配合数据库的分页,避免把所有数据拉到内存里再排一遍。
需要多说一句:如果这些排序条件来自用户输入,要先做好参数校验和权限控制,不要直接拼接不可控内容。这是SQL开发的基本安全要求,和CASE本身无关,但写动态查询时很容易忽略。
3.2 用CASE做自定义分组和行转列
GROUP BY后面放CASE,可以按自定义逻辑分组。比如把成绩按分数段分组,统计每个等级有多少人:
SELECT CASE WHEN score >= 90 THEN '优秀' WHEN score >= 80 THEN '良好' WHEN score >= 60 THEN '及格' ELSE '不及格' END AS score_level, COUNT(*) AS cnt FROM student_score GROUP BY CASE WHEN score >= 90 THEN '优秀' WHEN score >= 80 THEN '良好' WHEN score >= 60 THEN '及格' ELSE '不及格' END;这里有个细节:SELECT里的CASE和GROUP BY里的CASE必须保持一致。有些数据库允许在GROUP BY里直接写别名GROUP BY score_level,但标准SQL里更稳妥的做法是重复写完整表达式,避免不同数据库行为不一致。
CASE在分组统计里还有一个经典用途是行转列。假设成绩表里每行是一个学生的一门课成绩,现在想把语文、数学、英语成绩放到同一行展示:
SELECT student_name, MAX(CASE WHEN subject = '语文' THEN score END) AS chinese, MAX(CASE WHEN subject = '数学' THEN score END) AS math, MAX(CASE WHEN subject = '英语' THEN score END) AS english FROM student_score GROUP BY student_name;这段SQL的核心逻辑是:先用CASE WHEN subject = '语文' THEN score把语文成绩挑出来,其他科目返回NULL;再在外面套MAX,让同一学生的多行数据合并成一行,同时忽略NULL。用MIN或SUM也可以,但因为成绩是单值,MAX和MIN的语义是“取那个非NULL值”。
如果希望没有成绩的科目显示0而不是NULL,可以把ELSE写成0:
MAX(CASE WHEN subject = '语文' THEN score ELSE 0 END) AS chinese但这里要注意区分业务含义:NULL表示“该学生没有这条记录”,0表示“语文成绩确实是0”。两者不能混为一谈。我更建议在行转列时保留NULL,等数据进入报表层再决定怎么展示。
3.3 用CASE处理NULL和空字符串
数据清洗是CASE表达式的另一个主场。数据库里经常会出现两种“空”:一种是NULL,表示没有值;另一种是空字符串,表示录入了空内容。业务上往往要区别对待。
比如客户表里,手机号字段可能是NULL,也可能是空字符串。现在要展示成一段说明文字:
SELECT customer_id, CASE WHEN phone IS NULL THEN '手机号未录入' WHEN phone = '' THEN '手机号为空字符串' ELSE phone END AS phone_status FROM customers;这里顺序很重要:一定要先判断IS NULL,再判断等于空字符串。如果反过来,先写phone = '',NULL行不会满足这个条件,会继续往下走,最后落到ELSE。逻辑上有点绕,但SQL对NULL的判断就是这样:任何和NULL做等值比较的结果都是“未知”,不会返回TRUE。
如果需求只是“把NULL和空字符串统一显示成未填写”,可以简化成:
CASE WHEN COALESCE(phone, '') = '' THEN '未填写' ELSE phone ENDCOALESCE先把NULL转成空字符串,再判断是否为空。这是一种有效的简化,但对新手来说可读性稍差。我建议先用完整写法,确保每一步都看得明白,再考虑简化。
4. 再复杂一点的场景:聚合、嵌套、UPDATE和去重
4.1 CASE与聚合函数配合做条件统计
条件统计是CASE表达式在报表场景里最常用的组合。比如统计成绩表里优秀人数、及格人数、不及格人数,一次查询全部算出来:
SELECT COUNT(CASE WHEN score >= 90 THEN 1 END) AS excellent_cnt, COUNT(CASE WHEN score >= 60 AND score < 90 THEN 1 END) AS pass_cnt, COUNT(CASE WHEN score < 60 THEN 1 END) AS fail_cnt FROM student_score;这里使用COUNT,只统计非NULL行的数量。满足条件时返回1,不满足时返回NULL,所以最终数字就是满足条件的行数。也可以使用SUM:
SUM(CASE WHEN score >= 90 THEN 1 ELSE 0 END) AS excellent_cnt两种写法结果一样。区别在于:COUNT不写ELSE,看起来更简洁;SUM必须配合ELSE 0,否则不满足条件时返回NULL,后续计算可能被NULL污染。我个人在统计场景更倾向于用SUM(CASE WHEN ... THEN 1 ELSE 0 END),因为返回值类型更固定,不容易出现NULL传播的问题。
4.2 嵌套CASE什么时候值得用
CASE表达式可以嵌套,也就是在一个THEN或WHEN里再写一个CASE。例如按分数和补考次数综合评价:
SELECT student_name, score, makeup_count, CASE WHEN score >= 60 THEN CASE WHEN makeup_count = 0 THEN '正常通过' ELSE '补考通过' END ELSE '未通过' END AS final_result FROM student_score;这个嵌套逻辑本身没问题,但读起来已经有点费力。原因是CASE和WHEN的数量一多,人眼很难快速匹配每个分支。我的建议是:嵌套最多控制在两层,如果业务规则更复杂,优先拆成子查询或CTE,先计算出一个中间字段,再在外部查询里写CASE。
比如上面的需求,可以先用一个子查询算出考试类型,再在外层评级:
WITH score_info AS ( SELECT student_name, score, makeup_count, CASE WHEN makeup_count = 0 THEN '正常' ELSE '补考' END AS exam_type FROM student_score ) SELECT student_name, score, CASE WHEN score >= 60 AND exam_type = '正常' THEN '正常通过' WHEN score >= 60 AND exam_type = '补考' THEN '补考通过' ELSE '未通过' END AS final_result FROM score_info;这种写法多写了几行SQL,但每一层只解决一个问题,后期维护时不需要展开一长串嵌套CASE。生产环境里,清晰比炫技重要。
4.3 在UPDATE里用CASE做批量条件更新
CASE表达式不仅能用于查询,也能用在UPDATE语句里,实现一次更新多条记录的不同状态。比如订单超过7天未支付,自动把状态改成“已取消”;已经支付的订单,状态改成“处理中”;其他情况保持不变:
UPDATE orders SET status = CASE WHEN status = 1 AND DATEDIFF(NOW(), created_at) > 7 THEN 5 WHEN status = 2 THEN 3 ELSE status END WHERE order_id IN (101, 102, 103, 104, 105);这个写法的价值在于,避免对同一张表执行多次UPDATE,减少事务次数和锁等待。如果不加ELSE status,那么不满足条件的记录会把status更新成NULL,这通常是灾难性的。所以这里ELSE status不是可选项,而是必须写的保护逻辑。
4.4 用CASE做去重统计
CASE和COUNT(DISTINCT ...)组合可以完成带条件的去重统计。比如统计“有有效手机号的用户数”,手机号非空且长度合理才算有效:
SELECT COUNT(DISTINCT CASE WHEN phone IS NOT NULL AND LENGTH(phone) >= 11 THEN user_id END) AS valid_user_cnt FROM customers;这个查询的逻辑是:满足条件时返回user_id,不满足时返回NULL;COUNT(DISTINCT ...)会先忽略NULL,再对剩余user_id去重。这是统计有效用户、活跃用户时很常见的写法。
使用这个组合时要注意:CASE返回的字段类型要和DISTINCT匹配,不能一会返回数字,一会返回字符串;如果user_id数据量很大,去重统计会消耗较多资源,建议先通过WHERE把无关数据过滤掉,再丢给COUNT(DISTINCT ...),不要全表硬扛。
5. 容易被坑的地方和排查思路
5.1 类型不一致导致报错或结果异常
CASE表达式的所有THEN分支,返回类型尽量保持一致。比如一个分支返回字符串,另一个分支返回数字,不同数据库有不同处理方式:
- 有些数据库会做隐式转换,把数字转成字符串或反过来,结果看起来能用,但可能丢失精度。
- 有些数据库直接报错,提示类型不一致。
最典型的错误是数字和字符串混用:
CASE WHEN status = 1 THEN '正常' WHEN status = 2 THEN 0 END这段代码在不同数据库里表现不一样,但都不推荐。正确做法是统一返回类型,比如都返回字符串,或者都返回数值。
排查这类问题,第一步不是盯着SQL看,而是先看完整报错信息。数据库通常会把出错的表达式和位置一起输出。把CASE每个分支的返回值列出来,对比目标字段类型,就能定位到是哪个分支出了问题。
5.2 逻辑顺序错误导致结果不对
这是CASE表达式最常见、最隐蔽的问题。SQL不会报错,但结果和预期不符。核心逻辑是:CASE从上到下匹配WHEN,遇到第一个为真的条件就返回,不再继续判断。
所以写范围条件时顺序必须统一。比如:
CASE WHEN score >= 60 THEN '及格' WHEN score >= 90 THEN '优秀' ELSE '不及格' END这个查询永远不会返回“优秀”,因为90分也满足score >= 60,在第一个WHEN就被截住了。排查这种问题,不要先怀疑数据库,先把WHEN条件列出来,按从大到小或从小到大排一遍,再用一条边界数据手动走一遍流程,很快就能发现。
我一般会拿三条边界数据测试:最小值、分界值、最大值。比如成绩表里取60分、90分、59分三条记录,分别检查映射结果是否符合预期。边界值能覆盖大部分WHEN顺序问题。
5.3 性能到底受不受影响
很多新手担心使用CASE会拖慢查询。实际上,在SELECT和ORDER BY里使用CASE表达式,通常不会造成严重性能问题,尤其是在数据量不大的报表场景。真正需要关注的是下面几种情况:
- 在
WHERE条件里用CASE包裹字段,比如CASE WHEN score >= 60 THEN 1 ELSE 0 END = 1,这类写法会让优化器难以使用字段上的索引。遇到这种情况,尽量改写成score >= 60。 - 行转列时使用
MAX(CASE WHEN ... END),如果表很大且没有合适索引,需要检查执行计划。 - 嵌套CASE层级过深,会增加SQL解析和优化成本,但通常不是查询瓶颈。更实际的问题是维护成本。
排查性能问题,正确顺序是:
- 先看单条查询耗时是否真的达到瓶颈。
- 再看执行计划,找到消耗最大的节点。
- 然后检查是索引、扫描行数、连接顺序,还是CASE表达式本身导致的问题。
- 最后根据优化器建议调整,而不是盲目删除CASE。
把问题归因到CASE之前,一定要先看执行计划。很多时候真正的瓶颈是全表扫描、缺失索引或JOIN条件写错,CASE只是背锅。
| 现象 | 常见原因 | 先检查什么 |
|---|---|---|
| 结果全部落在某一个分支 | WHEN顺序不对 | 用边界值数据手动走一遍 |
| 返回类型报错 | 分支类型不一致 | 每个THEN的返回值类型 |
| 查询速度慢 | 索引失效或全表扫描 | 执行计划 |
| 结果显示NULL或空白 | 没有ELSE或NULL判断顺序不对 | 检查输入数据和WHERE条件 |
6. 面试题、实战案例和CASE表达式的边界
6.1 成绩评级题:把WHEN顺序当作第一考点
面试里最常见的CASE题目是这样的:给一个成绩字段,要求显示“优秀、良好、及格、不及格”。参考答案:
SELECT student_name, score, CASE WHEN score >= 90 THEN '优秀' WHEN score >= 80 THEN '良好' WHEN score >= 60 THEN '及格' ELSE '不及格' END AS grade FROM student_score;这道题考察的其实不只是语法,而是WHEN顺序。如果先写>= 60,后面所有高分段都不会命中。面试官还会追问“不写ELSE会怎样”,答案是没有匹配时返回NULL,这在聚合统计里可能被静默忽略。
6.2 订单行转列:固定列用CASE,动态列换思路
另一个高频面试题是订单按月统计并转成列。假设订单表有订单日期和金额,要统计1到3月每个月的销售额,并输出成三列:
SELECT SUM(CASE WHEN MONTH(order_date) = 1 THEN amount ELSE 0 END) AS jan_sales, SUM(CASE WHEN MONTH(order_date) = 2 THEN amount ELSE 0 END) AS feb_sales, SUM(CASE WHEN MONTH(order_date) = 3 THEN amount ELSE 0 END) AS mar_sales FROM orders WHERE order_date >= '2025-01-01' AND order_date < '2025-04-01';这个写法在面试里很标准,也适合固定月份报表。但生产环境里如果月份是动态变化的,我不建议硬编码列名。因为每来一个月就要改一次SQL,维护成本高。更稳妥的方案是先用普通GROUP BY month查询,由报表工具或程序侧完成列转换;或者使用数据库自带的透视功能。
这个例子很好地说明了一个边界:CASE表达式适合写“列结构固定”的转换,不适合写“列结构会变化”的场景。
6.3 实际项目里CASE表达式的定位
CASE表达式在真实项目里的定位,可以概括成三类:
- 数据翻译:把数据库里的状态码、类型值映射成可读文字。
- 条件计算:在查询内部完成评级、分类、归属、条件统计。
- 结构转换:配合聚合函数做行转列、条件去重、透视统计。
它不适合承担的工作也需要想清楚:非常复杂的业务规则、需要循环和游标的操作、需要访问外部系统或接口的逻辑、需要长期维护的复杂分支判断。这些应当放在程序代码、存储过程或专业报表工具里,而不是全部压进一条SQL。
我见过不少同事把一大段嵌套CASE当成万能方案,最后SQL写得又长又难读。其实很多分支逻辑拆成子查询和CTE后,反而更清晰。真正能写高质量SQL的人,不是会更多关键字,而是知道什么逻辑该放在SQL层,什么逻辑不该放。
这条经验也适合所有刚接触CASE表达式的读者:先在小查询里把语法跑通,再逐步尝试WHERE、ORDER BY、GROUP BY、行转列和聚合统计。单条任务跑稳之后,再处理批量报表和复杂业务,思路会清晰很多。