CASE WHEN 表达式
CASE 是 SQL 中唯一的标准条件表达式,用来在查询中实现「分支判断」:把不同条件映射为不同结果。它既能做行级标签(如按价格分档),也能做列级透视(行转列),还能与聚合函数组合实现条件统计。由于 CASE 是表达式而非语句,它可以出现在 SELECT、WHERE、ORDER BY、GROUP BY、UPDATE SET 等任何需要值的位置。
前置知识
阅读本节前,建议先了解:BETWEEN / IN / ANY / ALL
基础概念
| 概念 | 术语 | 说明 |
|---|---|---|
| 条件表达式 | CASE Expression | 按条件返回不同值的表达式,分简单 CASE 与搜索 CASE |
| 简单 CASE | Simple CASE | 形式为 CASE expr WHEN v THEN ...,对单一表达式做等值比较 |
| 搜索 CASE | Searched CASE | 形式为 CASE WHEN condition THEN ...,每个分支可独立写条件 |
| 分支 | WHEN / THEN | 一个条件与对应结果;自上而下依次判断,命中即返回 |
| 兜底分支 | ELSE | 所有 WHEN 都不命中时的默认结果;省略则返回 NULL |
| 条件聚合 | Conditional Aggregation | 在聚合函数内部用 CASE 只统计满足条件的行 |
| 过滤聚合 | FILTER 子句 | count(*) FILTER (WHERE ...),条件聚合的更清晰写法 |
| 行转列 | Pivot | 用 CASE + 聚合把行数据转成列,实现透视表 |
| 分桶 | Bucketing | 用 CASE 把连续数值映射到离散区间 |
| 惰性求值 | Lazy Evaluation | 分支自左至右短路,命中后不再计算后续分支 |
CASE 的关键特性:
- 返回单一类型。所有
THEN/ELSE分支的结果类型必须能归一到同一个类型,否则需要显式转换。 - 自上而下短路。先命中的分支直接返回,后续分支不再求值,因此分支顺序会影响结果。
- 没有
ELSE时默认 NULL。忘记写ELSE会静默产出 NULL,是最常见的疏漏。
-- 两种 CASE 形式的最小对照
SELECT
CASE 2 WHEN 1 THEN '一' WHEN 2 THEN '二' ELSE '其他' END AS simple_case, -- 二
CASE WHEN 2 > 1 THEN '大' WHEN 2 = 1 THEN '等' ELSE '小' END AS searched_case; -- 大语法与用法
-- 简单 CASE:对同一个表达式做多次等值比较
CASE 表达式
WHEN 匹配值1 THEN 结果1
WHEN 匹配值2 THEN 结果2
ELSE 默认结果
END
-- 搜索 CASE:每个分支可写独立的布尔条件
CASE
WHEN 条件1 THEN 结果1
WHEN 条件2 THEN 结果2
ELSE 默认结果
END-- 最小可运行示例
SELECT
CASE 'a'
WHEN 'a' THEN '第一名'
WHEN 'b' THEN '第二名'
ELSE '未上榜'
END AS rank_label,
CASE
WHEN 95 >= 90 THEN 'A'
WHEN 95 >= 80 THEN 'B'
ELSE 'C'
END AS grade;详细说明
简单 CASE 与搜索 CASE 的区别
两者最大的区别在于「比较的对象」:
- 简单
CASE只做等值比较,无法表达范围或复合条件,且匹配值与表达式都只求值一次。 - 搜索
CASE每个分支都能写完整布尔条件,表达力更强。
-- 简单 CASE:只能做等值比较
SELECT sku,
CASE category
WHEN '键盘' THEN '输入设备'
WHEN '鼠标' THEN '输入设备'
ELSE '其他'
END AS category_group
FROM products;
-- 搜索 CASE:可以做范围与复合条件
SELECT sku,
CASE
WHEN price IS NULL THEN '待定价'
WHEN price >= 1000 THEN '高端'
WHEN price >= 100 THEN '中端'
ELSE '入门'
END AS price_tier
FROM products;注意一个陷阱:简单 CASE 的 WHEN NULL 永远不成立,因为它使用 = 比较。要判断 NULL 必须用搜索 CASE。
-- 错误!NULL 不会匹配 WHEN NULL
SELECT CASE price WHEN NULL THEN '无价' ELSE '有价' END FROM products;
-- 正确:改用搜索 CASE + IS NULL
SELECT CASE WHEN price IS NULL THEN '无价' ELSE '有价' END FROM products;分支顺序决定结果
CASE 自上而下短路求值,因此「宽泛条件写在前面」会吞掉后面更具体的分支。
-- 错误!>= 0 会吞掉所有非负值,>= 100 的分支永远不会命中
SELECT CASE
WHEN stock >= 0 THEN '有货'
WHEN stock >= 100 THEN '充足'
ELSE '缺货'
END AS stock_label
FROM products;
-- 正确:从最严格的条件开始写
SELECT CASE
WHEN stock >= 100 THEN '充足'
WHEN stock > 0 THEN '有货'
ELSE '缺货'
END AS stock_label
FROM products;结果类型统一与类型转换
所有分支必须归一到同一类型。若分支分别是整数与字符串,会报类型错误或发生隐式转换。
-- 错误!分支类型不一致
SELECT CASE WHEN true THEN 1 ELSE 'x' END;
-- 正确:显式转换
SELECT CASE WHEN true THEN 1::text ELSE 'x' END;
-- 数值分支统一精度
SELECT CASE WHEN price >= 1000 THEN price::numeric(10,2) ELSE 0.00 END FROM products;条件聚合:用 CASE 实现「按条件统计」
把 CASE 放进聚合函数里,就能只统计满足条件的行。这是 CASE 最重要的实战用法之一。
-- 一次查询得到「总数 / 有货数 / 缺货数」
SELECT
count(*) AS total,
count(CASE WHEN stock > 0 THEN 1 END) AS in_stock,
count(CASE WHEN stock = 0 THEN 1 END) AS out_of_stock
FROM products;count(CASE WHEN cond THEN 1 END) 之所以成立,是因为 CASE 在不命中时返回 NULL,而 count(col) 会跳过 NULL。
FILTER 子句:条件聚合的现代写法
PostgreSQL 9.4+ 提供 FILTER (WHERE ...) 子句,专门用于条件聚合,比 CASE 更直观,且通常更快。
-- 等价于上面的 CASE 写法,但更清晰
SELECT
count(*) AS total,
count(*) FILTER (WHERE stock > 0) AS in_stock,
count(*) FILTER (WHERE stock = 0) AS out_of_stock,
sum(amount) FILTER (WHERE qty > 1) AS bulk_amount
FROM orders;两者的选择建议:只做计数、求和这类标准聚合时优先 FILTER;需要自定义表达式(如 CASE 返回值再参与运算)时用 CASE。
行转列(Pivot)
用 CASE + GROUP BY 可以实现透视表:把某一列的不同取值变成多个列。
-- 把订单按商品类别透视成列
SELECT
date_trunc('month', o.created_at)::date AS month,
sum(CASE WHEN p.category = '显示器' THEN o.amount ELSE 0 END) AS monitor_amount,
sum(CASE WHEN p.category = '键盘' THEN o.amount ELSE 0 END) AS keyboard_amount,
sum(CASE WHEN p.category = '鼠标' THEN o.amount ELSE 0 END) AS mouse_amount
FROM orders o
JOIN products p ON p.id = o.product_id
GROUP BY 1
ORDER BY 1;注意 ELSE 0 而非省略 ELSE:省略会让不匹配的行变成 NULL,sum 忽略 NULL 后结果虽相同,但可读性差且易在 coalesce 之前出现意外。
CASE 用于排序与分组键
CASE 可以直接作为 ORDER BY 与 GROUP BY 的表达式,实现自定义排序或动态分组。
-- 自定义排序:把「缺货」排在最前,其余按价格升序
SELECT sku, name, stock, price
FROM products
ORDER BY
CASE WHEN stock = 0 THEN 0 ELSE 1 END,
price;
-- 自定义分组:按价格档位分组统计
SELECT
CASE
WHEN price IS NULL THEN '待定价'
WHEN price >= 1000 THEN '高端'
WHEN price >= 100 THEN '中端'
ELSE '入门'
END AS price_tier,
count(*) AS cnt
FROM products
GROUP BY 1
ORDER BY 1;CASE 在 UPDATE 中的批量赋值
CASE 可以让一条 UPDATE 完成「按条件赋不同值」,避免多次往返数据库。
UPDATE products
SET stock = CASE
WHEN stock = 0 THEN 10
WHEN stock < 10 THEN stock + 5
ELSE stock
END
WHERE id = ANY (ARRAY[1, 2, 3]);实战示例
步骤 1:建表并造数据
-- 统一的示例库
CREATE DATABASE demo;
DROP TABLE IF EXISTS orders;
DROP TABLE IF EXISTS products;
CREATE TABLE products (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
sku text NOT NULL UNIQUE,
name text NOT NULL,
category text,
price numeric(10, 2),
stock integer NOT NULL DEFAULT 0
);
CREATE TABLE orders (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
order_no text NOT NULL,
product_id integer,
qty integer NOT NULL,
amount numeric(10, 2),
status text,
created_at timestamp NOT NULL DEFAULT now()
);
INSERT INTO products (sku, name, category, price, stock) VALUES
('SKU-1001', '27 英寸显示器', '显示器', 1299.00, 12),
('SKU-1002', '机械键盘', '键盘', 399.00, 30),
('SKU-1003', '无线鼠标', '鼠标', 159.00, 0),
('SKU-1004', 'HDMI 线缆', '线缆', 49.00, 200),
('SKU-1005', 'USB 集线器', '配件', 89.00, 5),
('SKU-1006', '显卡支架', '配件', 129.00, 8),
('SKU-1007', '散热硅脂', '配件', NULL, 0);
INSERT INTO orders (order_no, product_id, qty, amount, status, created_at) VALUES
('SO-2001', 1, 2, 2598.00, 'paid', '2025-03-05 10:12:00'),
('SO-2002', 2, 1, 399.00, 'paid', '2025-03-18 15:30:00'),
('SO-2003', NULL, 1, 159.00, 'pending', '2025-03-31 23:59:00'),
('SO-2004', 3, 3, 477.00, 'shipped', '2025-04-01 08:00:00'),
('SO-2005', NULL, 2, 178.00, 'cancelled','2025-04-20 12:00:00'),
('SO-2006', 4, 5, 245.00, 'paid', '2025-05-02 09:15:00');步骤 2:搜索 CASE 做分档标签
SELECT sku, name, price,
CASE
WHEN price IS NULL THEN '待定价'
WHEN price >= 1000 THEN '高端'
WHEN price >= 100 THEN '中端'
ELSE '入门'
END AS price_tier
FROM products
ORDER BY sku; sku | name | price | price_tier
----------+-------------+---------+------------
SKU-1001 | 27 英寸显示器 | 1299.00 | 高端
SKU-1002 | 机械键盘 | 399.00 | 中端
SKU-1003 | 无线鼠标 | 159.00 | 中端
SKU-1004 | HDMI 线缆 | 49.00 | 入门
SKU-1005 | USB 集线器 | 89.00 | 入门
SKU-1006 | 显卡支架 | 129.00 | 中端
SKU-1007 | 散热硅脂 | | 待定价
(7 rows)步骤 3:条件聚合统计库存状态
SELECT
count(*) AS total,
count(*) FILTER (WHERE stock >= 100) AS ample,
count(*) FILTER (WHERE stock > 0 AND stock < 100) AS normal,
count(*) FILTER (WHERE stock = 0) AS out_of_stock
FROM products; total | ample | normal | out_of_stock
-------+-------+--------+--------------
7 | 1 | 4 | 2
(1 row)-- 用 CASE 写等价查询(兼容不支持 FILTER 的场景)
SELECT
count(*) AS total,
count(CASE WHEN stock >= 100 THEN 1 END) AS ample,
count(CASE WHEN stock > 0 AND stock < 100 THEN 1 END) AS normal,
count(CASE WHEN stock = 0 THEN 1 END) AS out_of_stock
FROM products; total | ample | normal | out_of_stock
-------+-------+--------+--------------
7 | 1 | 4 | 2
(1 row)步骤 4:行转列做订单状态透视
SELECT
date_trunc('month', created_at)::date AS month,
sum(CASE WHEN status = 'paid' THEN amount ELSE 0 END) AS paid_amount,
sum(CASE WHEN status = 'shipped' THEN amount ELSE 0 END) AS shipped_amount,
sum(CASE WHEN status = 'pending' THEN amount ELSE 0 END) AS pending_amount,
sum(CASE WHEN status = 'cancelled' THEN amount ELSE 0 END) AS cancelled_amount
FROM orders
GROUP BY 1
ORDER BY 1; month | paid_amount | shipped_amount | pending_amount | cancelled_amount
------------+-------------+----------------+----------------+------------------
2025-03-01 | 2997.00 | 0.00 | 159.00 | 0.00
2025-04-01 | 0.00 | 477.00 | 0.00 | 178.00
2025-05-01 | 245.00 | 0.00 | 0.00 | 0.00
(3 rows)步骤 5:自定义排序与分组
-- 缺货商品排最前,其余按价格升序
SELECT sku, name, stock, price
FROM products
ORDER BY
CASE WHEN stock = 0 THEN 0 ELSE 1 END,
price NULLS LAST; sku | name | stock | price
----------+-------------+-------+---------
SKU-1003 | 无线鼠标 | 0 | 159.00
SKU-1007 | 散热硅脂 | 0 |
SKU-1004 | HDMI 线缆 | 200 | 49.00
SKU-1005 | USB 集线器 | 5 | 89.00
SKU-1006 | 显卡支架 | 8 | 129.00
SKU-1002 | 机械键盘 | 30 | 399.00
SKU-1001 | 27 英寸显示器 | 12 | 1299.00
(7 rows)-- 按价格档位分组统计
SELECT
CASE
WHEN price IS NULL THEN '待定价'
WHEN price >= 1000 THEN '高端'
WHEN price >= 100 THEN '中端'
ELSE '入门'
END AS price_tier,
count(*) AS cnt,
min(price) AS min_price,
max(price) AS max_price
FROM products
GROUP BY 1
ORDER BY 1; price_tier | cnt | min_price | max_price
------------+-----+-----------+-----------
中端 | 3 | 129.00 | 399.00
入门 | 2 | 49.00 | 89.00
待定价 | 1 | |
高端 | 1 | 1299.00 | 1299.00
(4 rows)步骤 6:CASE 在 UPDATE 中批量赋值
-- 一次 UPDATE 完成不同商品的补货策略
BEGIN;
UPDATE products
SET stock = CASE
WHEN stock = 0 THEN 10
WHEN stock < 10 THEN stock + 5
ELSE stock
END
WHERE category = '配件';
SELECT sku, name, stock FROM products WHERE category = '配件' ORDER BY sku;
ROLLBACK; -- 仅演示,回滚以保持数据一致 sku | name | stock
----------+------------+-------
SKU-1005 | USB 集线器 | 10
SKU-1006 | 显卡支架 | 13
SKU-1007 | 散热硅脂 | 10
(3 rows)注意事项
- 忘记写
ELSE会静默返回 NULL。 只要分支可能不覆盖全部输入,就应显式写出ELSE。
-- 不推荐:不命中时返回 NULL
CASE WHEN price >= 1000 THEN '高端' END
-- 推荐
CASE WHEN price >= 1000 THEN '高端' ELSE '其他' END- 简单
CASE的WHEN NULL永远不成立。 它使用等值比较,判 NULL 必须改用搜索CASE。
-- 错误!
CASE price WHEN NULL THEN '无价' ELSE '有价' END
-- 正确
CASE WHEN price IS NULL THEN '无价' ELSE '有价' END- 分支顺序错误会吞掉后续条件。 宽泛条件写在具体条件之前,后面的分支永远不会命中。
-- 错误!>= 0 覆盖了所有非负值
CASE WHEN stock >= 0 THEN '有货' WHEN stock >= 100 THEN '充足' END所有分支结果类型必须一致。 混用数字与字符串要么报错,要么发生隐式转换导致意外结果。
CASE不是语句,不能用于流程控制。 它只返回值,不能执行INSERT/UPDATE等动作;过程化逻辑需用PL/pgSQL的IF。CASE会阻止部分索引优化。 若在WHERE中对列使用CASE,该列索引通常失效,应改写为OR条件。
-- 不推荐:索引失效
WHERE CASE WHEN flag THEN a ELSE b END = 1
-- 推荐:拆成 OR,优化器可分别走索引
WHERE (flag AND a = 1) OR (NOT flag AND b = 1)CASE中的子查询会按行求值。 在大表上可能造成性能问题,优先用JOIN或LEFT JOIN代替。count(CASE WHEN ... THEN 1 END)中的1不能省略。 写成THEN NULL END就等价于不计数。
最佳实践
- 条件聚合优先用
FILTER (WHERE ...),它比CASE更直观,且优化器处理更高效。
SELECT
count(*) FILTER (WHERE status = 'paid') AS paid_cnt,
sum(amount) FILTER (WHERE status = 'paid') AS paid_sum
FROM orders;CASE分支从最严格写到最宽泛,利用短路求值保证逻辑正确。
CASE
WHEN price >= 1000 THEN '高端'
WHEN price >= 100 THEN '中端'
ELSE '入门'
END始终显式写
ELSE,即便是ELSE NULL,也让意图明确、便于排查。行转列时给不匹配分支写
ELSE 0,避免数字列出现 NULL 影响后续算术。
sum(CASE WHEN category = '键盘' THEN amount ELSE 0 END) AS keyboard_amount- 把分档逻辑封装为函数或视图,避免在多个查询里重复同一段
CASE,降低维护成本。
CREATE OR REPLACE FUNCTION price_tier(p numeric)
RETURNS text LANGUAGE sql IMMUTABLE AS $$
SELECT CASE
WHEN p IS NULL THEN '待定价'
WHEN p >= 1000 THEN '高端'
WHEN p >= 100 THEN '中端'
ELSE '入门'
END;
$$;用
CASE实现自定义排序时,ORDER BY中的表达式要尽量简单,复杂表达式可先在子查询里算好再排序。在
WHERE中避免用CASE包裹索引列,改写成OR组合,让优化器仍能使用索引。UPDATE ... SET col = CASE ...前先BEGIN,用SELECT复核结果再提交,避免批量误改。
BEGIN;
UPDATE products SET stock = CASE WHEN stock = 0 THEN 10 ELSE stock END;
SELECT sku, stock FROM products ORDER BY sku; -- 复核
ROLLBACK; -- 确认无误后改为 COMMIT