ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MySQL CASE WHEN 表达式实战:语法、业务场景与性能优化

MySQL CASE WHEN 表达式实战:语法、业务场景与性能优化 SQL这行干久了你会发现真正高频的东西往往不是那些看起来炫酷的黑科技而是像CASE WHEN这种谁都会写、但未必谁都写对的表达式。做报表、写统计脚本、处理状态流转我几乎每个星期都要和它打交道。CASE WHEN在 MySQL 里本质上是一个条件表达式不是控制流语句它的作用是在一条 SQL 中根据条件返回不同的结果从而把复杂的 if-else 逻辑从业务代码下沉到数据层。这篇文章适合三类人刚学 MySQL 的学生、被报表需求反复折磨的开发同学、以及想在数据加工上少绕弯子的取数分析师。我会从语法拆起结合大量业务示例最后把那些让人抓狂的坑一个一个踩给你看。1. CASE WHEN 是什么先从语法和执行原理说起想用好CASE WHEN第一步是理解它的两种写法。很多教程只讲一种导致你换个场景就写不出来了。1.1 两种写法简单函数和搜索函数MySQL 的CASE有两种语法格式目的相同但适用场景不同。第一种叫简单函数格式simple case语法非常紧凑CASE value WHEN compare_value THEN result1 WHEN compare_value THEN result2 ... ELSE resultN END这种写法拿value逐个和compare_value做等值比较匹配上哪个就返回对应的result。比如根据状态码返回状态名称SELECT order_id, CASE status WHEN 0 THEN 待支付 WHEN 1 THEN 已支付 WHEN 2 THEN 已发货 ELSE 未知状态 END AS status_name FROM orders;第二种叫搜索函数格式searched case也是日常开发中更常用的姿势CASE WHEN condition1 THEN result1 WHEN condition2 THEN result2 ... ELSE resultN ENDWHEN后面跟的是完整的条件表达式可以是 也可以是LIKE、BETWEEN、IN甚至是子查询。比如把订单金额分成几个档位SELECT order_id, amount, CASE WHEN amount 100 THEN 小额订单 WHEN amount 1000 THEN 普通订单 WHEN amount 5000 THEN 大额订单 ELSE 超大额订单 END AS order_level FROM orders;这两种格式之间有个最容易被忽视的区别简单格式只能做等值比较搜索格式能做范围、模糊、集合判断。实际业务里范围判断多得多所以搜索格式出镜率最高。我在工作中几乎只写搜索格式简单格式偶尔用于枚举值映射。1.2 CASE WHEN 的执行顺序和返回值规则很多人把CASE WHEN当成 IF-ELSE IF 链来看大方向没错但有个关键细节CASE WHEN是自上而下匹配一旦命中某个 THEN 就立即返回后面的分支不再计算。这意味着分支顺序很重要。看这个例子SELECT score, CASE WHEN score 60 THEN 及格 WHEN score 80 THEN 优秀 WHEN score 90 THEN 学霸 ELSE 不及格 END AS grade FROM exam;这一段 SQL 永远只可能出现“及格”和“不及格”两个结果因为分数只要超过 60第一个分支就把结果拦住了。如果你把 90写在最上面 80写第二个 60写最后一个三个档位才能正常输出。我见过不止一次这种因为顺序写错导致报表分级全乱的事故排查起来还特别隐蔽因为 SQL 语法没错肉眼扫过去也不容易发现问题。另外要记住如果所有条件都不满足又没有写ELSE表达式返回的是NULL。NULL在统计和合计时会有连锁反应后面专门讲。1.3 为什么说 CASE 是“表达式”而不是“语句”标题写的是“CASE WHEN 语句”但严格来说MySQL 里的CASE是一个表达式英文叫 expression不是 statement。表达式意味着它必须产生一个值可以出现在 SELECT 的字段列表里、WHERE 条件里、ORDER BY 排序字段里、GROUP BY 分组字段里甚至出现在函数参数中。这一点不是抠字眼。存储过程里有一个独立的CASE流程控制语句那是真正的“语句”用来控制程序走向不产生值。很多新手在存储过程里想用CASE返回一个结果却写成了流程控制语句结果各种报错。搞清楚“表达式”和“语句”的区别能少踩一个很大的坑。2. CASE WHEN 的经典业务场景一次看清五个用法理论讲完了接下来看CASE WHEN在真实业务里最常见的五个用法。这里的例子我都用实际业务场景改写过你可以直接套用。2.1 报表统计一次 SELECT 搞定多条件计数报表需求里最烦人的是“同一张表、同一种维度、好几套统计口径”。比如领导要同时看订单总数、已支付数、已发货数、已取消数很多人第一反应是写四条 SQL 再在业务层聚合。用CASE WHEN可以一条 SQL 解决SELECT COUNT(*) AS total_order, SUM(CASE WHEN status PAID THEN 1 ELSE 0 END) AS paid_order, SUM(CASE WHEN status SHIPPED THEN 1 ELSE 0 END) AS shipped_order, SUM(CASE WHEN status CANCELLED THEN 1 ELSE 0 END) AS cancelled_order FROM orders;这里的技巧是用SUM(CASE WHEN ... THEN 1 ELSE 0 END)对满足条件的行计数。THEN 1表示命中就加 1ELSE 0表示不命中就不加。这条 SQL 走一次表扫描把所有统计口径一次算完比四条独立 SQL 性能好太多。如果只是计数还可以简化成COUNT(CASE WHEN status PAID THEN 1 END)当条件不匹配时返回 NULL而 COUNT 会自动忽略 NULL。此时可以省略ELSE 0代码更干净。2.2 数据分桶把连续数值切成业务区间分桶的典型场景是“年龄分段”“价格区间”“时间区间”。数据是连续的业务是离散的CASE WHEN就是连接两者的桥。以商品价格为例SELECT CASE WHEN price 50 THEN 0-50元 WHEN price 100 THEN 50-100元 WHEN price 500 THEN 100-500元 ELSE 500元以上 END AS price_bucket, COUNT(*) AS product_count, MAX(price) AS max_price FROM products GROUP BY price_bucket;这一段 SQL 注意两点。第一分桶条件用price 50而不是price 0 AND price 50因为前一个条件天然包含了后一个的下界当然前提是你确认没有负数价格第二GROUP BY 可以直接使用 SELECT 里定义的别名price_bucket这在 MySQL 里是允许的但如果你用 Oracle 或者严苛模式的数据库最好把整个 CASE 表达式复制到 GROUP BY 里保证可移植性。分桶也可以按日期做。比如区分“今年订单”和“历史订单”SELECT CASE WHEN YEAR(created_at) YEAR(CURDATE()) THEN 今年 ELSE 历史 END AS date_bucket, COUNT(*) AS cnt FROM orders GROUP BY date_bucket;如果你要按月分桶更推荐DATE_FORMAT(created_at, %Y-%m)但那是另一个函数的话题了和CASE WHEN组合起来用效果一样好。实际报表需求里我经常用“CASE WHEN 分桶 GROUP BY 聚合 ORDER BY FIELD 自定义排序”的组合拳三行代码就能把一张有档位、有数量、有排序的统计报表拉出来。2.3 自定义排序让 ORDER BY 按你的规则走MySQL 默认排序只能按字段值顺序来数字从小到大字符串按字典序但这往往不是业务想要的顺序。比如任务表的状态有“待处理”“处理中”“已完成”“已取消”你希望列表按“待处理优先、处理中其次、已完成最后”的顺序排这个需求就得上CASE WHENSELECT task_id, task_name, status FROM tasks WHERE deleted 0 ORDER BY CASE status WHEN 待处理 THEN 1 WHEN 处理中 THEN 2 WHEN 已完成 THEN 3 ELSE 4 END;用在 ORDER BY 里的 CASE 不会出现在 SELECT 字段里也不会影响返回的数据只负责告诉 MySQL“按什么顺序吐数据”。这比在业务层先取出所有记录再手工排序要高效得多尤其当分页查询时排序在数据库完成才能真正按正确顺序分页。自定义排序还可以和业务权重结合比如紧急且超时的任务排最前ORDER BY CASE WHEN priority 紧急 AND is_duedate_exceeded 1 THEN 1 WHEN priority 紧急 THEN 2 WHEN due_date IS NOT NULL THEN 3 ELSE 4 END;注意 ORDER BY 中的 CASE 表达式会在每条记录上执行如果表特别大这里会有一定的计算开销但只要排序字段走不了索引这个开销就是不可避免的。后面性能小节再细说。2.4 行转列把明细数据变成宽表行转列、透视表这类需求CASE WHEN配合聚合函数是最经典的实现方式。比如一张订单明细表每个商品分类一行你想看每个分类在不同支付状态下的订单数竖排明细是方便查询的但办报表的人想要横排宽表SELECT product_type, COUNT(CASE WHEN status PAID THEN 1 END) AS paid_cnt, COUNT(CASE WHEN status SHIPPED THEN 1 END) AS shipped_cnt, COUNT(CASE WHEN status CANCELLED THEN 1 END) AS cancelled_cnt FROM orders GROUP BY product_type;这里我用COUNT而不是SUM原因前面提过COUNT忽略 NULL条件不匹配时 CASE 返回 NULL自然不计入计数。这段 SQL 跑完输出就是一张“每个商品分类 × 不同状态订单数”的宽表Excel 透视表能做的事SQL 也能做。如果想统计的是金额而不是单数把1换成需要求和的字段SELECT product_type, SUM(CASE WHEN status PAID THEN amount ELSE 0 END) AS paid_amount FROM orders GROUP BY product_type;行转列能解决很多实际痛点。我在做经营分析报表时通常会用CASE WHEN把“同一时间维度下的多个指标”展开到同一行里比如“本月新增用户、本月活跃用户、本月成交用户”这样后续做同比环比时只查一次表就够了。行转列和聚合函数是CASE WHEN最黄金的组合没有之一。2.5 空值处理和格式化输出数据查询最常见的问题之一就是 NULL 显示为空表格导出后不好看业务同事也不买账。CASE WHEN可以灵活处理 NULL 值。SELECT user_id, nickname, CASE WHEN phone IS NULL THEN 未绑定手机号 ELSE phone END AS phone_display, CASE WHEN register_time IS NULL THEN 未知注册时间 ELSE DATE_FORMAT(register_time, %Y-%m-%d) END AS register_date FROM users;这里很多人会写成CASE phone WHEN NULL THEN ...这是错的。简单 CASE 格式比较时用的是等号规则而phone NULL的结果是未知 UNKNOWN永远不会为真所以如果列值是 NULLCASE phone WHEN NULL根本不会命中。处理 NULL 一定要用IS NULL判断要么使用搜索格式。另外CASE WHEN还能把布尔值、枚举值美化成人话。比如把0/1字段转成“是/否”把状态码转成“已支付/未支付”。格式化输出这个用法虽然技术含量不高但交付给非技术同事时体验完全不是一个级别。3. 进阶玩法CASE WHEN 与特性组合出战斗力基础会了之后开始上强度。CASE WHEN真正的威力在于和 MySQL 其他特性组合使用尤其是聚合、更新、窗口函数。3.1 聚合函数加 CASESUM、COUNT、AVG 的组合拳最常见的组合是SUM(CASE WHEN ...)和COUNT(CASE WHEN ...)。前者做条件求和后者做条件计数。还有一个使用频率稍低但很实用的组合AVG(CASE WHEN ... THEN ... END)它只对符合条件的记录求平均值。比如统计每个分类的平均发货时长但要剔除异常订单SELECT product_type, AVG(CASE WHEN order_status SHIPPED THEN ship_hours END) AS avg_ship_hours FROM orders GROUP BY product_type;条件不匹配的订单返回 NULLAVG 自动忽略 NULL这样算出来的平均数不会被异常状态污染。如果你对“特定数据子集的均值”有需求这个写法比子查询加 JOIN 简单得多。CASE WHEN还经常和COUNT(DISTINCT ...)一起用。比如看每天有多少个用户产生了支付行为但一个用户一天可能下多单直接 COUNT 会重复计数SELECT DATE(created_at) AS day, COUNT(DISTINCT CASE WHEN status PAID THEN user_id END) AS paid_user_count FROM orders GROUP BY DATE(created_at);这一段 SQL 是我做用户分析时的高频代码。CASE WHEN先把有效用户 ID 筛选出来DISTINCT保证同一用户只计一次COUNT忽略 NULL三个机制环环相扣一个多余的子查询都不用写。3.2 在 UPDATE 语句中做多路条件更新CASE WHEN不只是查询时用还非常适合在 UPDATE 语句里做“多条件赋不同值”的批量更新。典型场景根据订单金额重新划级根据会员积分重算等级。UPDATE orders SET order_level CASE WHEN amount 5000 THEN S WHEN amount 1000 THEN A WHEN amount 100 THEN B ELSE C END, settlement_status CASE WHEN amount 0 THEN 异常 ELSE settlement_status END WHERE status PAID;这种写法把多条 UPDATE 合并成一条大大减少了和数据库的交互次数。在批量数据修正场景下性能提升是非常明显的。但这里我要提醒一个非常容易踩的坑在同一个 UPDATE 语句里多个 SET 字段上的CASE WHEN是并行的它们都基于WHERE筛选出来的旧值计算不会互相引用前面已经 SET 出的新值。这个和存储过程按行执行的CASE流程控制完全不同别指望“先 SET 状态再用新状态决定下一个字段”这种逻辑在一条 UPDATE 里成立。想做级联式的状态流转应该把逻辑拆开或者用存储过程按步骤执行。另外CASE WHEN也可以用在INSERT ... ON DUPLICATE KEY UPDATE里冲突时根据条件决定新值INSERT INTO order_daily_summary (order_date, total_amount, order_count) VALUES (CURDATE(), 1000.00, 5) ON DUPLICATE KEY UPDATE total_amount total_amount VALUES(total_amount), order_count CASE WHEN order_count 100 THEN order_count VALUES(order_count) ELSE order_count END;这种写法看起来有点绕但确实可以在数据库层面做“有条件的增量更新”比先 SELECT 再判断再 UPDATE 的组合要简洁得多。3.3 窗口函数里的 CASE WHEN累计值和同环比MySQL 8.0 支持窗口函数后CASE WHEN和窗口函数的组合直接打开了一片新的应用场景。举一个最常见的需求统计每个用户截至当前订单的累计已支付金额。SELECT user_id, order_id, amount, status, SUM(CASE WHEN status PAID THEN amount ELSE 0 END) OVER (PARTITION BY user_id ORDER BY created_at) AS cumulative_paid_amount FROM orders;这行 SQL 的输出结果里每一行都会带上一个“当前用户到目前为止的累计已支付金额”。CASE 先过滤出已支付的金额SUM 配合 OVER 子句做窗口累加PARTITION BY 保证不同用户的累计互不干扰。这种代码在财务对账、销售业绩追踪里非常实用。窗口函数里也能用COUNT(DISTINCT CASE WHEN ...) OVER (...)做滑动窗口的活跃用户数统计但要注意它对性能和内存的消耗明显比非窗口写法大表数据量特别大时需要有心理预期。3.4 存储过程里的 CASE 语句和 CASE 表达式不是一回事前面提过“语句”和“表达式”的区别这里展开讲讲因为这是很多人容易混淆的雷区。在存储过程里存在一种独立的流程控制语法CASECREATE PROCEDURE process_order(IN order_id INT) BEGIN DECLARE status_code INT; SELECT status INTO status_code FROM orders WHERE id order_id; CASE status_code WHEN 0 THEN UPDATE orders SET remark 待支付提醒 WHERE id order_id; WHEN 1 THEN UPDATE orders SET remark 已支付无需处理 WHERE id order_id; ELSE UPDATE orders SET remark 其他状态 WHERE id order_id; END CASE; END这个CASE ... END CASE是存储过程里的“流程控制语句”控制执行多条语句本身不返回值。它和 SQL 查询里CASE WHEN ... END的“标量表达式”完全是两回事。我在以前带新人时经常看到有人把END CASE和END混着写或者把查询用的CASE WHEN直接搬进存储过程想控制流程结果一堆语法错误。判断方法很简单看它是否在一个能产生值的上下文里。SELECT 字段后面是表达式存储过程过程块里是语句。4. 性能、索引与写法优化别让 CASE WHEN 拖慢查询很多 SQL 教程只讲能跑通不讲跑的代价。这一节把CASE WHEN的性能问题说清楚。4.1 CASE WHEN 会索引失效吗先说结论CASE表达式本身不会让索引失效要看你把它放在什么位置以及CASE内部包裹了什么。最常见的问题是把CASE WHEN用在 WHERE 条件里包住了本该走索引的列。比如下面这种写法SELECT * FROM orders WHERE CASE WHEN :p_mode today THEN created_at CURDATE() WHEN :p_mode week THEN created_at CURDATE() - INTERVAL 6 DAY ELSE TRUE END;这段 SQL 的意图是根据传入参数动态筛选“今天”或“最近一周”的订单。但它有一个致命问题WHERE 里的函数表达式包住了created_atMySQL 无法在非等值情况下对created_at直接使用索引优化器大概率选择全表扫描。数据量小的时候没感觉数据量上到几百万行就会变成灾难。更好的写法是把条件展开成布尔表达式让优化器有机会走索引SELECT * FROM orders WHERE (:p_mode today AND created_at CURDATE()) OR (:p_mode week AND created_at CURDATE() - INTERVAL 6 DAY);多数情况下这种写法可以将created_at上的范围条件拆开让优化器对每根分支做索引范围扫描。两种写法在结果上等价但执行计划差别巨大。我个人测试过 500 万行的表第二种写法比第一种快了一个数量级。4.2 与 WHERE、IF、UNION ALL 的取舍CASE WHEN的职责是“根据条件生成新值”WHERE的职责是“过滤行”两者并不是互相替代但有些查询确实可以用不同的方式实现。需要做对比时我会看三个维度维度CASE WHENIF 函数UNION ALL 改写标准性SQL 标准通用MySQL 专属标准通用分支数不限只能三分IF(条件,真,假)每段独立查询无限段可读性中等分支多时偏长高但不灵活代码冗余性能特点一次扫描逐行计算同左多段查询可能多次扫描适用场景行内生成列、聚合条件、排序简单两分判断对不同条件走不同索引策略如果你只是在 SQL 里做二选一的简单映射IF()函数确实更简洁可读性也好SELECT name, IF(score 60, 及格, 不及格) AS pass_grade FROM exam;但当分支超过两个或者需要做范围比较就别死磕IF()了CASE WHEN才是合适的选择。IF()的语法只能支持一个条件和两个返回值嵌套三四层IF()之后那代码根本没法维护。UNION ALL改写适合出现在 WHERE 中需要多条件访问索引的场景。比如你要统计“本月新订单”和“上月历史订单”两类数据写成两个SELECT ... WHERE再拼起来每段都能充分利用各自的条件索引整体执行效率可能会更好。但这种写法的代价是代码长度翻倍、开发维护成本高。所以我的策略是简单场景优先CASE WHEN复杂大表多分支优先UNION ALL先跑执行计划再决定要不要改。4.3 一个真实慢查询的优化过程分享一个我实际排查过的案例。有一张交易流水表每天新增几十万行其中有个统计接口的查询要让运营同事等十几秒才出结果慢得没法用。原始 SQL 长这样SELECT user_id, SUM(CASE WHEN trans_type RECHARGE THEN amount ELSE 0 END) AS recharge_amount, SUM(CASE WHEN trans_type WITHDRAW THEN amount ELSE 0 END) AS withdraw_amount FROM transactions WHERE trans_type IN (RECHARGE, WITHDRAW) AND create_time 2025-01-01 00:00:00 GROUP BY user_id;表面上看没啥问题但执行计划显示它全表扫描了因为create_time虽然有索引trans_type上的 IN 条件选择性不够优化器认为还不如直接扫全表快。后来我把查询改成两条独立统计再合并SELECT user_id, SUM(amount) AS recharge_amount, 0 AS withdraw_amount FROM transactions WHERE trans_type RECHARGE AND create_time 2025-01-01 00:00:00 GROUP BY user_id UNION ALL SELECT user_id, 0 AS recharge_amount, SUM(amount) AS withdraw_amount FROM transactions WHERE trans_type WITHDRAW AND create_time 2025-01-01 00:00:00 GROUP BY user_id;改写后每个分支都只处理一种trans_typeMySQL 可以对(trans_type, create_time)的联合索引做高效范围扫描查询从十几秒降到了两秒以内。这个案例想说明的不是CASE WHEN慢而是任何工具都要看上下文。CASE WHEN写起来简洁但如果它导致优化器无法选择最优执行计划该拆就拆该用 UNION ALL 就用。性能调优的原则永远是先看执行计划再动刀。5. 常见错误与排查技巧我踩过的坑你都别踩了这一节把CASE WHEN最容易踩的坑集中列出来每个都是真实线上遇到过的问题。5.1 和 NULL 纠缠不清统一说一句话判断 NULL 只能用IS NULL不能写 NULL更不能在简单 CASE 里写CASE col WHEN NULL。错误写法SELECT name, CASE phone WHEN NULL THEN 无手机号 ELSE phone END AS phone_display FROM users;这个写法永远走不进THEN因为phone NULL的结果是 NULL 未知不是真也不是假。正确写法SELECT name, CASE WHEN phone IS NULL THEN 无手机号 ELSE phone END AS phone_display FROM users;再往深一层CASE和聚合函数组合算 NULL 时也要注意聚合函数的口径。SUM(amount)碰到 NULL 会跳过COUNT(字段)也会跳过 NULL某些报表里遇到“明明有数据但汇总缺失”的情况多半就是数据里藏着 NULL。5.2 返回值类型不一致CASE WHEN的每个分支返回值会被 MySQL 做类型统一。如果分支一返回字符串分支二返回数字MySQL 会自动把数字转成字符串或者反过来这个隐式转换可能导致比较和排序结果出乎意料。我之前遇到过一个支付金额溢出 0 位的怪问题排查到最后发现 CASE 里THEN 0和THEN 00混用了MySQL 把数字 0 转成了字符串 0后续拼接时就少了一位补零。解决办法是让所有分支返回同一种类型。写 SQL 时有个习惯同一段 CASE 里类型要一致必要时用 CAST 显式声明返回类型。SELECT CASE WHEN type 1 THEN CAST(amount AS CHAR) WHEN type 2 THEN N/A ELSE 0 END AS amount_text FROM pay_log;5.3 忘记写 ELSE结果返回 NULL很多新手不知道ELSE是可选的不写时所有未匹配的记录都会返回 NULL。如果是 SELECT 字段展示最多看到空值如果是聚合条件NULL 被聚合函数忽略统计结果就会缺一块。看这个错误案例SELECT product_type, SUM(CASE WHEN status PAID THEN amount END) AS paid_amount FROM orders GROUP BY product_type;状态不是 PAID 的记录CASE 返回 NULLSUM 自动忽略所以这个 sum 是对的。但如果你的本意是“未支付金额为 0也需要参与某些求和或均值计算”少了ELSE 0就意味着这些记录直接消失。在均值场景结果会悄悄出错。所以我的经验是除了明确想用 NULL 占位的场景一律补上 ELSE。这也让代码意图更清晰可读性更好。5.4 简单 CASE 和搜索 CASE 用错地方简单 CASE 只能做“一个字段对多个离散值的等值比较”一旦你写成这样就会报错SELECT CASE score WHEN score 60 THEN 及格 WHEN score 80 THEN 优秀 END FROM exam;简单 CASE 的WHEN后面跟的是值不是条件表达式。想比较大小、做范围判断必须用搜索格式CASE WHEN score 60 THEN ...。反过来如果你的条件是等值判断比如状态编码映射用简单 CASE 或搜索 CASE 都行两者性能没有本质差别选自己看着习惯的就行。5.5 问题排查速查表症状可能原因解决方案命中的分支总不是预期的分支顺序写反宽条件在前把窄条件放前面宽条件放后面结果全是 NULL忘了 ELSE或判断 NULL 用了 NULL补 ELSE使用 IS NULL报语法错误简单 CASE 的 WHEN 里写了条件表达式改用搜索格式排序错乱CASE 返回类型不一致隐式转换统一返回类型用 CASTWHERE 条件很慢CASE 包住了索引列索引失效拆成布尔表达式或 UNION ALL6. 完整示例从建表到报表一次走通最后用一个完整案例把前面所有知识点串起来。需求定义经营分析要给运营团队出一张“商品分类 × 订单状态”的日报要求统计每个分类下的订单数、已支付金额、平均单笔金额并支持按“电子、服装、食品、其他”的固定顺序输出。先建表并插入一批测试数据CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, product_type VARCHAR(20) NOT NULL, amount DECIMAL(10,2) NOT NULL DEFAULT 0.00, status VARCHAR(20) NOT NULL DEFAULT PENDING, created_at DATETIME NOT NULL ); INSERT INTO orders (user_id, product_type, amount, status, created_at) VALUES (101, 电子, 1200.00, PAID, 2025-01-05 10:00:00), (101, 服装, 230.50, SHIPPED, 2025-01-10 11:30:00), (102, 电子, 88.00, CANCELLED, 2025-02-01 09:20:00), (103, 食品, 39.90, PAID, 2025-02-03 14:00:00), (104, 电子, 5999.00, PAID, 2025-02-05 16:45:00), (105, 其他, 500.00, PENDING, 2025-02-06 08:30:00), (106, 服装, 899.00, PAID, 2025-02-07 20:15:00), (107, 食品, 156.00, CANCELLED, 2025-02-08 12:10:00), (108, 电子, 2999.00, SHIPPED, 2025-02-09 18:00:00);然后写核心报表 SQLSELECT product_type, COUNT(*) AS order_count, SUM(CASE WHEN status PAID THEN amount ELSE 0 END) AS paid_amount, AVG(CASE WHEN status PAID THEN amount ELSE NULL END) AS paid_avg_amount, COUNT(CASE WHEN status PAID THEN 1 END) AS paid_order_count FROM orders WHERE created_at 2025-01-01 00:00:00 GROUP BY product_type ORDER BY CASE product_type WHEN 电子 THEN 1 WHEN 服装 THEN 2 WHEN 食品 THEN 3 ELSE 4 END;这段 SQL 把本节前面所有知识点全用上了SUM(CASE WHEN)条件求和AVG(CASE WHEN)忽略 NULL 计算均值COUNT(CASE WHEN)条件计数ORDER BY CASE自定义类别顺序。查出来的结果如下product_typeorder_countpaid_amountpaid_avg_amountpaid_order_count电子47199.002399.673服装2899.00899.001食品239.9039.901其他10.00NULL0输出结果里有个点值得留意其他分类没有任何已支付订单paid_avg_amount是 NULL。如果你希望它显示 0.00 而不是 NULL就要在外层包一层IFNULL。我对写 SQL 的个人习惯是把需求里互斥的状态先理清楚再决定用几个CASE分支顺序从窄条件写到宽条件每条CASE都补足ELSE。这样写出来的报表 SQL 经得起折腾也方便几个月后再回来维护。你大概率也会遇到比我更变态的报表需求但只要把CASE WHEN这套底子打牢后面用PIVOT、窗口函数、CTE 组合时都会轻松很多。
返回列表