CASE 语句(简单 CASE / 搜索 CASE)
CASE 是 PL/pgSQL 中与 IF-ELSIF 并列的多路分支结构,但用途更聚焦:它是「把一个值映射成另一个结果」的最佳表达方式。PL/pgSQL 提供两种形态——简单 CASE(对同一个表达式做等值匹配)与搜索 CASE(逐条判断布尔条件)。它们有一个和 IF 完全不同的行为:ELSE 省略且没有分支命中时会抛异常 CASE_NOT_FOUND。
前置知识
阅读本节前,建议先了解:条件分支(IF / THEN / ELSIF / ELSE)
基础概念
CASE 语句(CASE Statement)是 PL/pgSQL 的复合语句之一,必须用 END CASE; 结束。它和 SQL 里的 CASE 表达式名字相同、写法相似,但一个是语句、一个是表达式,用途完全不同。
| 概念 | 术语 | 归属 | 出现位置 |
|---|---|---|---|
CASE 语句 | CASE Statement | PL/pgSQL | 只能作为一条独立语句 |
CASE 表达式 | CASE Expression | SQL | 能出现在任何表达式位置 |
简单 CASE | Simple CASE | PL/pgSQL | CASE 表达式 WHEN 值 THEN ... |
搜索 CASE | Searched CASE | PL/pgSQL | CASE WHEN 条件 THEN ... |
CASE_NOT_FOUND | —— | 条件名 | SQLSTATE 20000 |
两者的位置差异可以用一张图说明:
SQL 层(可在表达式里出现) PL/pgSQL 层(只能是语句)
┌──────────────────────────────────┐ ┌────────────────────────────────┐
│ SELECT CASE WHEN x > 0 THEN 'p' │ │ CASE x │
│ ELSE 'n' END │ │ WHEN 1 THEN ... │
│ FROM t; │ │ WHEN 2 THEN ... │
│ │ │ ELSE ... │
│ -- CASE 表达式,有返回值 │ │ END CASE; │
└──────────────────────────────────┘ │ -- CASE 语句,无返回值 │
└────────────────────────────────┘最关键的差异:PL/pgSQL 的 CASE 语句若省略 ELSE 且所有 WHEN 都不匹配,会报错;而 SQL 的 CASE 表达式省略 ELSE 时只是返回 NULL。
-- 演示:SEARCHED CASE 命中与不命中的差别
CREATE OR REPLACE FUNCTION demo_case_miss(p_val integer)
RETURNS text
LANGUAGE plpgsql
IMMUTABLE
AS $$
DECLARE
v_msg text;
BEGIN
CASE
WHEN p_val > 100 THEN
v_msg := '大于 100';
WHEN p_val > 0 THEN
v_msg := '正数';
END CASE; -- 省略了 ELSE:没有分支命中就报 CASE_NOT_FOUND
RETURN v_msg;
END;
$$;
SELECT demo_case_miss(50) AS hit;-- SELECT demo_case_miss(50) -> 正数
-- SELECT demo_case_miss(0) -> ERROR: case not found
-- HINT: CASE statement is missing ELSE part.
-- CONTEXT: PL/pgSQL function demo_case_miss(integer) line 6 at CASE语法与用法
两种形态的完整语法:
-- 简单 CASE:先求一次 search-expression,再逐个与 WHEN 的值比较
CASE search-expression
WHEN expression [, expression ... ] THEN
语句;
[ WHEN expression [, expression ... ] THEN
语句; ]
[ ELSE
语句; ]
END CASE;
-- 搜索 CASE:逐个求值布尔条件
CASE
WHEN boolean-expression THEN
语句;
[ WHEN boolean-expression THEN
语句; ]
[ ELSE
语句; ]
END CASE;| 形态 | 匹配方式 | 比较运算符 | 能否写区间判断 |
|---|---|---|---|
简单 CASE | 等值匹配 | = | 不能 |
搜索 CASE | 布尔条件 | 任意(>、<、LIKE、BETWEEN…) | 能 |
三条语法规则:
| 规则 | 说明 |
|---|---|
END CASE 后必须有分号 | 与 END IF 同样要求 |
| 两种形态不能混用 | 写了 search-expression 就不能再用布尔条件的 WHEN |
简单 CASE 允许多值合写 | WHEN 1, 2 THEN 表示「等于 1 或 2」 |
最小可运行示例——同一段业务用两种形态各写一次:
CREATE TABLE IF NOT EXISTS demo_tickets(
ticket_id bigserial PRIMARY KEY,
priority integer NOT NULL CHECK (priority BETWEEN 1 AND 5),
status text NOT NULL DEFAULT 'open',
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE OR REPLACE FUNCTION demo_case_forms(p_priority integer)
RETURNS text
LANGUAGE plpgsql
IMMUTABLE
AS $$
DECLARE
v_label text;
BEGIN
-- 简单 CASE:对 p_priority 做等值匹配
CASE p_priority
WHEN 1, 2 THEN
v_label := '高';
WHEN 3 THEN
v_label := '中';
WHEN 4, 5 THEN
v_label := '低';
ELSE
v_label := '未知';
END CASE;
RETURN v_label;
END;
$$;
SELECT demo_case_forms(1) AS p1, demo_case_forms(3) AS p3,
demo_case_forms(5) AS p5, demo_case_forms(9) AS p9;-- demo_case_forms(1)=高, (3)=中, (5)=低, (9)=未知详细说明
简单 CASE:对同一个值做等值匹配
简单 CASE 的 search-expression 只求值一次,然后依次与每个 WHEN 后的表达式列表比较,用的是 = 运算符。这带来两个直接结论:表达式有副作用时只执行一次;比较规则完全遵循 = 的语义(包括 NULL 的处理)。
CREATE OR REPLACE FUNCTION demo_simple_case(p_status text)
RETURNS text
LANGUAGE plpgsql
IMMUTABLE
AS $$
DECLARE
v_action text;
BEGIN
CASE p_status
WHEN 'pending' THEN
v_action := '等待支付';
WHEN 'paid' THEN
v_action := '等待发货';
WHEN 'shipped' THEN
v_action := '等待签收';
WHEN 'done', 'cancelled' THEN
v_action := '已结束';
ELSE
v_action := format('未知状态:%s', p_status);
END CASE;
RETURN v_action;
END;
$$;
SELECT demo_simple_case('pending') AS a,
demo_simple_case('done') AS b,
demo_simple_case('zzz') AS c;-- demo_simple_case('pending')='等待支付', ('done')='已结束', ('zzz')='未知状态:zzz'简单 CASE 里的 WHEN 值必须是常量或可在编译期确定为不变量的表达式。想用变量做比较对象,得用搜索 CASE:
-- 不推荐:简单 CASE 的 WHEN 写变量,语义上是「与变量的值做等值比较」
-- CASE p_status
-- WHEN v_expected THEN ... -- 合法但容易误读成「条件判断」
-- 推荐:需要变量参与判定时用搜索 CASE
CASE
WHEN p_status = v_expected THEN ...
END CASE;搜索 CASE:逐条判断布尔条件
搜索 CASE 不写 search-expression,每个 WHEN 后面直接跟布尔条件。它和 IF-ELSIF 链完全等价,唯一区别是「ELSE 省略且无命中时会报错」而不是「什么都不做」。
CREATE OR REPLACE FUNCTION demo_searched_case(p_score integer)
RETURNS text
LANGUAGE plpgsql
IMMUTABLE
AS $$
DECLARE
v_grade text;
BEGIN
CASE
WHEN p_score IS NULL THEN
v_grade := '未录入'; -- 显式处理 NULL,必须在最前
WHEN p_score >= 90 THEN
v_grade := 'A';
WHEN p_score >= 80 THEN
v_grade := 'B';
WHEN p_score >= 70 THEN
v_grade := 'C';
WHEN p_score >= 60 THEN
v_grade := 'D';
ELSE
v_grade := 'F';
END CASE;
RETURN v_grade;
END;
$$;
SELECT demo_searched_case(95) AS a,
demo_searched_case(72) AS b,
demo_searched_case(10) AS c,
demo_searched_case(NULL) AS d;-- demo_searched_case(95)='A', (72)='C', (10)='F', (NULL)='未录入'和 IF-ELSIF 一样,顺序敏感、命中即停。第一个为 true 的条件命中后,后面的 WHEN 不再求值。
搜索 CASE 的求值过程:
WHEN #1 ──true──▶ 执行语句 #1 ──▶ 跳到 END CASE 之后
│false/NULL
▼
WHEN #2 ──true──▶ 执行语句 #2 ──▶ 跳到 END CASE 之后
│false/NULL
▼
ELSE ──────────▶ 执行 ELSE 语句
│
└─没有 ELSE─▶ 抛 CASE_NOT_FOUNDCASE_NOT_FOUND:省略 ELSE 的代价
这是本节最需要记住的机制。两种 CASE 形态在省略 ELSE 且无匹配时都会抛 CASE_NOT_FOUND(SQLSTATE 20000),而不是静默跳过。
CREATE OR REPLACE FUNCTION demo_case_not_found(p_code integer)
RETURNS text
LANGUAGE plpgsql
IMMUTABLE
AS $$
DECLARE
v_name text;
BEGIN
CASE p_code
WHEN 1 THEN
v_name := 'one';
WHEN 2 THEN
v_name := 'two';
-- 没有 ELSE
END CASE;
RETURN v_name;
END;
$$;
SELECT demo_case_not_found(1) AS hit;-- SELECT demo_case_not_found(1) -> one
-- SELECT demo_case_not_found(99) -> ERROR: case not found
-- HINT: CASE statement is missing ELSE part.
-- CONTEXT: PL/pgSQL function demo_case_not_found(integer) line 6 at CASE| 写法 | 无匹配时的行为 |
|---|---|
CASE ... ELSE ... END CASE; | 执行 ELSE 分支 |
CASE ... END CASE;(有返回类型的函数里赋给变量) | 抛 CASE_NOT_FOUND |
IF ... ELSIF ... END IF;(无 ELSE) | 什么都不做,继续往下 |
这条差异意味着:从 IF-ELSIF 改写成 CASE 时必须补上 ELSE,否则原本「静默跳过」的路径会变成异常。反过来说,如果你希望「未预期的值必须暴露出来」,CASE 省略 ELSE 恰好是一种断言。
可以用异常处理把 CASE_NOT_FOUND 转成更友好的错误:
CREATE OR REPLACE FUNCTION demo_case_safe(p_code integer)
RETURNS text
LANGUAGE plpgsql
IMMUTABLE
AS $$
DECLARE
v_name text;
BEGIN
CASE p_code
WHEN 1 THEN v_name := 'one';
WHEN 2 THEN v_name := 'two';
END CASE;
RETURN v_name;
EXCEPTION
WHEN CASE_NOT_FOUND THEN
RETURN format('未预期的 code:%s', p_code);
END;
$$;
SELECT demo_case_safe(2) AS a, demo_case_safe(99) AS b;-- demo_case_safe(2)='two', demo_case_safe(99)='未预期的 code:99'NULL 在简单 CASE 中不匹配任何分支
简单 CASE 用 = 比较,而 NULL = 任何值 的结果是 NULL,不是 true。所以 search-expression 为 NULL 时,除非某个 WHEN 也写作 NULL(这本身不会匹配),否则一定落空。
CREATE OR REPLACE FUNCTION demo_case_null(p_val text)
RETURNS text
LANGUAGE plpgsql
IMMUTABLE
AS $$
DECLARE
v_r text;
BEGIN
CASE p_val
WHEN 'a' THEN v_r := '匹配 a';
WHEN 'b' THEN v_r := '匹配 b';
ELSE v_r := format('未匹配(原值 %s)', coalesce(p_val, 'NULL'));
END CASE;
RETURN v_r;
END;
$$;
SELECT demo_case_null('a') AS a,
demo_case_null('c') AS c,
demo_case_null(NULL) AS n;-- demo_case_null('a')='匹配 a', ('c')='未匹配(原值 c)', (NULL)='未匹配(原值 NULL)'试着写 WHEN NULL THEN 也不会命中——NULL = NULL 结果是 NULL 而非 true:
-- 错误!这个分支永远不会命中
-- CASE p_val
-- WHEN NULL THEN v_r := 'x'; -- NULL = p_val 恒为 NULL
-- END CASE;要处理 NULL,改为搜索 CASE 并显式判空:
CASE
WHEN p_val IS NULL THEN v_r := '空值';
WHEN p_val = 'a' THEN v_r := '匹配 a';
ELSE v_r := '其它';
END CASE;IF-ELSIF 与 CASE 的取舍
两者能力等价(除了 ELSE 省略时的行为),选择依据是可读性。
| 场景 | 推荐 | 理由 |
|---|---|---|
| 同一个值映射到不同结果 | 简单 CASE | 意图明确、search-expression 只求值一次 |
| 分支是区间/复杂条件 | 搜索 CASE 或 IF-ELSIF | 两者皆可,CASE 视觉上更整齐 |
| 分支需要多条语句且逻辑各不相同 | IF-ELSIF | 语句块较长时 IF 更自然 |
分支内要 RETURN / RAISE | 两者皆可 | CASE 内也可以 RETURN |
| 期望「无匹配即报错」 | CASE(省略 ELSE) | 天然断言 |
-- 推荐:值映射用简单 CASE,一眼看出是「枚举分发」
CASE p_status
WHEN 'open' THEN v_color := 'blue';
WHEN 'closed' THEN v_color := 'gray';
ELSE v_color := 'black';
END CASE;
-- 推荐:区间判断用搜索 CASE,视觉上比 IF 链更短
CASE
WHEN p_score >= 90 THEN v_grade := 'A';
WHEN p_score >= 60 THEN v_grade := 'D';
ELSE v_grade := 'F';
END CASE;
-- 推荐:分支内要做多件事、要提前返回时用 IF
IF p_status = 'open' THEN
UPDATE demo_tickets SET status = 'closed' WHERE ticket_id = p_id;
INSERT INTO demo_log(msg) VALUES ('自动关闭');
RETURN 'closed';
END IF;一个实用的判断标准:如果每个分支都只是 v_x := 某个值,用 CASE;如果有分支不止一条语句,用 IF。
CASE 语句与 SQL CASE 表达式如何配合
CASE 表达式可以内联在 SQL 里,能省掉整个 PL/pgSQL 分支结构。当「映射」是纯值的、不涉及多语句时,优先用表达式。
-- 推荐:把映射交给 SQL,函数体只有一个 RETURN QUERY
CREATE OR REPLACE FUNCTION app_ticket_summary(p_min_priority integer)
RETURNS TABLE(priority integer, label text, cnt bigint)
LANGUAGE plpgsql
STABLE
AS $$
BEGIN
RETURN QUERY
SELECT t.priority,
CASE t.priority -- 这里是 SQL CASE 表达式
WHEN 1 THEN 'P1-紧急'
WHEN 2 THEN 'P2-高'
WHEN 3 THEN 'P3-中'
ELSE 'P4/P5-低'
END AS label,
count(*) AS cnt
FROM demo_tickets t
WHERE t.priority >= p_min_priority
GROUP BY t.priority
ORDER BY t.priority;
END;
$$;再比如「一行里同时算多个衍生列」这种需求,CASE 表达式几乎是唯一选择:
SELECT ticket_id,
CASE status WHEN 'open' THEN 1 ELSE 0 END AS is_open,
CASE WHEN created_at > now() - interval '1 day' THEN 'new' ELSE 'old' END AS age_bucket
FROM demo_tickets;| 需求 | 手段 |
|---|---|
在 SELECT 列表里做条件取值 | SQL CASE 表达式 |
在 ORDER BY / GROUP BY 里做条件排序 | SQL CASE 表达式 |
| 依据条件执行多条 DML | PL/pgSQL CASE 语句 |
依据条件 RAISE / RETURN | PL/pgSQL CASE 语句 |
空分支与 NULL 语句
CASE 允许某个分支写空语句——这不是语法错误,表示「命中后什么都不做」。
CREATE OR REPLACE FUNCTION demo_case_noop(p_val integer)
RETURNS text
LANGUAGE plpgsql
IMMUTABLE
AS $$
DECLARE
v_r text := 'default';
BEGIN
CASE p_val
WHEN 1 THEN
NULL; -- 显式空操作
WHEN 2 THEN
v_r := 'two';
ELSE
v_r := 'other';
END CASE;
RETURN v_r;
END;
$$;
SELECT demo_case_noop(1) AS a, demo_case_noop(2) AS b, demo_case_noop(3) AS c;-- demo_case_noop(1)='default', (2)='two', (3)='other'NULL; 是 PL/pgSQL 的「什么都不做」语句,与 PERFORM 不同:PERFORM 是执行一段查询但丢弃结果,NULL; 是完全不做任何事。注意单独一个分号 ; 不是合法语句,PL/pgSQL 不接受空语句。
复杂分发的替代方案
分支数继续膨胀时,CASE 会变成一大段难以维护的映射表。此时应该把映射外置。
-- 把「状态 → 展示名 → 是否终态」放进配置表
CREATE TABLE IF NOT EXISTS demo_status_dict(
status text PRIMARY KEY,
label text NOT NULL,
is_final boolean NOT NULL DEFAULT false,
sort_order integer NOT NULL DEFAULT 0
);
INSERT INTO demo_status_dict(status, label, is_final, sort_order) VALUES
('pending', '等待支付', false, 1),
('paid', '等待发货', false, 2),
('shipped', '等待签收', false, 3),
('done', '已完成', true, 4),
('cancelled', '已取消', true, 5)
ON CONFLICT (status) DO NOTHING;
CREATE OR REPLACE FUNCTION app_status_label(p_status text)
RETURNS text
LANGUAGE sql
STABLE
AS $$
SELECT d.label FROM demo_status_dict d WHERE d.status = p_status;
$$;
SELECT app_status_label('paid') AS label;-- app_status_label('paid') = '等待发货'| 分支数量 | 推荐手段 |
|---|---|
| 2~3 | IF-ELSE 或 CASE |
| 4~8 | CASE(简单形态优先) |
| 8 以上、且是纯映射 | 配置表 + 一次查询 |
| 8 以上、且每支逻辑不同 | 拆成多个函数,用分发函数调用 |
配置表方案还有个额外好处:新增状态不需要改函数、不需要重新部署,业务方插入一行即可。
CASE 在异常处理中的用法
CASE 常见于把「原始错误码」翻译成「业务错误码」,供上层统一处理。
CREATE OR REPLACE FUNCTION app_translate_error(p_sqlstate text)
RETURNS text
LANGUAGE plpgsql
IMMUTABLE
AS $$
DECLARE
v_biz_code text;
BEGIN
CASE p_sqlstate
WHEN '23505' THEN v_biz_code := 'DUPLICATE';
WHEN '23503' THEN v_biz_code := 'FK_VIOLATION';
WHEN '23502' THEN v_biz_code := 'NOT_NULL';
WHEN '22001' THEN v_biz_code := 'TOO_LONG';
WHEN 'P0002' THEN v_biz_code := 'NOT_FOUND';
ELSE v_biz_code := 'UNKNOWN:' || p_sqlstate;
END CASE;
RETURN v_biz_code;
END;
$$;
SELECT app_translate_error('23505') AS a,
app_translate_error('P0002') AS b,
app_translate_error('XX000') AS c;-- app_translate_error('23505')='DUPLICATE', ('P0002')='NOT_FOUND', ('XX000')='UNKNOWN:XX000'注意 ELSE 里的 'UNKNOWN:' || p_sqlstate 保证了任何未列出的错误码都有输出,不会触发 CASE_NOT_FOUND——这是处理「外部输入驱动的映射」时的标准做法。
实战示例
场景一:工单 SLA 分级与自动派单
需求:按优先级与创建时长计算 SLA 状态,并给出负责团队。
CREATE TABLE IF NOT EXISTS demo_sla_policy(
priority integer PRIMARY KEY,
resolve_min integer NOT NULL, -- 解决时限(分钟)
team text NOT NULL
);
INSERT INTO demo_sla_policy(priority, resolve_min, team) VALUES
(1, 240, 'oncall-core'),
(2, 480, 'oncall-app'),
(3, 1440, 'support-l2'),
(4, 2880, 'support-l1'),
(5, 5760, 'support-l1')
ON CONFLICT (priority) DO NOTHING;
CREATE OR REPLACE FUNCTION app_sla_state(p_priority integer, p_age_min integer)
RETURNS text
LANGUAGE plpgsql
STABLE
AS $$
DECLARE
v_policy demo_sla_policy%ROWTYPE;
v_left integer;
v_state text;
BEGIN
SELECT * INTO v_policy FROM demo_sla_policy WHERE priority = p_priority;
IF NOT FOUND THEN
RAISE EXCEPTION '未知优先级 %', p_priority USING ERRCODE = '22023';
END IF;
v_left := v_policy.resolve_min - p_age_min;
-- 搜索 CASE:区间判断
CASE
WHEN v_left < 0 THEN
v_state := format('已超时 %s 分钟,需立即升级', -v_left);
WHEN v_left < v_policy.resolve_min * 0.2 THEN
v_state := format('剩余 %s 分钟,临界', v_left);
ELSE
v_state := format('剩余 %s 分钟,正常', v_left);
END CASE;
RETURN format('P%s → %s(负责团队 %s)', p_priority, v_state, v_policy.team);
END;
$$;
SELECT app_sla_state(1, 30) AS p1_ok,
app_sla_state(1, 250) AS p1_over,
app_sla_state(3, 1200) AS p3_near;-- app_sla_state(1,30) = 'P1 → 剩余 210 分钟,正常(负责团队 oncall-core)'
-- app_sla_state(1,250) = 'P1 → 已超时 10 分钟,需立即升级(负责团队 oncall-core)'
-- app_sla_state(3,1200) = 'P3 → 剩余 240 分钟,临界(负责团队 support-l2)'场景二:按状态与金额计算退款金额
需求:已发货的订单扣运费后退款,已取消的全额退,已完成的按比例扣手续费,其余不可退。用简单 CASE 分发到不同计算逻辑。
CREATE OR REPLACE FUNCTION app_refund_amount(
p_status text,
p_amount numeric,
p_shipping numeric
)
RETURNS numeric
LANGUAGE plpgsql
IMMUTABLE
AS $$
DECLARE
v_refund numeric;
BEGIN
IF p_amount IS NULL OR p_amount < 0 THEN
RAISE EXCEPTION '金额非法:%', p_amount USING ERRCODE = '22023';
END IF;
-- 简单 CASE:按状态分发(每个分支都是一段独立计算)
CASE p_status
WHEN 'pending' THEN
v_refund := p_amount; -- 未支付,原路退回
WHEN 'paid' THEN
v_refund := p_amount; -- 已支付未发货,全额退
WHEN 'shipped' THEN
v_refund := p_amount - coalesce(p_shipping, 0); -- 扣运费
WHEN 'done' THEN
v_refund := round(p_amount * 0.95, 2); -- 扣 5% 手续费
WHEN 'cancelled' THEN
v_refund := 0; -- 已取消,无需再退
ELSE
RAISE EXCEPTION '不支持退款的状态:%', p_status USING ERRCODE = 'P0001';
END CASE;
-- 兜底:退款金额不为负
IF v_refund < 0 THEN
v_refund := 0;
END IF;
RETURN v_refund;
END;
$$;
SELECT app_refund_amount('paid', 100.00, 10.00) AS paid,
app_refund_amount('shipped', 100.00, 10.00) AS shipped,
app_refund_amount('done', 200.00, 0.00) AS done,
app_refund_amount('cancelled', 50.00, 0.00) AS cancelled;-- paid=100.00, shipped=90.00, done=190.00, cancelled=0.00非法状态会明确报错而不是静默返回 0,避免把「逻辑漏洞」伪装成「无需退款」:
-- SELECT app_refund_amount('refunded', 10.00, 0.00);
-- ERROR: 不支持退款的状态:refunded
-- CONTEXT: PL/pgSQL function app_refund_amount(text,numeric,numeric) line 20 at RAISE注意事项
END CASE后必须有分号。漏掉会和END IF一样报语法错误。sql-- 错误!syntax error at or near "RETURN" -- CASE v_x WHEN 1 THEN v_r := 'a'; END CASE -- RETURN v_r; -- 正确 CASE v_x WHEN 1 THEN v_r := 'a'; END CASE; RETURN v_r;省略
ELSE且无匹配必然报错,这是CASE与IF-ELSIF最关键的差异。sql-- 不推荐:输入不在预期内时抛 CASE_NOT_FOUND,而非静默跳过 CASE p_code WHEN 1 THEN v_name := 'one'; END CASE; -- 推荐:除非「未预期值必须报错」正是你的意图 CASE p_code WHEN 1 THEN v_name := 'one'; ELSE v_name := 'unknown'; END CASE;两种形态不能混用。写了
search-expression就不可能再用布尔条件。sql-- 错误!简单 CASE 形式下 WHEN 只能是值,不能是比较表达式 -- CASE v_x -- WHEN v_x > 10 THEN ... -- 语法能过,但语义是 v_x = (v_x > 10),几乎恒不匹配 -- END CASE; -- 正确 CASE WHEN v_x > 10 THEN ... END CASE;简单
CASE的WHEN NULL永远不命中。NULL = NULL的结果是NULL。sql-- 错误!该分支永远不执行 -- CASE v_val -- WHEN NULL THEN v_r := 'is null'; -- END CASE; -- 正确:改用搜索 CASE CASE WHEN v_val IS NULL THEN v_r := 'is null'; END CASE;NULL输入不会被任何WHEN值匹配,必须靠ELSE兜底或显式判空。sqlCASE p_status WHEN 'open' THEN ... -- p_status 为 NULL 时不命中 ELSE v_r := 'unknown'; END CASE;搜索
CASE的WHEN顺序敏感。宽条件在前会让窄条件成为死代码。sql-- 错误!p_score >= 60 会吃掉所有 ≥60 的输入 -- CASE WHEN p_score >= 60 THEN v_g := 'D'; -- WHEN p_score >= 90 THEN v_g := 'A'; -- 不可达CASE是语句,不能当表达式用。别把它塞进SELECT列表或赋值右侧。sql-- 错误!CASE 语句不能出现在赋值右侧 -- v_x := CASE v_y WHEN 1 THEN 10 ELSE 20 END CASE; -- 正确(SQL CASE 表达式,注意无 CASE 关键字结尾) v_x := CASE v_y WHEN 1 THEN 10 ELSE 20 END;别在
CASE分支里写「既赋值又修改外部状态」的复杂逻辑,那样可读性不如IF,也丢掉了CASE的整齐优势。sql-- 不推荐:分支里做多件事,用 CASE 已无收益 CASE p_status WHEN 'open' THEN UPDATE a SET ...; INSERT INTO b ...; v_log := 'closed'; END CASE;CASE分支不会自动加锁或加事务边界。多个CASE分支里的 DML 与普通语句一样处在同一个函数事务中。映射类
CASE超过八支就该外置。长CASE难维护,且每次新增分支都要改函数、重新部署。
常见错误与 SQLSTATE:
| 错误现象 | SQLSTATE | 常见原因 |
|---|---|---|
case not found | 20000 | CASE 省略 ELSE 且无分支命中 |
syntax error at or near "END" | 42601 | 漏写 END CASE 后的分号 |
syntax error at or near "CASE" | 42601 | 把 CASE 语句写在表达式位置 |
variable "v_x" is not declared | 42601 | 分支里用了未在 DECLARE 声明的变量 |
control reached end of function without RETURN | 2F005 | 分支外没有兜底的返回路径 |
最佳实践
纯值映射优先用简单
CASE,它把「输入值 → 输出值」的对应关系摆得最清楚:sqlCASE p_status WHEN 'pending' THEN v_color := 'orange'; WHEN 'paid' THEN v_color := 'blue'; ELSE v_color := 'gray'; END CASE;搜索
CASE里把IS NULL判断放在最前,否则NULL会一路穿到ELSE:sqlCASE WHEN p_score IS NULL THEN v_grade := '未录入'; WHEN p_score >= 90 THEN v_grade := 'A'; ELSE v_grade := 'F'; END CASE;除非「未预期的输入必须报错」,否则永远写上
ELSE。省略ELSE等于埋了一个CASE_NOT_FOUND:sqlELSE RAISE EXCEPTION '未支持的状态:%', p_status USING ERRCODE = 'P0001';ELSE分支里带上原值,让报错信息能自解释:sqlELSE v_r := format('未知状态:%s', coalesce(p_status, 'NULL'));ELSE是兜底而不是「正常分支」。如果ELSE频繁命中,说明前面的枚举不全,应补齐而不是依赖兜底。用
CASE省略ELSE做断言。当你确信「枚举必须被穷举」时,让它报错是最省事的校验:sql-- 故意不写 ELSE:新增枚举值时会立刻暴露 CASE_NOT_FOUND CASE v_kind WHEN 'a' THEN ... WHEN 'b' THEN ... END CASE;区间判定把条件从窄到宽排,与
IF-ELSIF同一原则:sqlCASE WHEN p_amount >= 10000 THEN v_rate := 0.05; WHEN p_amount >= 5000 THEN v_rate := 0.02; ELSE v_rate := 0; END CASE;值映射内联在 SQL 里时改用 SQL
CASE表达式,能省掉整个 PL/pgSQL 分支结构:sqlSELECT CASE status WHEN 'open' THEN 1 ELSE 0 END AS is_open FROM demo_tickets;映射超过八支改用配置表,让业务方自行维护,也避免函数重新部署:
sqlSELECT d.label FROM demo_status_dict d WHERE d.status = p_status;把「状态字典」和「状态流转规则」分开。字典用
CASE或配置表做显示映射,流转规则用IF做校验,两者职责不同,混在一起会越来越难改。CASE分支保持「一支一件事」。每支都只是一条赋值或一条 DML,长度均匀,阅读体验最好。优先简单
CASE、退化到搜索CASE、再退化到IF。这是可读性递减、表达力递增的顺序,按需选择最左侧可行的形态。
下一节
下一节我们学习:LOOP / WHILE / FOR,掌握无条件循环、条件循环与整数 FOR 的边界求值规则,以及带标签的嵌套循环。