写SQL做报表这件事,时间一长你就会发现,真正卡住效率的往往不是那些复杂到头皮发麻的窗口函数,反而是像 PostgreSQL 的 CASE WHEN 这种看起来平平无奇的基础表达式。我接触 PG 是在几个业务系统陆续迁到开源数据库之后,日常干得最多的事,就是把一张流水表按业务口径拆成各种统计结果。这类需求如果全丢给应用层去处理,代码会绕一整圈;直接在 SQL 里把条件分支写清楚,三五行就能让结果集变成报表需要的结构。最近看到 PostgreSQL 相关检索里“case when”“count(case when”一直是高频词,说明大家写 SQL 时对这个表达式还是有不少纠结的地方,所以我打算把平时用得比较多、也踩过坑的几个场景整理成一篇偏实战的笔记。
这篇文章会紧扣 PostgreSQL 来写,覆盖 CASE WHEN 的基础语法、条件聚合、条件更新、执行计划视角下的性能边界,以及最容易翻车的那几个细节。适合刚接触 PG 的读者照着敲一遍,也适合已经写过一些 SQL 但对 CASE WHEN 的认知还停留在“if-else 的替代品”的同学,帮你把它真正用出价值。
1. 先回归本质:CASE WHEN 是标量表达式,不是控制流程
不少从 Java、Python 转过来写 SQL 的人,第一反应会把 CASE WHEN 理解成 if-else。这个类比能帮助你快速上手,但它会带来一个隐患:你会下意识觉得 CASE WHEN 能“执行某段逻辑”,而实际上它在 PostgreSQL 里只是一个会返回单个值的表达式。理解这一点,是所有高级用法的基础。
1.1 表达式和语句的区别在哪
if-else 是过程式语言里的控制结构,它可以决定“接下来执行哪一段代码”。SQL 是声明式语言,你在 SELECT 列表里写的每一列,本质都是一个表达式,CASE WHEN 只是众多表达式中的一种。它做的事情是:对每一行输入,根据 WHEN 后面的条件决定返回哪个值。
举个例子就很好懂:
sql复制SELECT
order_id,
order_status,
CASE order_status
WHEN 'P' THEN '已下单'
WHEN 'S' THEN '已发货'
WHEN 'D' THEN '已完成'
WHEN 'C' THEN '已取消'
ELSE '未知状态'
END AS status_name
FROM orders
WHERE order_date >= date '2024-01-01';
这里 CASE ... END 和 order_id 没有本质区别,都是返回一个值。这个值的类型由所有 THEN/ELSE 分支共同决定。既然它只是表达式,那它可以出现的位置就非常广:SELECT 列表、WHERE 条件、GROUP BY、ORDER BY、HAVING 里都能写,甚至可以嵌在另一个表达式内部。
1.2 状态码翻译:最朴素的实战场景
状态码翻译是 CASE WHEN 最直观的用途。很多业务表为了省空间,不会直接存中文状态,而是用单个字母或数字表示。直接暴露给运营看肯定是灾难,于是你会在查询里做一层翻译。这种情况我通常推荐用“简单 CASE”写法,代码会更短:
sql复制SELECT
logistics_code,
shipping_status,
CASE shipping_status
WHEN 0 THEN '待揽收'
WHEN 1 THEN '运输中'
WHEN 2 THEN '派送中'
WHEN 3 THEN '已签收'
WHEN 4 THEN '异常件'
ELSE '未同步'
END AS status_text
FROM logistics
LIMIT 100;
除了翻译字段,还有一类高频场景是“分档”。比如订单金额要分成大额、普通、小额,或者用户年龄要分桶,这种区间判断用简单 CASE 写不了,需要用到搜索 CASE:
sql复制SELECT
user_id,
total_amount,
CASE
WHEN total_amount >= 5000 THEN '大额订单'
WHEN total_amount >= 1000 THEN '普通订单'
ELSE '小额订单'
END AS order_level
FROM user_orders
WHERE stat_date = date '2024-06-30';
看明白了吗?前一种 CASE 直接跟一个字段,后面 WHEN 后面写的是“值”;后一种 CASE 后面不跟字段,WHEN 后面写的是完整条件表达式。这两种形态也就是 PostgreSQL 文档里说的简单 CASE 和搜索 CASE。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 简单 CASE 和搜索 CASE:两种写法背后的执行逻辑差异
我见过有人把两种写法混着用,看着不报错,但出了逻辑问题半天定位不到。事实上 PostgreSQL 对两种写法的执行逻辑有明确区分,尤其是遇到 NULL 和范围条件时,差异会直接决定结果对不对。
2.1 简单 CASE 只能做等值判断
简单 CASE 的语法是:
sql复制CASE expression
WHEN value THEN result
[WHEN ...]
[ELSE result]
END
它会把 expression 的计算结果依次和每个 WHEN value 做等值比较。这个比较用的是普通等号语义,不涉及模糊匹配,也不支持大于小于。所以如果你要判断“金额超过 1000 元”,是不能写成 CASE total_amount WHEN > 1000 THEN ... 的,语法直接报错。这种场景只能换成搜索 CASE:
sql复制CASE
WHEN total_amount > 1000 THEN '满足'
ELSE '不满足'
END
简单 CASE 最大的价值是可读性。当你要翻译的状态码有几十个时,CASE status WHEN 0 ... WHEN 1 ... 这种写法一眼就能看出在枚举取值,比写一长串 WHEN status = 0 要干净得多。
2.2 简单 CASE 对 NULL 的处理是个经典坑
先看这段逻辑:
sql复制CASE commission_rate
WHEN NULL THEN 0
ELSE commission_rate
END
你写这段的意图很明显:当 commission_rate 为 NULL 时返回 0,否则返回原值。但这个 CASE 永远走不到第一个分支,因为简单 CASE 会执行 commission_rate = NULL 这个等值比较,而 NULL 和任何值做等值比较的结果都不是 TRUE,是 NULL。最终返回结果永远是 commission_rate 本身,NULL 还是 NULL,字段并没有被兜底。
正确的写法是用搜索 CASE:
sql复制CASE
WHEN commission_rate IS NULL THEN 0
ELSE commission_rate
END
或者更简单的做法,直接用 COALESCE(commission_rate, 0)。如果你发现某个 CASE 表达式对 NULL 字段没有生效,先检查是不是在简单 CASE 里写了 WHEN NULL。这也是我在 review 同事代码时经常看到的问题。
2.3 分支顺序和短路行为:前面的条件先被评估
PostgreSQL 执行 CASE 时会从上到下依次判断 WHEN 条件,只要遇到第一个为 TRUE 的条件,就直接返回对应的 THEN 值,后面的分支不会再执行。这个短路的特性在区间判断里尤其重要。
比如你想根据年龄分三档:
sql复制CASE
WHEN age >= 18 THEN '成年'
WHEN age >= 60 THEN '老年'
ELSE '未成年'
END
这段逻辑看着好像没什么问题,但实际跑起来你会发现,60 岁以上的用户全部被归到“成年”了。因为 age >= 60 也满足 age >= 18,第一个 WHEN 先命中,后面的分支根本没有机会执行。
正确的写法要调整顺序,或者把边界写得更严谨:
sql复制CASE
WHEN age >= 60 THEN '老年'
WHEN age >= 18 THEN '成年'
ELSE '未成年'
END
所以我的经验是:写区间 CASE 时,要么把最严格、最特殊的条件放在前面,要么在条件里把区间边界写全。靠记住 PostgreSQL “从上往下执行”的顺序写代码没错,但人总会犯错,我在 SQL review 里见过太多次因为顺序写反导致的统计偏差。
3. count(case when 这类条件聚合,是统计报表的硬核写法
热搜词里 count(case when 出现的频率非常高,可见大家实际工作中对这个组合的需求量很大。它解决的是一个非常典型的报表问题:在不改变 SQL 行数粒度的前提下,把多个维度的统计结果放在同一行里。
3.1 理解 count 只数非 NULL 是掌握这个技巧的钥匙
PostgreSQL 的 count(字段) 会跳过 NULL 值,而 CASE WHEN 在没有分支命中且没有 ELSE 时,返回的正是 NULL。这两个特性加在一起,就成了条件计数的利器。
想象你有一张员工表,想统计每个部门的男女人数。通常会写两条 SQL:
sql复制SELECT department, count(*) FROM employees WHERE gender = 1 GROUP BY department;
SELECT department, count(*) FROM employees WHERE gender = 2 GROUP BY department;
但报表往往希望一行显示一个部门的所有指标,于是用条件聚合可以合并成一条:
sql复制SELECT
department,
count(CASE WHEN gender = 1 THEN 1 END) AS male_cnt,
count(CASE WHEN gender = 2 THEN 1 END) AS female_cnt,
count(CASE WHEN salary >= 20000 THEN 1 END) AS high_salary_cnt
FROM employees
GROUP BY department
ORDER BY department;
这里的原理很简单:当 gender = 1 时,CASE 返回 1,count 会计数;当 gender <> 1 时,CASE 没有 ELSE,返回 NULL,count 会跳过。所以 count(CASE WHEN 条件 THEN 1 END) 统计的就是满足条件的行数。
如果你手滑加了 ELSE 0,那完蛋了:不满足条件的行会返回 0,而 count 不会跳过 0,会把所有行都数进去。这个细节必须刻在脑子里,否则你会得到一堆看似正常、实际全错的数字。
3.2 sum 搭配 CASE WHEN:把布尔条件转成数值累加
与 count 不同,sum(CASE WHEN 条件 THEN 金额 END) 常用于按条件汇总金额。
比如统计每个区域的成交额和退款额:
sql复制SELECT
region,
sum(total_amount) AS gmv,
sum(CASE WHEN refund_flag = 1 THEN refund_amount END) AS refund_total
FROM orders
WHERE order_date >= date '2024-01-01'
AND order_date < date '2024-02-01'
GROUP BY region;
这里 refund_amount 本身是数值,不满足条件时 CASE 返回 NULL,sum 会忽略 NULL,所以不影响求和。如果加 ELSE 0 也安全,因为 0 不会改变求和结果。所以在 sum 场景下 ELSE 0 可加可不加,但为了统一风格,我通常会保留 ELSE 0,让语义更明确。
3.3 行转列:把同一列数据拆成多列
报表里另一个高频场景是把月份、状态等维度从行变成列。比如一份用户月度消费表里有 order_month 和 order_amount,要输出每个用户在一月、二月的消费金额,就能用 CASE WHEN 分组后配合 max 或 sum 实现:
sql复制SELECT
user_id,
max(CASE WHEN order_month = '2024-01' THEN order_amount END) AS jan_amount,
max(CASE WHEN order_month = '2024-02' THEN order_amount END) AS feb_amount,
max(CASE WHEN order_month = '2024-03' THEN order_amount END) AS mar_amount
FROM user_orders
WHERE order_month IN ('2024-01', '2024-02', '2024-03')
GROUP BY user_id;
有人会问,为什么要套一层 max?因为 GROUP BY user_id 之后,每个用户可能对应多行,CASE 只是把符合条件的行筛选出来,但聚合函数仍然需要决定“取哪一个值”。当每个用户在一个月份里只有一条记录时,用 max 或 min 都能得到正确结果;如果存在多条记录,那就要根据业务决定用 sum 还是 max。这个设计思路在做宽表时非常常见。
3.4 顺带一提:PostgreSQL 的 FILTER 比 CASE WHEN 更优雅
如果你确定只会在 PostgreSQL 上运行,可以试试 FILTER (WHERE ...),这是 PG 在聚合函数上提供的语法糖:
sql复制SELECT
department,
count(*) FILTER (WHERE gender = 1) AS male_cnt,
count(*) FILTER (WHERE gender = 2) AS female_cnt
FROM employees
GROUP BY department;
它的效果和 count(CASE WHEN ... THEN 1 END) 完全一样,但可读性明显更好。缺点是可移植性不如 CASE WHEN,如果你的 SQL 需要同时在 MySQL、SQL Server 等数据库上跑,CASE WHEN 才是统一的写法。我的习惯是:项目里如果有跨库需求,用 CASE WHEN;如果确定只跑 PostgreSQL,优先 FILTER。
4. UPDATE 里的 CASE WHEN:把条件分支写进 DML
很多人对 CASE WHEN 的使用范围停留在 SELECT,却忘了它在 UPDATE 语句里同样能发挥巨大作用。尤其是批量修改状态、按行设置不同字段值时,CASE WHEN 能避免写多条 UPDATE 的麻烦。
4.1 在 SET 子句中做“行级条件判断”
先看一个电商场景:订单支付超过 45 分钟还没确认支付,需要先把状态置为“待取消”,如果支付确认已经回调成功,再置为“退款中”。这类需求用一条 UPDATE 就能完成:
sql复制UPDATE orders
SET order_status = CASE
WHEN order_status = 'PAID'
AND paid_at < now() - interval '45 minutes'
AND payment_confirm = FALSE
THEN 'PENDING_CANCEL'
WHEN order_status = 'PAID'
AND payment_confirm = TRUE
THEN 'REFUNDING'
ELSE order_status
END
WHERE order_status = 'PAID';
最关键的是最后那行 ELSE order_status。当条件不满足时,把原值原样写回去。如果没有 ELSE,所有不满足条件行的 order_status 都会被改成 NULL,这种事故我见过不止一次。所以请记住:UPDATE 里的 CASE WHEN,只要你只是想修改部分行,几乎都应该带 ELSE 原字段。
4.2 结合另一张表做条件更新
PostgreSQL 的 UPDATE 可以配合 FROM 子句引入额外表,再通过 WHERE 关联,可以更灵活地做跨表更新。比如按员工绩效等级统一调整薪资档位:
sql复制UPDATE employees e
SET salary_level = CASE
WHEN p.performance_score >= 90 THEN 'S'
WHEN p.performance_score >= 75 THEN 'A'
ELSE 'B'
END
FROM performance p
WHERE e.employee_id = p.employee_id
AND p.period = '2024H1';
这里要注意:FROM 子句引入表之后,关联条件写在 WHERE 里。如果 performance 表里有多个员工的多条记录,UPDATE 可能多次更新同一行,最终结果取决于其中某一条。为了避免这种不确定性,最好先让 FROM 子句的结果集在业务上唯一,或者使用更可控的 CTE 方式。
4.3 一个代替多条 UPDATE 的小技巧
有时候你想把一批指定主键的记录改成不同状态,最直观的做法是逐条 UPDATE,代码多且性能差。也可以用一个 VALUES 列表和 CASE WHEN 配合:
sql复制UPDATE orders
SET order_status = v.new_status
FROM (VALUES
(1001, 'COMPLETED'),
(1002, 'REFUNDING'),
(1003, 'CANCELLED')
) AS v(order_id, new_status)
WHERE orders.order_id = v.order_id;
再进一步,如果需要在同一个事务里根据不同条件更新同一个表的不同行,可以按主键分拆后改用 CASE 一次更新:
sql复制UPDATE orders
SET order_status = CASE order_id
WHEN 1001 THEN 'COMPLETED'
WHEN 1002 THEN 'REFUNDING'
WHEN 1003 THEN 'CANCELLED'
ELSE order_status
END
WHERE order_id IN (1001, 1002, 1003);
这种写法比较取巧,但对于“少量固定 ID 要改不同值”的场景非常高效,还避免了多次数据库往返。不过建议主键数量控制在几百以内,否则 SQL 文本过长反而影响解析性能。
5. 执行计划视角:CASE WHEN 会不会挡住索引
很多人在写长 SQL 时会担心一个问题:CASE WHEN 用多了,查询会不会变慢?我的答案是:要分位置看。CASE WHEN 本身是标量表达式,它是否会拖慢查询,关键不是分支多不多,而是它出现在 SELECT、WHERE 还是 JOIN 条件里,以及它里面的字段是否被函数包裹。
5.1 出现在 SELECT 列表时,成本通常可以接受
如果是把 SELECT 列表里的字段做翻译或分档,CASE WHEN 只影响输出层,不会改变表的扫描方式。PostgreSQL 仍然可以走原来的索引扫描或者顺序扫描,额外的 CPU 消耗主要来自每行计算 CASE 分支。只要分支数量不是夸张到几十上百个,这种消耗对绝大多数报表查询来说可以忽略。
真正需要注意的是别在 WHERE 里用 CASE WHEN 去“包住”本来能走索引的字段。举个例子,你想根据订单类型分别过滤金额:
sql复制-- 不推荐:CASE 把过滤逻辑变成一个整体表达式
SELECT *
FROM orders
WHERE CASE
WHEN category = 'VIP' THEN amount >= 10000
ELSE amount >= 2000
END;
这段看起来没毛病,但你很难让索引直接作用于这个复合表达式。PostgreSQL 要算出 CASE 的结果才能决定是否保留某行,扫描路径大概率就是全表扫。
而如果改写成布尔逻辑:
sql复制SELECT *
FROM orders
WHERE (category = 'VIP' AND amount >= 10000)
OR (category <> 'VIP' AND amount >= 2000);
优化器有机会把 category = 'VIP' 和 category <> 'VIP' 拆解成不同的分支,配合位图扫描等策略,比包一层 CASE 要友好得多。所以我的建议是:能用普通布尔表达式描述的过滤条件,就别强行写成 CASE WHEN 放进 WHERE。
5.2 可以给 CASE 表达式建索引吗
可以。PostgreSQL 支持在表达式上建索引,所以如果某个字段经常要按 CASE 分支后的结果做过滤,是可以把 CASE 表达式直接放进索引里的。比如一个订单表里很多状态码为空,查询经常要把空状态翻译成“UNKNOWN”再分组:
sql复制CREATE INDEX idx_orders_status_normalized
ON orders ((CASE WHEN status IS NULL THEN 'UNKNOWN' ELSE status END));
SELECT status_normalized, count(*)
FROM (
SELECT
CASE WHEN status IS NULL THEN 'UNKNOWN' ELSE status END AS status_normalized
FROM orders
) t
GROUP BY status_normalized;
当然,这种索引的维护成本会比普通索引高,因为每一行写入时 PG 都要计算一次 CASE。实际业务中还是应该先看瓶颈在哪里,不要为了“炫技”盲目创建表达式索引。
5.3 少在 CASE 分支里放相关子查询
CASE WHEN 的每个 THEN 也可以是子查询,但你要警惕这条子查询是否会对每一行重复执行。比如想给员工查询时附带一个“本部门平均薪资”:
sql复制SELECT
e.name,
CASE
WHEN e.salary > (
SELECT avg(salary) FROM employees d WHERE d.department_id = e.department_id
) THEN '高于部门均值'
ELSE '低于或等于部门均值'
END AS salary_compare
FROM employees e;
这个写法在员工表几百行时感觉不到问题,但一旦表到几十万行,相关子查询可能会被反复执行,性能会很差。更稳妥的做法是用窗口函数或者先聚合出部门均值,再 JOIN 回去:
sql复制WITH dept_avg AS (
SELECT department_id, avg(salary) AS avg_salary
FROM employees
GROUP BY department_id
)
SELECT
e.name,
CASE
WHEN e.salary > d.avg_salary THEN '高于部门均值'
ELSE '低于或等于部门均值'
END AS salary_compare
FROM employees e
LEFT JOIN dept_avg d ON e.department_id = d.department_id;
使用 CASE 本身的短路特性是好的,但不要让子查询成为短路的牺牲品。如果子查询只是用来取一个不会变化的配置值,尽量先 CTE 或 JOIN 算好,不要在每一行 CASE 分支里重复寻找。
6. 列类型、NULL 与分支结构:实际排雷经验
最后这部分是我最想分享的,因为很多 SQL 写出来不报错,但结果就是不对。一个合格的数据库开发,应该能在写 CASE WHEN 时就预判到类型、NULL、顺序可能埋下的雷。
6.1 各分支返回类型不一致:隐式转换会坑你
CASE WHEN 的所有 THEN/ELSE 返回值最终要统一成一个数据类型。PostgreSQL 会尝试做隐式转换,但它不是万能的。比如:
sql复制SELECT
user_id,
CASE
WHEN is_vip = 1 THEN price * 0.8
ELSE '非会员原价'
END AS final_price
FROM orders;
这段 SQL 很可能直接报错,因为 price * 0.8 是数值类型,而 '非会员原价' 是文本类型,PostgreSQL 找不到一个不丢失信息的公共类型。更隐蔽的情况是两个分支都能转成同一种类型,但结果会超出你的预期。
所以我给自己定了一个习惯:凡是 CASE 分支里出现不同类型,务必显式写 CAST,不让 PG 自己猜。比如金额计算的分支统一转成 numeric,文本分支统一转成 text:
sql复制CASE
WHEN is_vip = 1 THEN (price * 0.8)::numeric(10,2)
ELSE 0::numeric(10,2)
END AS final_price
这样既避免了类型推断带来的歧义,也方便下游程序统一处理。
6.2 分支里写 字段 = NULL 永远为 false
这是很多新手都踩过的坑。SQL 里的 NULL 不等同于空字符串,也不等同于 0,它表示“未知”。任何 字段 = NULL 的结果都不是 TRUE,也不是 FALSE,而是 UNKNOWN。CASE WHEN 只认 TRUE,所以 WHEN state = NULL 永远不会命中。
如果你要判断字段是否为 NULL,必须用 IS NULL:
sql复制CASE
WHEN state IS NULL THEN '未填写'
ELSE state
END
还有一点值得提醒:NULL 值在简单 CASE 里也有类似问题。前面提到过,CASE state WHEN NULL THEN ... 不会命中,原因是它会被解释成 state = NULL。所以处理 NULL 时,搜索 CASE + IS NULL 才是正路。
6.3 区间判断的顺序错位会导致数据被错误归类
前面的年龄分档例子已经展示了顺序问题。我再给一个实际报表里经常出现的例子:工资区间统计。假设你要把月薪分为 5000 以下、5000-10000、10000-20000、20000 以上 四档:
sql复制CASE
WHEN salary < 5000 THEN '5000以下'
WHEN salary < 10000 THEN '5000-10000'
WHEN salary < 20000 THEN '10000-20000'
ELSE '20000以上'
END
这种从小到大排列的分支顺序是安全的,每个分支天然互斥。但如果你把 salary < 20000 放在 salary < 10000 前面,那月薪 8000 的人会先被归到 10000-20000,统计结果彻底歪掉。所以我写区间分档时,习惯从低到高或从高到低严格排序,而不是随意排列。
另外还要注意边界是否含等号。salary <= 10000 和 salary < 10000 差一个等号,统计口径就可能差出整批人。最好在代码注释里写明区间的开闭方式。
6.4 除法运算里的保护:把 CASE WHEN 当成安全的短路开关
PostgreSQL 的 CASE WHEN 有一个特性经常被忽略:没有命中的分支,其 THEN 表达式不会被真正求值。这意味着你可以用它来避免“除零”错误。
比如计算某商品打折后的折扣率,折扣率可能为 0,直接用 100 / discount_rate 会报 division by zero。包一层 CASE:
sql复制SELECT
product_name,
CASE
WHEN discount_rate = 0 THEN NULL
ELSE 100 / discount_rate
END AS discount_percent
FROM products;
当 discount_rate = 0 时,第一个分支命中,返回 NULL,ELSE 里的除法根本不会执行。这就是一种安全短路。类似的技巧还能用在防止字符串转数字失败、防止日期函数收到非法值等场景。只要某个表达式在特定输入下会抛错,就可以用 WHEN 条件先做拦截。
但要注意,WHEN 条件本身的执行顺序并不像 THEN 那样有严格的“安全网”保证。不要写类似 WHEN x <> 0 AND y / x > 1 这种依赖执行顺序来防除零的代码,因为一旦优化器调整了条件的评估顺序,仍然可能爆炸。最好把除法放到 THEN 分支里做。
6.5 不能在同一层 SELECT 里直接引用 CASE 别名
这个坑几乎每个人都遇到过。你在 SELECT 列表里给 CASE WHEN 起了个别名叫 level,想在 WHERE 里过滤 level = 'A',结果 PostgreSQL 直接报错:column "level" does not exist。原因是 SQL 的查询逻辑顺序里,SELECT 列表的别名在 WHERE 阶段还不存在,要到 ORDER BY 阶段才可见。
正确的做法是包一层子查询,或者用 CTE:
sql复制WITH user_level AS (
SELECT
user_id,
CASE
WHEN score >= 90 THEN 'A'
WHEN score >= 80 THEN 'B'
ELSE 'C'
END AS level
FROM exam_scores
)
SELECT *
FROM user_level
WHERE level = 'A';
这个写法不仅解决了别名问题,还让复杂的 CASE 表达式只写一次,后续可以反复引用。我写多步报表 SQL 时,特别推荐这种“先用 CASE 生成派生列,再在外部过滤/聚合”的套路,代码清晰,也方便调试中间结果。
从我个人习惯来说,CASE WHEN 用得好不好,不取决于你背了多少语法,而取决于你能不能判断“这个表达式最终要生成一个什么类型的值”“NULL 会不会影响统计”“分支顺序会不会导致某个区间永远走不到”。只要你写每个 CASE 之前都花十秒钟想这三个问题,踩坑概率会小非常多。最后再分享一个小经验:写长 SQL 时尽量把 CASE WHEN 的派生结果往外层提,不要在一个大查询里重复粘贴同一段 CASE 逻辑,否则后期改业务口径时,你可能会为了找一个漏改的分支头发掉光。
