
1. 先搞懂 CASE 到底是干什么的接触 MySQL 的朋友基本都会遇到 CASE但很多人只是把它当成一个“多分支判断”的语法背下来了实际用起来却总差那么点意思。其实 CASE 在 SQL 里的定位非常特别它不是一个流程控制语句而是一个表达式。也就是说它最终会产生一个返回值可以在 SELECT、UPDATE、ORDER BY、GROUP BY 甚至 HAVING 里当普通字段一样使用。这是它和编程语言里 switch…case 最大的区别也是很多新手写 SQL 时想不通“为什么这样也行”的根源。CASE 有两种写法。一种是简单 CASE用一个字段或表达式跟在 CASE 后面然后逐个 WHEN 比较等值。另一种是搜索 CASEWHEN 后面直接跟布尔条件灵活度更高几乎能覆盖所有判断需求。搜索引擎里关于“case when用法”的热搜居高不下说明这个知识点在开发场景中确实高频。但高频不等于被真正吃透许多人会在 NULL、数据类型、索引这些地方翻车所以我建议先从基础语法和设计意图开始把 CASE 的“底子”打牢。1.1 简单 CASE 和搜索 CASE怎么选简单 CASE 的语法长这样CASE 字段名 WHEN 值1 THEN 结果1 WHEN 值2 THEN 结果2 ELSE 默认结果 END它会拿字段名去和每个 WHEN 后面的值做等值比较。注意这个比较用的是语义遇到 NULL 时结果是“未知”不会匹配任何 WHEN也不会落到 ELSE这个后面专门讲。搜索 CASE 的语法则直接写条件CASE WHEN 条件表达式1 THEN 结果1 WHEN 条件表达式2 THEN 结果2 ELSE 默认结果 END搜索 CASE 的范围更广可以写大于、小于、LIKE、IN、BETWEEN以及多个条件用 AND/OR 组合。如果你的业务只是判断某个字段等于几个固定值用简单 CASE 会更精简一旦涉及范围判断、多个字段联动判断请优先选择搜索 CASE。我个人的习惯是任何时候都倾向于写搜索 CASE因为后期改条件时不用大范围调整结构只需改 WHEN 后面的布尔表达式即可。简单 CASE 看着省字符但维护起来可能让你多花几倍时间。1.2 为什么 SQL 里要有 CASE它帮你解决了什么问题SQL 是一门面向集合的语言我们写一条 SELECT本质上是在描述“对某张表的每一行施加怎样的函数变换”而不是像 Java、Python 那样逐行循环。CASE 的价值就在这里它让你能在单条 SQL 内完成逐行的条件映射把原本需要“先查出来再在程序里 if…else 加工”的逻辑下沉到数据库层。举个例子订单表里有一个金额字段你想在页面展示时附带一个“订单级别”1 万以上叫“大单”5000 到 1 万叫“中单”其余叫“小单”。如果不会 CASE你的第一反应可能是查出来后在 Java 里 for 循环判断。这当然没问题但如果你要对这些级别做 GROUP BY 聚合统计那程序里就无法高效完成必须先在 SQL 里转成新的字段再按新字段分组。CASE 正是做这种“数据清洗和语义映射”的标准工具。除此之外CASE 还能解决“条件聚合”——比如同时统计满足 A 条件的行数和满足 B 条件的行数且两种条件有交集。这种需求用常规WHERE做不了因为 WHERE 只能筛一次但用SUM(CASE WHEN ... THEN 1 ELSE 0 END)就能在扫同一张表时完成多个口径的独立统计。可以说CASE 是 SQL 从“查询语言”迈向“数据处理语言”的关键拼图。2. CASE 在 SELECT 实战中的几种高阶用法很多教程只讲 CASE 最基本的“分类打标”比如把性别 0 转成“男”1 转成“女”。这当然没错但 CASE 的实战价值远不止这些。我平时用得最多的场景是分类标签、条件聚合、行列转换、优先级判断。下面逐个展开并配上可以直接复用的 SQL。2.1 给查询结果打分类标签这是最基础也最常见的用法。假设有一张用户表user字段create_time记录了注册时间你希望在结果里增加一列“注册年限”要求按年份跨度分类SELECT user_name, create_time, CASE WHEN create_time 2020-01-01 THEN 老用户 WHEN create_time 2023-01-01 THEN 中期用户 ELSE 新用户 END AS user_segment FROM user ORDER BY create_time DESC;这里要特别提醒WHEN 的判断顺序是从上到下只要命中第一个 TRUE 就会返回对应的 THEN后面条件不再判断。所以写条件时要把范围小的、优先级高的放前面。比如你想区分 90 天内活跃和 180 天内活跃要把“90 天内”的 CASE 放在“180 天内”前面否则 90 天内的用户会先被“180 天内”捕获最终标签全部变成“180 天内活跃”逻辑就全错了。这种“拦截顺序”问题是新手最容易踩的坑。如果要给 SQL 语句里多个字段都打标签可以写多个 CASE 表达式每个独立 END 即可。注意别名别重复尤其是复制粘贴时很多人把两个 CASE 的别名都写成type查出来只看到最后一列还以为是数据库的问题实际上是你自己命名撞车了。2.2 CASE 加聚合函数的组合拳这一招是我的日常工作利器。最常见的需求是统计某一天不同支付渠道的订单数和金额。如果只用WHERE pay_channel alipay再扫一次表会很笨重而且每多一个渠道就多一次全表扫描。用 CASE 配合聚合函数可以一次搞定SELECT order_date, COUNT(CASE WHEN pay_channel alipay THEN 1 END) AS alipay_cnt, COUNT(CASE WHEN pay_channel wechat THEN 1 END) AS wechat_cnt, SUM(CASE WHEN pay_channel alipay THEN order_amount ELSE 0 END) AS alipay_amount, SUM(CASE WHEN pay_channel wechat THEN order_amount ELSE 0 END) AS wechat_amount FROM orders GROUP BY order_date;注意 COUNT 里面有两种写法COUNT(CASE WHEN 条件 THEN 1 END)和SUM(CASE WHEN 条件 THEN 1 ELSE 0 END)。它们的区别在于COUNT 只计数非 NULL 的值所以当条件不成立时THEN 后面的 1 没有被返回而是返回 NULLNULL 不会被 COUNT 计入。这样写比 SUM 更简洁。但要注意如果后面的统计值是金额请使用SUM(CASE WHEN 条件 THEN 金额 ELSE 0 END)因为 SUM 会忽略 NULL但你想要的结果是“不满足条件的金额按 0 处理”这样合并汇总时不会把 NULL 带进去形成算术运算问题。这种写法本质上是在一次扫描中完成了多个条件分支的独立统计数据量大时性能提升非常明显。我在处理千万级订单表时经常用这种方式替代多个临时表和多次 JOIN查询时间能从分钟级降到秒级。2.3 CASE 做行列转换的正反案例行列转换是数据分析里非常典型的需求。假设有一张sales表每个城市有多条销售记录你想把不同产品类型转成列显示SELECT city, SUM(CASE WHEN product_type 手机 THEN sales_amount ELSE 0 END) AS phone_sales, SUM(CASE WHEN product_type 电脑 THEN sales_amount ELSE 0 END) AS computer_sales, SUM(CASE WHEN product_type 平板 THEN sales_amount ELSE 0 END) AS pad_sales FROM sales GROUP BY city;这就是“行转列”的标准姿势。去掉 GROUP BY 后它还能变成一条总计行转置后与多表 JOIN 结果做对比。反过来如果你已经有一张行格式的表想变成列格式通常用 UNION ALL 配合 CASE 实现。这两种操作在报表系统里非常高频理解了 CASE 和聚合的组合思路后你就不需要依赖复杂存储过程了。2.4 CASE 嵌套与优先级判断有时候一个分类依赖另一个分类的结果你可能会想到嵌套 CASE。嵌套没问题但不要写得像意大利面条一样层层套。比如SELECT user_name, CASE WHEN age 60 THEN 老年 WHEN age 30 THEN 中年 WHEN age 18 THEN 青年 ELSE 未成年 END AS stage, CASE WHEN stage 老年 AND grade VIP THEN 重点关怀用户 ELSE 普通用户 END AS remark FROM user;上面这种写法有个隐患SELECT 子句里的别名stage在同级 SELECT 中在 MySQL 某些版本里是可以被后面的表达式引用的吗答案是不保证SQL 的语义不允许在同一 SELECT 层中引用别名。所以你不能在第二个 CASE 里直接使用stage这个别名除非外面再套一层查询。正确的做法是SELECT user_name, stage, CASE WHEN stage 老年 AND grade VIP THEN 重点关怀用户 ELSE 普通用户 END AS remark FROM ( SELECT user_name, grade, CASE WHEN age 60 THEN 老年 WHEN age 30 THEN 中年 WHEN age 18 THEN 青年 ELSE 未成年 END AS stage FROM user ) t;利用派生表把同层的别名问题绕开。这种写法虽然多套一层但逻辑非常清晰特别适合后续还要继续基于第一次分类结果做二次判断的场景。永远记住CASE 是个表达式它的结果只在 SQL 执行完成后才成为一列你无法在同一批处理中对这一列做再次判断。3. CASE 在排序、更新、分组中的冷门但实用的姿势大多数文章只教 SELECT 里的 CASE忽略了一个很重要的点CASE 可以出现在 SQL 的几乎所有子句里。这不是炫技而是有一些真实场景只有用 CASE 才能优雅解决。3.1 用 CASE 自定义排序规则想按业务优先级排序时ORDER BY只能按字段本身的数值或字典序这时 CASE 就派上用场了。比如订单状态有pending、paid、shipped、cancelled你想让正在处理的把顺序放在前面取消的永远排最后SELECT order_id, order_status FROM orders ORDER BY CASE order_status WHEN shipped THEN 1 WHEN paid THEN 2 WHEN pending THEN 3 WHEN cancelled THEN 4 END;这里使用了简单 CASE在 ORDER BY 子句里为每个状态映射出一个数字MySQL 就会按这个数字排序。还可以在此基础上加上第二排序字段比如ORDER BY CASE ... END, create_time DESC实现“状态优先时间倒序”的组合排序。这种方式的优点是排序逻辑完全收在 SQL 里不需要在应用层重排缺点是如果你的状态枚举非常多维护起来略微繁琐但比起在程序里做“字段映射表 内存排序”性能通常更好。3.2 UPDATE 中用 CASE 做批量条件更新最常见的批量更新需求是“根据旧值计算新值”。比如商品表有个price不同品类涨价幅度不同你可以写多条 UPDATEUPDATE products SET price price * 1.1 WHERE category 饮料; UPDATE products SET price price * 1.08 WHERE category 零食;这样写没问题但如果需要在一个事务里保证一致性或者不想产生多轮写入日志就可以用 CASE 合并成一条UPDATE products SET price CASE WHEN category 饮料 THEN price * 1.1 WHEN category 零食 THEN price * 1.08 ELSE price END;注意 ELSE 分支一定要保留或者至少保证每条记录都能有明确的赋值路径。因为 UPDATE 是对整表逐行扫描的如果没有 ELSE某些行会在执行完后价格变成 NULL——这是灾难级的低级事故我见过不止一次。另外使用 CASE 做 UPDATE 时不需要再加 WHERE 限定全表更新的条件因为 CASE 本身已经按行做了映射。但如果你只想更新部分行可以在 UPDATE 语句尾部继续加 WHERE比如WHERE category IN (饮料,零食)此时 CASE 中的 ELSE 可以省略因为不满足 WHERE 的行根本不会被更新。3.3 CASE 写在 GROUP BY 里的限制与替代方案MySQL 允许在 GROUP BY 里也写 CASE比如把订单分成不同金额段后分别统计SELECT CASE WHEN order_amount 100 THEN 小额 WHEN order_amount 1000 THEN 中额 ELSE 大额 END AS amount_level, COUNT(*) AS order_cnt FROM orders GROUP BY CASE WHEN order_amount 100 THEN 小额 WHEN order_amount 1000 THEN 中额 ELSE 大额 END;这种写法的可读性比在 SELECT 中定义别名后直接GROUP BY amount_level要差但它在某些 MySQL 版本中更安全因为直接使用别名分组其实 SQL 标准并不保证支持。不过说实话我建议把 CASE 表达式用别名写在 SELECT 里然后 GROUP BY 别名很多版本也能跑不过万一碰上严格模式可能会报错。最稳妥的写法是把整段 CASE 表达式复制到 GROUP BY 后面虽然看起来冗长但兼容性最好。另外在 HAVING 中也可以使用 CASE 条件下的聚合结果比如筛选出“至少 3 笔大额订单”的客户SELECT customer_id FROM orders GROUP BY customer_id HAVING SUM(CASE WHEN order_amount 1000 THEN 1 ELSE 0 END) 3;这比先查再分段统计要简洁得多。3.4 CASE 与其他函数配合的注意事项CASE 写入 SELECT 列表时与字符串函数、数学函数、日期函数的配合特别常见。比如你想拼接“用户名 等级前缀”SELECT CONCAT( CASE WHEN is_vip 1 THEN VIP- ELSE END, user_name ) AS display_name FROM user;这种写法要注意CASE 返回的数据类型必须稳定。比如第一个分支返回字符串第二个分支返回数字MySQL 会尝试做隐式类型转换可能导致索引失效或者结果不符合预期。在拼接场景建议把每个分支都写成字符型必要时用 CAST 显式转换。日期方面很多新手把 CASE 和 DATE_FORMAT 混用结果在 WHEN 里写DATE_FORMAT(create_time, %Y-%m-%d) 2024-01-01虽然不算错但会影响这一列上函数索引的使用。如果能用create_time 2024-01-01 AND create_time 2024-01-02代替就尽量不要包一层 DATE_FORMAT。4. 常见问题排查与避坑实录CASE 写起来似乎很简单但实际跑 SQL 时我在聊天群里看到最多的错误几乎都集中在几个点上。这里把高频问题整理成速查表并说说我排查这些问题的思路。4.1 NULL 陷阱所有条件都不满足时到底返回什么先看一个例子SELECT CASE WHEN score 60 THEN 及格 END AS result FROM exam;这条 SQL 在执行时如果 score 为 55 分CASE 里的条件不成立且没有 ELSE 分支那么返回的是NULL。很多人以为会返回空字符串或者跳过该行但事实上查出来的 result 列就是 NULL类型上它是 NULL 值。如果你要把它插入到目标表或做非空校验就可能报错。同理简单 CASE 比较相等也无法处理 NULLCASE name WHEN NULL THEN 未知 ELSE name END当 name 为 NULL 时这个表达式不会走到 THEN因为NULL NULL的结果不是 TRUE而是 UNKNOWN。如果想把 NULL 显示为“未知”必须写成搜索 CASEWHEN name IS NULL THEN 未知 ELSE name END。4.2 CASE 真的能走索引吗聊聊性能影响很多人问在 WHERE 条件里写CASE WHEN ... THEN ...能不能让查询走索引这里要分情况。如果在 WHERE 中使用搜索 CASE 包裹一个字段比如WHERE CASE WHEN status 1 THEN create_time ELSE 1900-01-01 END 2024-01-01MySQL 很难对这个表达式使用索引因为条件被包在了一个函数式的 CASE 里优化器无法转换成简单的范围比较。实际生产环境中我极少在 WHERE 里用 CASE因为大部分情况下都能用 OR、IN、范围条件等更高效的方式代替。比如“status 为 1 且时间大于 2024 年或者 status 不为 1 且时间大于 2023 年”可以写成WHERE (status 1 AND create_time 2024-01-01) OR (status 1 AND create_time 2023-01-01)这种写法更容易被优化器转换为索引范围扫描。所以 CASE 尽量用在 SELECT、ORDER BY、GROUP BY 等输出转换层而不是把它当索引条件过滤器。如果确实无法避免你至少要明白可能全表扫描并做好数据量评估。4.3 简单 CASE 的判断覆盖不全表达式类型必须一致简单 CASE 的 WHEN 后面只支持等值判断所以当你需要“字段 1 等于值 A 且字段 2 等于值 B”时无法用简单 CASE 合并必须转成搜索 CASE。搜索 CASE 的每个 WHEN 都是一个完整的布尔表达式能够自由组合。判断类型不一致也是常见问题比如CASE order_amount WHEN 100 THEN 满减 ELSE 未满减 ENDorders.amount 数值类型 100你用字符串 100 去比较MySQL 会做隐式转换但若涉及索引或排序可能产生意外结果。最好的习惯是让 WHEN 后面的值与字段类型严格一致不要依赖隐式转换。4.4 求值顺序与短路行为为什么说 CASE 比 IF 更安全CASE 求值时会按 WHEN 顺序依次判断一旦命中一个条件为 TRUE 就不再继续往下的判断这一特性叫“短路”。所以你可以放心地把最后兜底条件放到最后一个 WHEN 或 ELSE 中。这与 MySQL 的IF()函数行为类似但 IF 只能处理单个条件CASE 的可读性和扩展性更强。除此之外CASE 在语句中能替换IFNULL实现更复杂的分支空值处理比如SELECT CASE WHEN remark IS NULL THEN 暂无备注 WHEN CHAR_LENGTH(remark) 100 THEN LEFT(remark, 100) ELSE remark END AS remark_info FROM user;这个例子结合了 NULL、长度判断和截断展示的正是搜索 CASE 应对多分支问题时的优势。如果你在一个查询里用多个 IF 嵌套代码会很乱改成 CASE 后一眼看完所有分支后续维护也会轻松很多。我把常见问题整理成一张表方便排查现象原因解决办法查询结果出现 NULL 而非期望的默认值没写 ELSE所有 WHEN 都不成立补全 ELSE 分支或使用COALESCE(CASE ... END, 默认值)简单 CASE 处理 NULL 无效NULL 与任何值相等比较都为 UNKNOWN改用搜索 CASE用IS NULL判断WHERE 子句里写了 CASESQL 变得极慢CASE 阻碍了索引条件转换改写为等价的 OR/AND 条件组合ORDER BY 排序结果和预期相反简单 CASE 的映射值和排序规则不对应检查映射值大小方向或加 DESC/ASC更新语句把整列数据置为 NULLUPDATE 中 CASE 没有 ELSE强制补 ELSE 为原字段值或兜底值报错Unknown column在 SELECT 中引用同层别名内嵌派生表将别名投影到外层这些坑我基本都踩过尤其是 NULL 陷阱。你做数据清洗时会发现CASE 的 ELSE 分支如果省略返回的 NULL 常常在 JOIN 和可视化报表里引发连锁问题。所以我现在有个习惯凡是 CASE 打算用在最终展示或结果落库的场合永远写 ELSE 分支凡是只是临时中间计算才可能省略 ELSE 并依赖 NULL 的特性。这个技巧看似不起眼但在线上维护时能帮你省去很多“为什么这里显示空白”的排查时间。CASE 的用法远不止几个语法片段它是把“行级逻辑判断”融入 SQL 集合化操作的连接器。掌握好它你写出来的 SQL 会更简洁也更接近一个优秀数据工程师的水平。