前几天接到一个数据汇总的需求,业务方要一份订单金额分布报表:0到100元、100到500元、500到1000元、1000元以上各有多少订单。按我最初的想法,这不得拆成四条带WHERE条件的SQL挨个跑,然后再手工拼到一张表里。后来同事看了一眼,直接在一条SQL里用CASE WHEN把区间字段算出来,配合GROUP BY一次搞定。那一刻我才意识到,MySQL的数据汇总能力远不止GROUP BY和SUM那么简单,CASE WHEN在汇总场景里就是一把瑞士军刀。
这篇文章把我在实际练习和项目里用CASE WHEN做数据汇总的经验完整梳理了一遍,包括基础语法、区间统计、行转列透视、条件聚合、去重统计这些经典玩法,还有我踩过的几个坑和排查思路。不管你是刚学SQL的初学者,还是已经写了几年SQL但没用过CASE WHEN做复杂报表的人,都应该能从里面找到可以直接抄作业的写法。
1. 一次性跑N条SQL的日子:为什么要学CASE WHEN做汇总
1.1 一个被拆成五条的查询需求
先还原一下我当时遇到的场景。有一张订单表,结构大概是这样的:
CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, amount DECIMAL(10,2) NOT NULL, channel VARCHAR(20) NOT NULL, status TINYINT NOT NULL, created_at DATETIME NOT NULL );业务要统计四个金额区间的订单量。我最初的做法很耿直:
SELECT COUNT(*) FROM orders WHERE amount < 100; SELECT COUNT(*) FROM orders WHERE amount BETWEEN 100 AND 500; SELECT COUNT(*) FROM orders WHERE amount BETWEEN 500 AND 1000; SELECT COUNT(*) FROM orders WHERE amount > 1000;四条SQL跑完,把结果手工填到一张Excel里发给业务。这样做的问题很明显:首先,订单表扫描了四遍,数据量大一点就非常慢;其次,手工拼接结果,漏一条、错一条都很难查;最后,如果业务临时说"金额区间再细分成五档",我又得改六条SQL。
后来用一条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 price_range, COUNT(*) AS order_cnt FROM orders GROUP BY price_range;表只扫描了一遍,分组结果直接按区间汇总好,业务改需求我也只改CASE WHEN里的条件就行。这就是CASE WHEN做数据汇总最核心的价值:它让你能在"行"的粒度上先把数据分类,然后再交给GROUP BY做"组"的粒度聚合。
1.2 CASE WHEN的本质:先分类再汇总,而不是先过滤再拼接
很多人刚接触CASE WHEN时,只把它当成SELECT里给输出字段做分支判断的小功能,比如把状态数字1、2、3转换成"待支付""已支付""已取消"这种显示层面的处理。这当然没错,但太浪费了。
CASE WHEN真正厉害的地方在于:它是一个表达式,可以出现在SELECT、WHERE、ORDER BY、GROUP BY、HAVING的任意位置。当它出现在SELECT配合聚合函数使用时,它的作用从"给值做翻译"升级成了"给行打标签"。
打个比方。GROUP BY只能按照表里已有的列分组,就像把一箱混合水果按"种类"挑拣——苹果归苹果、香蕉归香蕉,前提是每个水果身上贴着种类标签。可现实里,你需要按"甜度""价格区间""是否当天到货"这种表里没有直接体现的维度分组,那就先得由CASE WHEN给每个水果临时贴一张标签,然后再按标签归类。
这种"先打标签再聚合"的机制,是所有复杂报表的基础。也是因为这样,CASE WHEN与聚合函数的配合才成了MySQL数据汇总练习里绕不开的核心技能。
2. 语法不是背的:简单CASE与搜索CASE的取舍逻辑
2.1 两种写法,一个用等值匹配,一个用条件判断
MySQL里的CASE表达式有两种形式。第一种是简单CASE,适合做等值匹配:
CASE channel WHEN 'app' THEN 'App端' WHEN 'web' THEN '网页端' WHEN 'mini' THEN '小程序' ELSE '其他' END它的执行逻辑是拿CASE后面的字段值,挨个和WHEN后面的值比相等。简单、易读,但只能做等值判断,大于小于、区间判断、模糊匹配做不了。
第二种是搜索CASE,CASE后面不跟字段,直接在每个WHEN后面写完整的条件表达式:
CASE WHEN amount < 100 THEN '低金额' WHEN amount < 500 THEN '中金额' ELSE '高金额' END这种写法能处理任意复杂的条件,逻辑判断完全开放。在实际数据汇总场景里,搜索CASE的使用频率远高于简单CASE,因为"分组"的需求绝大多数不是等值匹配,而是区间、多条件组合、甚至跨字段判断。
有人会问:两种写法能不能混用?能,但没必要。实际项目中我更推荐统一用搜索CASE。原因很实在:第一,搜索CASE覆盖了简单CASE的全部能力,等值匹配也能写成WHEN channel = 'app';第二,团队协作时,统一一种写法,别人看代码不用切换思维模式;第三,后续需求从等值判断改成区间判断时,简单CASE很可能要改结构,搜索CASE只需改条件表达式。
2.2 ELSE和END:看起来不起眼,漏了就是事故
CASE WHEN的语法里有两个词最容易出错,一个是ELSE,一个是END。
ELSE不是必填项,但实际写汇总SQL时我强烈建议显式加上。原因在于:当所有WHEN条件都不满足时,如果没有ELSE,整个CASE表达式的值就是NULL。很多时候NULL不会报错,而是静默地混进你的统计结果里。
举个例子,你按渠道分组统计:
SELECT channel, COUNT(*) AS total, SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) AS paid_cnt FROM orders GROUP BY channel;如果这里不写ELSE 0而写成:
SUM(CASE WHEN status = 1 THEN 1 END) AS paid_cnt那么该渠道下没有已支付订单时,SUM的结果不是0,而是NULL。前端拿到NULL显示成空白,业务就会问"为什么这里没数字"。
再说END。END是CASE表达式的结束标志,漏写会直接报语法错误,"the right syntax to use near..."是MySQL最常见的报错之一。而且嵌套CASE时,到底写了几个CASE就要写几个END,很容易数错。我的习惯是从最内层开始补END,写完一遍再数一遍。
除了语法,还有一个逻辑细节:CASE WHEN是从上到下按顺序匹配的,命中第一个满足条件的WHEN就返回,后面的不再判断。这意味着WHEN条件的顺序会直接影响结果。比如:
CASE WHEN amount < 100 THEN 'low' WHEN amount < 500 THEN 'mid' ELSE 'high' END和
CASE WHEN amount < 500 THEN 'mid' WHEN amount < 100 THEN 'low' ELSE 'high' END结果是完全不同的。第一个写法里100到499正确归入'mid',第二个写法里所有小于500的订单全被归入'mid','low'这个分支永远不会生效。所以写区间条件时,顺序要么从小到大、要么从大到小,必须有节奏感,不能乱排。
3. 金额区间统计:CASE WHEN最典型的汇总场景
3.1 先把需求翻译成"每行数据打什么标签"
区间统计是练习CASE WHEN数据汇总最好的入门案例。需求再复杂一点:不只是四个区间,而是五个区间,并且每个区间还要分别统计"订单总数"和"成功支付的订单数"。
拿到这种需求,第一件事不是急着写SQL,而是在纸上把标签定义写清楚。我这边的规则是这样:
- 0到100元(含100):区间A
- 100到500元(含500):区间B
- 500到1000元(含1000):区间C
- 1000到5000元(含5000):区间D
- 5000元以上:区间E
注意我刻意用了"含边界"的描述。边界归属是区间统计最容易出错的地方,写SQL前必须和业务确认清楚"等于100算哪个区间"。确认完之后,用搜索CASE表达:
CASE WHEN amount <= 100 THEN '区间A' WHEN amount <= 500 THEN '区间B' WHEN amount <= 1000 THEN '区间C' WHEN amount <= 5000 THEN '区间D' ELSE '区间E' END这段逻辑的关键是:因为CASE从上往下匹配,所以只要amount <= 500能命中,就说明它已经大于100了,不需要在第二个条件里再写amount > 100 AND amount <= 500这种冗余判断。这个写法不仅简洁,还能避免两个区间条件重叠时数据被重复计数的问题。
3.2 一条SQL完成多维统计:区间维度和其他维度交叉
有了标签之后,剩下的就是和GROUP BY配合。如果只需要按区间统计订单数,上一章那个例子就够了。但真实报表通常不会这么简单,业务往往会要求"每个区间里,App端来了多少订单、网页端来了多少订单"。
这种"区间 + 渠道"的交叉统计,本质上是多维度分组:
SELECT CASE WHEN amount <= 100 THEN '区间A' WHEN amount <= 500 THEN '区间B' WHEN amount <= 1000 THEN '区间C' WHEN amount <= 5000 THEN '区间D' ELSE '区间E' END AS price_range, channel, COUNT(*) AS order_cnt FROM orders GROUP BY price_range, channel ORDER BY price_range, channel;GROUP BY后面可以直接写SELECT里的别名,MySQL是支持这种写法的,执行效果和写完整的CASE表达式完全一样。但有一个细节要注意:如果你的SQL里既有GROUP BY 1、2这种位置写法,又有别名写法,混在一起会让查询很难维护,我建议统一用别名。
看到这里你可能会发现,CASE WHEN写出的区间分组,本质上是给每条订单打了一个"区间"标签,然后GROUP BY把这个标签当成一个普通的维度列来分组。理解了这一点,任何复杂分类需求(RFM模型分层、价格带划分、库存周转区间)就都能套用同一套思路了。
3.3 用EXPLAIN验证:区间统计真的只扫一遍表
做数据汇总练习时,我养成了一个习惯:写完SQL先不急着看结果,而是用EXPLAIN看一眼执行计划。
EXPLAIN SELECT CASE WHEN amount <= 100 THEN '区间A' WHEN amount <= 500 THEN '区间B' WHEN amount <= 1000 THEN '区间C' WHEN amount <= 5000 THEN '区间D' ELSE '区间E' END AS price_range, COUNT(*) FROM orders GROUP BY price_range;你会在EXPLAIN结果里看到,type通常是ALL或者range,rows显示扫描的行数接近全表,但重点是这份SQL只扫描一次表数据。相比四条WHERE查询各扫一遍,性能优势是实打实的。数据量在百万级时,这种差异可能就是几十毫秒和两百毫秒的差别,数据量上了千万级,差别会扩大到几秒甚至几十秒。
当然,如果区间统计的表数据量极大、查询频率很高,还有两个优化方向:一是对amount字段建索引,配合覆盖索引减少回表;二是如果区间边界固定不变,可以考虑生成一个"区间映射表",用JOIN代替CASE WHEN,把计算逻辑从查询时搬到数据准备时。不过对于大多数中小项目的报表需求,CASE WHEN的写法已经足够高效了。
4. 行转列透视汇总:把渠道从行变成列
4.1 一个让人头疼的"宽表"需求
数据汇总里最常见的第二类需求,就是行转列,业内也叫透视表。业务方的原话往往是:"我要一张表,每一行是一个月份,每一列是一个渠道,交叉点是订单数。"
直接GROUP BY month, channel写出来的结果是长表:
SELECT DATE_FORMAT(created_at, '%Y-%m') AS month, channel, COUNT(*) AS order_cnt FROM orders GROUP BY DATE_FORMAT(created_at, '%Y-%m'), channel;结果是三列:月份、渠道、订单数。数据本身没毛病,但业务方填Excel时得手动把同一月份的各渠道数据拆到一行里,麻烦不说,手工操作还容易错。他们真正想要的是这样的宽表:
| 月份 | App端 | 网页端 | 小程序 |
|---|---|---|---|
| 2025-01 | 1200 | 800 | 300 |
| 2025-02 | 980 | 750 | 260 |
这种需求,恰好用CASE WHEN配合聚合函数实现。
4.2 SUM(CASE WHEN ... THEN 1 ELSE 0 END) 的固定套路
行转列的核心思路是:把每个需要变成列的值,转成一个独立的条件计数表达式。
以"App端订单数"为例,我们希望它在每一行里只统计channel='app'的订单。那么可以这样写:
SUM(CASE WHEN channel = 'app' THEN 1 ELSE 0 END) AS app_cnt这里SUM的机制很巧妙:CASE WHEN对每一行先做判断,符合条件返回1,不符合返回0;SUM再把这一组里所有的1和0加起来。一个订单如果属于App渠道,它的1就只进入"App端"这一列,不会跑到其他列去。这样就把一个维度拆散了,塞进了不同的聚合列。
完整SQL如下:
SELECT DATE_FORMAT(created_at, '%Y-%m') AS month, SUM(CASE WHEN channel = 'app' THEN 1 ELSE 0 END) AS app_cnt, SUM(CASE WHEN channel = 'web' THEN 1 ELSE 0 END) AS web_cnt, SUM(CASE WHEN channel = 'mini' THEN 1 ELSE 0 END) AS mini_cnt FROM orders GROUP BY DATE_FORMAT(created_at, '%Y-%m') ORDER BY month;我习惯把THEN 1 ELSE 0 END写成代码块里的固定模板,用的时候只改条件判断部分。这个套路可以推广到任意枚举值:状态转列、地区转列、支付方式转列,都是一个道理。
除了SUM,COUNT搭配CASE WHEN也是一种常见的行转列写法:
COUNT(CASE WHEN channel = 'app' THEN 1 END) AS app_cntCOUNT只统计非NULL值,所以当条件不满足时返回NULL不会被计数。效果和SUM(CASE WHEN ... THEN 1 ELSE 0 END)一样。但要注意,如果你用COUNT的写法又想让不满足条件的行不参与计数,就不能写ELSE 0,因为0不是NULL,会被COUNT进去。
4.3 行转列时数据为0和没有数据的区别
行转列还有一个容易混淆的业务问题:某个月某个渠道的订单数是0,和那个月压根没有该渠道的数据,展示上往往不一样。
比如2025年1月小程序还没上线,那么1月的数据里根本没有channel='mini'的订单。执行上面的SQL时,mini_cnt这一列SUM的结果是0,看起来和小程序上线但没订单是一样的。
如果你需要严格区分"渠道不存在"和"渠道存在但订单为0"两种情况,单靠对订单表做聚合是区分不出来的,因为订单表里没有订单,自然也没有渠道的维度信息。正确的做法是引入一张独立的渠道维度表,或者用渠道表的全量数据做外连接,再关联聚合结果。这个细节在练习阶段不容易想到,但在真实的报表项目里,业务方对"0"和"无数据"的歧义往往非常敏感,提前确认清楚能避免后续返工。
另外,如果行转列的目标列很多(比如十几个渠道),SQL会变得很长,看起来很不美观。MySQL 8.0支持用GROUP_CONCAT动态拼接生成列名,但那属于另一个复杂度等级的技术,日常练习阶段先把固定列的写法练扎实就够了。
5. 条件聚合的进阶组合:比例、排名、去重统计
5.1 算转化率:COUNT与CASE WHEN的除法陷阱
行转列是"把不同条件变成不同列",条件聚合则是"在同一个组内对不同条件的行做差异化的统计"。最典型的场景是算转化率。
比如要统计每个渠道的支付转化率:已支付订单数除以总订单数。
SELECT channel, COUNT(*) AS total_cnt, COUNT(CASE WHEN status = 1 THEN 1 END) AS paid_cnt, COUNT(CASE WHEN status = 1 THEN 1 END) / COUNT(*) AS paid_rate FROM orders GROUP BY channel;paid_rate算出来的结果,在没有额外处理时是十进制小数,比如0.2365。但业务报表通常需要百分比,我一般会再套一层ROUND:
ROUND(COUNT(CASE WHEN status = 1 THEN 1 END) / COUNT(*) * 100, 2) AS paid_rate_pct这里有个我一直提醒自己的坑:MySQL里两个整数相除得到的结果是十进制数,不会自动截断成整数,所以转化率不会变成0。但如果把分母写成常量,比如COUNT(...) / 100,要小心除法的精度问题;更常见的坑反而是分母为0,当某个渠道总订单数为0时,整个除法表达式结果是NULL,不报错但结果看起来像缺失。
还有一点需要单独说明:COUNT(CASE WHEN ... THEN 1 END)这个写法,THEN后面不写ELSE,条件不满足时返回NULL,COUNT会忽略NULL。如果你写成SUM(CASE WHEN ... THEN 1 ELSE 0 END),效果一样。两种都可以,但要统一,不要同一个脚本里一个用SUM一个用COUNT,维护起来容易看花眼。
5.2 AVG的另一种打开方式:不是平均金额,而是平均行为
AVG和CASE WHEN的组合,很多人第一反应是"算满足条件的平均值",比如算每个渠道已支付订单的平均金额:
SELECT channel, AVG(CASE WHEN status = 1 THEN amount END) AS avg_paid_amount FROM orders GROUP BY channel;这个写法的妙处在于,未支付订单的amount在CASE WHEN里返回NULL,AVG会忽略NULL,所以算出来的就是已支付订单的平均金额,和子查询加WHERE的效果一样,但只扫描一遍表。
AVG加CASE WHEN还有一种更灵活的玩法:算"满足条件的行数占比"的另一种表达。假设要统计每笔订单金额超过该渠道平均值的比例,先算出各渠道的平均金额,再回到行级别做比较。用窗口函数可以写:
SELECT channel, AVG(CASE WHEN amount > avg_amount THEN 1.0 ELSE 0 END) AS above_avg_ratio FROM ( SELECT channel, amount, AVG(amount) OVER (PARTITION BY channel) AS avg_amount FROM orders ) t GROUP BY channel;这里面AVG的作用不再是对原始金额求平均,而是对"是否高于平均值"这个0/1标签求平均,结果就是比例。理解这个思路之后,AVG和CASE WHEN的组合可以延伸出很多"行为比例"统计,比如"下单后又取消的比例""太晚下单的比例"。
5.3 去重统计:CASE WHEN的误用重灾区
去重统计是很多人在CASE WHEN上栽跟头的地方。比如要统计每个渠道下"至少下单一次的用户数",新手很容易写成:
SELECT channel, SUM(CASE WHEN user_id > 0 THEN 1 ELSE 0 END) AS user_cnt FROM orders GROUP BY channel;这个结果根本不对。它统计的是"订单条数",不是"用户数"。一个用户下了三单,这里会被数三次。正确的去重统计要用COUNT(DISTINCT):
SELECT channel, COUNT(DISTINCT CASE WHEN status = 1 THEN user_id END) AS paid_user_cnt FROM orders GROUP BY channel;这个写法是先选出已支付订单里的user_id,再对user_id做去重计数。CASE WHEN在这里只是过滤作用,真正去重的是DISTINCT。注意,我用的是THEN user_id END而没写ELSE,因为NULL会被DISTINCT忽略,效果等同于"排除未支付订单"。如果你写ELSE 0,反而会把0作为一个非NULL值数进去,结果直接多一条。
COUNT(DISTINCT)在数据量大时性能一般,这是MySQL本身的一个短板。如果去重的基数特别大,优先考虑用子查询先缩小数据集,再在外面做聚合。但那是性能优化的话题了,练习数据汇总语法时,先把逻辑写对。
6. 实战里最容易翻车的几个点与排查流程
6.1 NULL的静默污染:为什么SUM结果"凭空变小"
CASE WHEN里NULL的表现是最隐蔽的坑。前面提到过ELSE不写会导致NULL,现在说一个更隐蔽的变体:
SUM(CASE WHEN channel = 'app' THEN amount END) AS app_amount这个写法想统计App渠道的订单总金额。问题在于,非App渠道的订单在CASE WHEN里返回NULL,SUM会忽略NULL,所以结果看起来是对的。但如果某一天数据里混入了channel为NULL的订单,这些订单也会被忽略,你不会得到任何报错,甚至不知道它们存在。
排查这一类问题时,我的流程很固定。先跑一条"探针SQL",把应该纳入统计的NULL情况单独列出来:
SELECT channel, COUNT(*) AS cnt, SUM(CASE WHEN channel = 'app' THEN amount END) AS app_amount_total, SUM(amount) AS all_amount FROM orders GROUP BY channel;对比app_amount_total和all_amount,如果两者差得很多,说明有大量数据没被CASE WHEN命中。再做一次全表检查:
SELECT COUNT(*) FROM orders WHERE channel IS NULL OR channel = '';数据质量问题的根因往往不在CASE WHEN的写法,而是源数据里就有脏值。CASE WHEN只是在聚合时把这些脏值静默地丢弃了。所以我的经验是:任何CASE WHEN做条件聚合的SQL,上线前都必须单独验证"未命中分支"到底有多少数据,不能想当然认为所有数据都落在你的WHEN条件里。
6.2 WHERE和CASE WHEN的执行顺序差异:过滤不能乱放
CASE WHEN可以写在WHERE里做过滤,也可以写在SELECT里做分类,但两者的执行语义完全不同。很多人写复杂SQL时会把逻辑混在一起,导致结果和预期对不上。
一个典型的错误是,想统计"每个金额区间里App渠道的订单数",却把渠道过滤条件只写进了WHEN里:
SELECT CASE WHEN amount <= 100 THEN '区间A' WHEN amount <= 500 THEN '区间B' ELSE '区间C' END AS price_range, COUNT(*) AS app_cnt FROM orders GROUP BY price_range;这里COUNT(*)统计的是所有渠道的订单,根本不是App渠道的。正确写法要么把channel='app'写进WHERE:
SELECT CASE WHEN amount <= 100 THEN '区间A' WHEN amount <= 500 THEN '区间B' ELSE '区间C' END AS price_range, COUNT(*) AS app_cnt FROM orders WHERE channel = 'app' GROUP BY price_range;要么用COUNT(CASE WHEN channel = 'app' THEN 1 END)。两种写法结果一样,但语义上,WHERE在聚合前过滤行,SELECT里的CASE WHEN在聚合时过滤值。能用WHERE的先在WHERE里过滤,尽量别把过滤条件堆在CASE WHEN里,这样SQL执行的行数更少,性能更好,逻辑也清晰。
顺带提一个执行顺序的常识:SQL里WHERE是在GROUP BY之前执行的,HAVING是在GROUP BY之后执行的。如果你在HAVING里引用CASE WHEN的别名做过滤,在MySQL里是允许的,比如HAVING price_range = '区间A',但要记住HAVING里的过滤发生在聚合之后,和WHERE的处理顺序完全不同。
6.3 性能与可维护性:CASE WHEN不是写得越多越好
CASE WHEN在汇总SQL里写多了,SQL会变得非常冗长。我之前接过一个同事留下的报表SQL,光CASE WHEN就写了三十多个,嵌套了三层,缩进一塌糊涂。这种SQL的运行效率通常不会太差,但维护体验极差。改一个条件要找半天,改错了还不敢保证没影响其他分支。
维护这种SQL,我现在的几个习惯:
- 每写一个CASE WHEN,都取一个语义明确、一看就知道业务含义的别名,不要用c1、c2这样的临时名。
- 多层嵌套时,每层缩进对齐,每个CASE配对的END在同一垂直线上,方便数配对。
- 同一份SQL里多次使用同一个分类逻辑时,考虑用WITH子查询先算出一个分类结果表,再JOIN聚合,避免在每个SUM里重复贴一大段CASE WHEN。
WITH order_tagged AS ( SELECT id, channel, CASE WHEN amount <= 100 THEN '区间A' WHEN amount <= 500 THEN '区间B' WHEN amount <= 1000 THEN '区间C' ELSE '区间D' END AS price_range, status FROM orders ) SELECT price_range, COUNT(*) AS total_cnt, COUNT(CASE WHEN status = 1 THEN 1 END) AS paid_cnt FROM order_tagged GROUP BY price_range;这样的结构还有个额外好处:如果之后需要按区间关联别的表,order_tagged这个临时结果可以直接复用。用WITH不仅让主查询变短,也让分类逻辑只维护一处。
至于性能,CASE WHEN本身在MySQL里的执行成本并不高,它只是对每行做一次条件判断。真正影响性能的是聚合的行数和索引利用情况。如果CASE WHEN的字段有索引(比如amount),MySQL有可能在聚合时用上索引;但CASE WHEN后分组的结果集,往往需要临时表排序。GROUP BY字段过多或者结果集过大时,可以在EXPLAIN的Extra列里看到Using temporary、Using filesort,那就该考虑优化了。
6.4 边界值和类型:写CASE WHEN之前先确认字段定义
最后一个坑来自字段类型。金额一般用DECIMAL,日期时间用DATETIME,状态码用TINYINT。但实际表里经常有历史遗留问题,比如金额列是VARCHAR,状态列是字符串"1"而不是数字1。这种时候,CASE WHEN的条件判断会走隐式类型转换,结果未必符合直觉。
比如:
CASE WHEN status = 1 THEN '已支付' ELSE '未支付' END如果status是VARCHAR类型且存的是'1',这个判断也能匹配上,MySQL会做隐式转换。但一旦数据里混入空格、字母,比如' 1'或者'ok',隐式转换的规则就会变得很奇怪,'abc'转数字会变成0,同时给一条warning。实践中我遇到过因为脏数据导致CASE WHEN分类全部落入ELSE分支的案例,排查了很久才发现是字段类型和数据质量的双重问题。
因此我的排查流程里有一条固定动作:写CASE WHEN之前,先看SHOW CREATE TABLE确认字段定义,再抽样几条数据确认实际存储值。尤其是做区间统计时,边界值(比如恰好等于100、500)到底算哪个区间,不仅要和业务确认,还要在SQL里用注释写清楚,防止别人接手时改错。
写在最后:一次把CASE WHEN练到位的方法
我在练习CASE WHEN数据汇总时,总结了一套自测清单,分享给你当检验标准。第一,用一张订单表做区间统计,验证边界值归属;第二,做一次行转列,把两个维度变成宽表;第三,写一条SQL同时统计总数、条件计数、条件求和、条件平均;第四,故意去掉ELSE,观察NULL对SUM和COUNT的影响;第五,用EXPLAIN看一遍执行计划,确认扫描行数和临时表情况。这五条练熟了,日常报表里的数据汇总需求基本都能覆盖。
我个人最深的体会是,CASE WHEN的学习曲线很短,但要真正用得不出错,靠的是对NULL行为、执行顺序和边界条件这三件事的敏感度。这三件事任何一件没搞清楚,写出来的SQL在数据量小的时候可能碰巧是对的,数据量一大或者脏数据一多,问题就全冒出来了。做练习时,多给自己造一些边界数据,比如空值、0、负数、超长字符串,能让你的SQL经得起真实业务的考验。