SQL 数据分析 · 指南 · 2026年10月30日 · 13 分钟阅读

SQL CASE WHEN 用法:条件判断、分组归类与条件聚合

机场里的指示牌根据旅客的机票把他们引向不同的登机口

CASE 就是 SQL 里的 if-then-else。它逐行检查条件,按顺序判断,返回第一个为真的条件对应的值。如果一个都不成立,就返回 ELSE 的值;没有 ELSE 时返回 NULL:

CASE
  WHEN condition_1 THEN value_1
  WHEN condition_2 THEN value_2
  ELSE fallback_value
END

CASE 是表达式,不是语句,所以凡是能放值的地方都能放它:SELECT、WHERE、GROUP BY、ORDER BY,以及聚合函数内部。最后这种用法叫条件聚合,是 CASE 对分析师最有价值的地方。

下面的例子都在 Nomto SQL 课程使用的航空公司数据库上运行:2026 年夏季 104 个机场之间的 33,121 个航班,以及订单、机票、座位和会员计划数据,金额单位为美元。SQL 采用 PostgreSQL 语法,每个结果都是真实运行得到的。

搜索 CASE:任意条件

最常见的写法叫搜索 CASE(searched CASE),每个 WHEN 后面跟一个完整的条件。把机队按航程分类:

SELECT model, range,
       CASE
         WHEN range >= 6000 THEN 'long-haul'
         WHEN range >= 3000 THEN 'medium-haul'
         ELSE 'short-haul'
       END AS category
FROM aircrafts
ORDER BY range DESC;
model range category
Boeing 777-300 11100 long-haul
Boeing 767-300 7900 long-haul
Airbus A319-100 6700 long-haul
Airbus A320-200 5700 medium-haul
Airbus A321-200 5600 medium-haul
Boeing 737-300 4200 medium-haul
Sukhoi Superjet-100 3000 medium-haul
Bombardier CRJ-200 2700 short-haul
Cessna 208 Caravan 1200 short-haul

注意第二个分支只写了 range >= 3000,而不是 range >= 3000 AND range < 6000。它不需要上限,因为 6,000 及以上的行已经被第一个分支拿走了。这正是 CASE 能工作的规则:分支从上到下依次检查,第一个匹配的生效。

简单 CASE:拿一个表达式和若干值比较

如果每个分支都是拿同一个表达式和常量比较,用简单 CASE(simple CASE)更短:

SELECT aircraft_code, model,
       CASE aircraft_code
         WHEN 'SU9' THEN 'Sukhoi'
         WHEN 'CR2' THEN 'Bombardier'
         WHEN 'CN1' THEN 'Cessna'
         ELSE split_part(model, ' ', 1)
       END AS maker
FROM aircrafts
ORDER BY aircraft_code;
aircraft_code model maker
319 Airbus A319-100 Airbus
320 Airbus A320-200 Airbus
321 Airbus A321-200 Airbus
733 Boeing 737-300 Boeing
763 Boeing 767-300 Boeing
773 Boeing 777-300 Boeing
CN1 Cessna 208 Caravan Cessna
CR2 Bombardier CRJ-200 Bombardier
SU9 Sukhoi Superjet-100 Sukhoi

CASE aircraft_code WHEN 'SU9' 的意思就是 CASE WHEN aircraft_code = 'SU9'。返回值也可以是表达式:这里的 ELSE 取机型名称的第一个单词。(在这个例子里,每个分支碰巧都等于第一个单词;在真实数据里,显式写出的分支就是用来放例外情况的。)简单 CASE 只能判断相等,所以要判断范围、LIKE、IN 或 NULL,就得用搜索 CASE。

把数值分档归类

把连续的数值切成几个区间,是 CASE 的日常工作。航段价格是怎么分布的?

SELECT CASE
         WHEN amount < 100 THEN 'under $100'
         WHEN amount < 300 THEN '$100-299'
         WHEN amount < 600 THEN '$300-599'
         ELSE '$600+'
       END AS price_band,
       COUNT(*) AS segments
FROM ticket_flights
GROUP BY price_band
ORDER BY MIN(amount);
price_band segments
under $100 359344
$100-299 525159
$300-599 93383
$600+ 67840

这里有两个小技巧。按别名 price_band 分组在 PostgreSQL 和 DuckDB 里都可以(严格的标准 SQL 会要求你把整个 CASE 再写一遍,或者用位置写 GROUP BY 1)。ORDER BY MIN(amount) 让各档按真实的大小顺序排列,而不是按字母顺序,否则 $100-299 会排在 under $100 前面。

同样的思路也适用于时间间隔。已到达航班起飞时晚了多少?

SELECT CASE
         WHEN actual_departure - scheduled_departure <= INTERVAL '5 minutes' THEN 'up to 5 min'
         WHEN actual_departure - scheduled_departure <= INTERVAL '15 minutes' THEN '6-15 min'
         WHEN actual_departure - scheduled_departure <= INTERVAL '3 hours' THEN '15 min to 3 h'
         ELSE 'over 3 h'
       END AS delay_group,
       COUNT(*) AS flights
FROM flights
WHERE status = 'Arrived'
GROUP BY delay_group
ORDER BY MIN(actual_departure - scheduled_departure);
delay_group flights
up to 5 min 14657
6-15 min 1254
15 min to 3 h 209
over 3 h 587

第一个匹配生效:分支顺序很重要

分支顺序写错,某个类别就会悄悄消失:

SELECT CASE
         WHEN actual_departure - scheduled_departure > INTERVAL '15 minutes' THEN 'late'
         WHEN actual_departure - scheduled_departure > INTERVAL '3 hours' THEN 'very late'
         ELSE 'on time'
       END AS delay_group,
       COUNT(*) AS flights
FROM flights
WHERE status = 'Arrived'
GROUP BY delay_group
ORDER BY flights DESC;
delay_group flights
on time 15911
late 796

每个晚点超过三小时的航班,也都晚点超过 15 分钟,所以第一个分支把它拿走了,very late 永远不会触发。没有报错,也没有警告,只是一个零行的类别,没人发现它不见了。用「大于」条件时,把最大的阈值放在最前面;用「小于」条件时,把最小的阈值放在最前面,就像上面的价格分档那样。

CASE 和 NULL

CASE 遇到 NULL 时,有两条规则常让人栽跟头。

条件为 NULL 时按假处理,这一行会落到 ELSE。会员计划里有些会员没有填 birth_year。看看下面会发生什么:

SELECT CASE
         WHEN birth_year < 1980 THEN 'born before 1980'
         ELSE 'born 1980 or later'
       END AS age_group,
       COUNT(*) AS members
FROM loyalty_members
GROUP BY age_group
ORDER BY age_group;
age_group members
born 1980 or later 35666
born before 1980 17334

6,319 名没有出生年份的会员被标成了「born 1980 or later」。要给 NULL 单独一个分支,并且放在最前面:

SELECT CASE
         WHEN birth_year IS NULL THEN 'unknown'
         WHEN birth_year < 1980 THEN 'born before 1980'
         ELSE 'born 1980 or later'
       END AS age_group,
       COUNT(*) AS members
FROM loyalty_members
GROUP BY age_group
ORDER BY age_group;
age_group members
born 1980 or later 29347
born before 1980 17334
unknown 6319

简单 CASE 匹配不到 NULL。 CASE birth_year WHEN NULL THEN 'unknown' ELSE 'known' END 会把全部 53,000 名会员都标成「known」,因为它判断的是 birth_year = NULL,而这永远不会为真。要用搜索 CASE 写 WHEN birth_year IS NULL。

如果省略 ELSE,没有匹配的行得到 NULL:

SELECT model,
       CASE
         WHEN range >= 6000 THEN 'long-haul'
         WHEN range >= 3000 THEN 'medium-haul'
       END AS category
FROM aircrafts
WHERE range < 3000;
model category
Cessna 208 Caravan NULL
Bombardier CRJ-200 NULL

有时这正是你想要的(下面的 AVG 技巧就靠它)。不想要时,显式写一个 ELSE 'other',空缺就会显露出来。更多 NULL 的行为见 SQL NULL 值处理。

ORDER BY 里的 CASE:自定义排序

字母顺序很少符合业务上的顺序。航班状态有一个自然的生命周期:

SELECT status, COUNT(*) AS flights
FROM flights
GROUP BY status
ORDER BY CASE status
           WHEN 'Scheduled' THEN 1
           WHEN 'On Time' THEN 2
           WHEN 'Delayed' THEN 3
           WHEN 'Departed' THEN 4
           WHEN 'Arrived' THEN 5
           ELSE 6
         END;
status flights
Scheduled 15383
On Time 518
Delayed 41
Departed 58
Arrived 16707
Cancelled 414

这个 CASE 返回的数字只用来排序,不会出现在输出里。ELSE 6 把任何意料之外的值(这里是 Cancelled)排到最后,而不会打乱顺序。

GROUP BY 里的 CASE:自定义分组

按 CASE 分组,可以造出表里本来没有的分组。周末每天的航班比工作日多吗?

SELECT CASE
         WHEN EXTRACT(ISODOW FROM scheduled_departure) IN (6, 7) THEN 'weekend'
         ELSE 'weekday'
       END AS day_type,
       COUNT(*) AS flights,
       COUNT(DISTINCT CAST(scheduled_departure AS date)) AS days,
       ROUND(COUNT(*) * 1.0 / COUNT(DISTINCT CAST(scheduled_departure AS date)), 1) AS flights_per_day
FROM flights
GROUP BY day_type
ORDER BY day_type;
day_type flights days flights_per_day
weekday 23555 44 535.3
weekend 9566 17 562.7

直接比较总数(23,555 对 9,566)毫无意义,因为工作日多得多。除以每组的天数才是公平的比较:周末大约忙 5%。

条件聚合

把 CASE 放进聚合函数里,就能在一次扫描中算出多个带筛选条件的指标,每个指标一列。这是分析工作里最有用的 CASE 写法。

用 SUM(CASE … THEN 1 ELSE 0 END) 计数

几个大机场各取消了多少航班,占比多少?

SELECT departure_airport,
       COUNT(*) AS flights,
       SUM(CASE WHEN status = 'Cancelled' THEN 1 ELSE 0 END) AS cancelled,
       ROUND(100.0 * SUM(CASE WHEN status = 'Cancelled' THEN 1 ELSE 0 END) / COUNT(*), 2) AS cancel_pct
FROM flights
WHERE departure_airport IN ('SVO', 'DME', 'VKO', 'LED')
GROUP BY departure_airport
ORDER BY departure_airport;
departure_airport flights cancelled cancel_pct
DME 3217 31 0.96
LED 1900 15 0.79
SVO 2981 26 0.87
VKO 1719 20 1.16

WHERE status = 'Cancelled' 做不到这一点:它会把其他航班扔掉,分母也跟着没了。条件聚合保留所有行,只数你关心的那些。注意这里写的是 100.0:如果写 100,计数又是整数,PostgreSQL 会做整数除法,返回 0。DuckDB 的 / 总是返回小数,所以同样的错误在 DuckDB 里看不出来,要到在 PostgreSQL 上运行时才暴露;DuckDB 的整数除法用 //。

条件计数有三种等价写法,还有一种很诱人的错误写法:

SELECT COUNT(*) AS arrived,
       SUM(CASE WHEN actual_departure - scheduled_departure > INTERVAL '15 minutes' THEN 1 ELSE 0 END) AS late,
       COUNT(CASE WHEN actual_departure - scheduled_departure > INTERVAL '15 minutes' THEN 1 END) AS late_count,
       ROUND(AVG(CASE WHEN actual_departure - scheduled_departure > INTERVAL '15 minutes' THEN 1.0 ELSE 0 END) * 100, 2) AS late_pct
FROM flights
WHERE status = 'Arrived';
arrived late late_count late_pct
16707 796 796 4.76

错误的写法是 COUNT(CASE ... THEN 1 ELSE 0 END)。0 不是 NULL,COUNT 会把它也数进去,结果是 16,707,也就是所有行。用 COUNT 就不要写 ELSE;用 SUM 就保留 ELSE 0。

FILTER:更简洁的语法

PostgreSQL 和 DuckDB 支持标准的 FILTER 子句,表达同样的意思更直接。每种机型的座位分布,每个舱位一列:

SELECT aircraft_code,
       COUNT(*) FILTER (WHERE fare_conditions = 'Economy') AS economy,
       COUNT(*) FILTER (WHERE fare_conditions = 'Comfort') AS comfort,
       COUNT(*) FILTER (WHERE fare_conditions = 'Business') AS business
FROM seats
GROUP BY aircraft_code
ORDER BY aircraft_code;
aircraft_code economy comfort business
319 96 0 20
320 120 0 20
321 142 0 28
733 118 0 12
763 192 0 30
773 324 48 30
CN1 12 0 0
CR2 50 0 0
SU9 85 0 12

这就是行转列(pivot):把 fare_conditions 的各行变成了列。MySQL、SQL Server 和 Oracle 不支持 FILTER,如果你的代码要在这些数据库上运行,就用 SUM(CASE ...),它在哪里都能用。

不只计数,还能求和

SUM 里的 CASE 可以返回值本身,而不是 1。按舱位拆分已到达航班的月度收入:

SELECT date_trunc('month', f.scheduled_departure)::date AS month,
       ROUND(SUM(CASE WHEN tf.fare_conditions = 'Economy' THEN tf.amount ELSE 0 END)) AS economy,
       ROUND(SUM(CASE WHEN tf.fare_conditions = 'Comfort' THEN tf.amount ELSE 0 END)) AS comfort,
       ROUND(SUM(CASE WHEN tf.fare_conditions = 'Business' THEN tf.amount ELSE 0 END)) AS business
FROM ticket_flights tf
JOIN flights f ON f.flight_id = tf.flight_id
WHERE f.status = 'Arrived'
GROUP BY month
ORDER BY month;
month economy comfort business
2026-07-01 47444895 1899027 17733606
2026-08-01 33400255 1327303 12473544

(8 月偏低,是因为数据快照截止到 8 月 11 日,当月剩下的航班还没飞。)

而不写 ELSE 时,AVG 只对匹配的行求平均,因为其他行是 NULL,聚合函数会跳过 NULL:

SELECT fare_conditions,
       ROUND(SUM(amount)) AS revenue,
       ROUND(SUM(CASE WHEN amount >= 600 THEN amount ELSE 0 END)) AS revenue_600plus,
       ROUND(AVG(CASE WHEN amount >= 600 THEN amount END), 2) AS avg_600plus
FROM ticket_flights
GROUP BY fare_conditions
ORDER BY revenue DESC;
fare_conditions revenue revenue_600plus avg_600plus
Economy 146956844 26449215 634.9
Business 55051796 31703523 1210.94
Comfort 5661169 0 NULL

在这里,ELSE 0 和不写 ELSE 给出的结果不同,但都是对的:SUM 需要 0,这样总和才是 0 而不是 NULL;AVG 则不能把 0 算进去,否则就成了对所有航段求平均。舒适舱没有 600 美元及以上的航段,所以它的平均值是 NULL:没有东西可以求平均。

类型:每个分支必须返回同一种类型

所有 THEN 和 ELSE 的值必须能归入同一种数据类型。CASE WHEN ... THEN 1 ELSE 'none' END 把整数和文本混在一起。PostgreSQL 会报错「invalid input syntax for type integer」。DuckDB 也会因类型转换出错,但只在某一行真的走到 'none' 分支时才报错,所以查询可能在小样本上测试通过,之后才出问题。要显式转换类型:THEN CAST(x AS VARCHAR),或者返回 NULL 而不是占位字符串。

WHERE 里的 CASE:通常用不着

你可以写 WHERE CASE WHEN ... THEN ... END,但筛选条件几乎总是用普通的 AND / OR 更好读。WHERE CASE WHEN status = 'Arrived' THEN actual_departure > '2026-08-01' ELSE false END 其实就是 WHERE status = 'Arrived' AND actual_departure > '2026-08-01'。普通条件也更方便优化器利用索引。

相关的简写

更多用到条件计数的 GROUP BY 写法,见 GROUP BY 和 HAVING 的区别。用条件聚合做行转列也是 SQL 面试题里的常客。

常见问题

简单 CASE 和搜索 CASE 有什么区别?

简单 CASE CASE x WHEN 1 THEN ... 拿一个表达式和若干值做相等比较。搜索 CASE CASE WHEN condition THEN ... 可以接任意条件:范围、LIKE、IN、IS NULL,以及用 AND 组合的条件。简单 CASE 能做的,搜索 CASE 都能做。

没有任何条件匹配时,CASE 返回什么?

返回 ELSE 的值;没有 ELSE 时返回 NULL。

WHERE 子句里能用 CASE 吗?

能,但很少是最清楚的写法。WHERE 里的大多数 CASE 表达式都可以改写成普通的 AND / OR 条件。

SQL 怎么统计满足某个条件的行数?

用条件聚合:SUM(CASE WHEN condition THEN 1 ELSE 0 END)、COUNT(CASE WHEN condition THEN 1 END),或者在 PostgreSQL 和 DuckDB 里用 COUNT(*) FILTER (WHERE condition)。三种写法都保留了其他行,方便计算总数和百分比。

CASE 可以嵌套吗?

可以,THEN 或 ELSE 里可以再放一个 CASE。不过通常用 AND 组合条件的单个 CASE,比嵌套更好读。

其他语言: English, Español, Português (Brasil), Tiếng Việt