Skip to content

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 StatementPL/pgSQL只能作为一条独立语句
CASE 表达式CASE ExpressionSQL能出现在任何表达式位置
简单 CASESimple CASEPL/pgSQLCASE 表达式 WHEN 值 THEN ...
搜索 CASESearched CASEPL/pgSQLCASE WHEN 条件 THEN ...
CASE_NOT_FOUND——条件名SQLSTATE 20000

两者的位置差异可以用一张图说明:

text
        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。

sql
-- 演示: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;
text
-- 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

语法与用法 ​

两种形态的完整语法:

text
-- 简单 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」

最小可运行示例——同一段业务用两种形态各写一次:

sql
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;
text
-- demo_case_forms(1)=高, (3)=中, (5)=低, (9)=未知

详细说明 ​

简单 CASE:对同一个值做等值匹配 ​

简单 CASE 的 search-expression 只求值一次,然后依次与每个 WHEN 后的表达式列表比较,用的是 = 运算符。这带来两个直接结论:表达式有副作用时只执行一次;比较规则完全遵循 = 的语义(包括 NULL 的处理)。

sql
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;
text
-- demo_simple_case('pending')='等待支付', ('done')='已结束', ('zzz')='未知状态:zzz'

简单 CASE 里的 WHEN 值必须是常量或可在编译期确定为不变量的表达式。想用变量做比较对象,得用搜索 CASE:

sql
-- 不推荐:简单 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 省略且无命中时会报错」而不是「什么都不做」。

sql
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;
text
-- demo_searched_case(95)='A', (72)='C', (10)='F', (NULL)='未录入'

和 IF-ELSIF 一样,顺序敏感、命中即停。第一个为 true 的条件命中后,后面的 WHEN 不再求值。

text
搜索 CASE 的求值过程:
  WHEN #1 ──true──▶ 执行语句 #1 ──▶ 跳到 END CASE 之后
     │false/NULL
     ▼
  WHEN #2 ──true──▶ 执行语句 #2 ──▶ 跳到 END CASE 之后
     │false/NULL
     ▼
  ELSE  ──────────▶ 执行 ELSE 语句
     │
     └─没有 ELSE─▶ 抛 CASE_NOT_FOUND

CASE_NOT_FOUND:省略 ELSE 的代价 ​

这是本节最需要记住的机制。两种 CASE 形态在省略 ELSE 且无匹配时都会抛 CASE_NOT_FOUND(SQLSTATE 20000),而不是静默跳过。

sql
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;
text
-- 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 转成更友好的错误:

sql
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;
text
-- demo_case_safe(2)='two', demo_case_safe(99)='未预期的 code:99'

NULL 在简单 CASE 中不匹配任何分支 ​

简单 CASE 用 = 比较,而 NULL = 任何值 的结果是 NULL,不是 true。所以 search-expression 为 NULL 时,除非某个 WHEN 也写作 NULL(这本身不会匹配),否则一定落空。

sql
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;
text
-- demo_case_null('a')='匹配 a', ('c')='未匹配(原值 c)', (NULL)='未匹配(原值 NULL)'

试着写 WHEN NULL THEN 也不会命中——NULL = NULL 结果是 NULL 而非 true:

sql
-- 错误!这个分支永远不会命中
-- CASE p_val
--   WHEN NULL THEN v_r := 'x';   -- NULL = p_val 恒为 NULL
-- END CASE;

要处理 NULL,改为搜索 CASE 并显式判空:

sql
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)天然断言
sql
-- 推荐:值映射用简单 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
-- 推荐:把映射交给 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 表达式几乎是唯一选择:

sql
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 表达式
依据条件执行多条 DMLPL/pgSQL CASE 语句
依据条件 RAISE / RETURNPL/pgSQL CASE 语句

空分支与 NULL 语句 ​

CASE 允许某个分支写空语句——这不是语法错误,表示「命中后什么都不做」。

sql
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;
text
-- demo_case_noop(1)='default', (2)='two', (3)='other'

NULL; 是 PL/pgSQL 的「什么都不做」语句,与 PERFORM 不同:PERFORM 是执行一段查询但丢弃结果,NULL; 是完全不做任何事。注意单独一个分号 ; 不是合法语句,PL/pgSQL 不接受空语句。

复杂分发的替代方案 ​

分支数继续膨胀时,CASE 会变成一大段难以维护的映射表。此时应该把映射外置。

sql
-- 把「状态 → 展示名 → 是否终态」放进配置表
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;
text
-- app_status_label('paid') = '等待发货'
分支数量推荐手段
2~3IF-ELSE 或 CASE
4~8CASE(简单形态优先)
8 以上、且是纯映射配置表 + 一次查询
8 以上、且每支逻辑不同拆成多个函数,用分发函数调用

配置表方案还有个额外好处:新增状态不需要改函数、不需要重新部署,业务方插入一行即可。

CASE 在异常处理中的用法 ​

CASE 常见于把「原始错误码」翻译成「业务错误码」,供上层统一处理。

sql
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;
text
-- app_translate_error('23505')='DUPLICATE', ('P0002')='NOT_FOUND', ('XX000')='UNKNOWN:XX000'

注意 ELSE 里的 'UNKNOWN:' || p_sqlstate 保证了任何未列出的错误码都有输出,不会触发 CASE_NOT_FOUND——这是处理「外部输入驱动的映射」时的标准做法。

实战示例 ​

场景一:工单 SLA 分级与自动派单 ​

需求:按优先级与创建时长计算 SLA 状态,并给出负责团队。

sql
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;
text
-- 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 分发到不同计算逻辑。

sql
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;
text
-- paid=100.00, shipped=90.00, done=190.00, cancelled=0.00

非法状态会明确报错而不是静默返回 0,避免把「逻辑漏洞」伪装成「无需退款」:

text
-- 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 兜底或显式判空。

    sql
    CASE 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 found20000CASE 省略 ELSE 且无分支命中
syntax error at or near "END"42601漏写 END CASE 后的分号
syntax error at or near "CASE"42601把 CASE 语句写在表达式位置
variable "v_x" is not declared42601分支里用了未在 DECLARE 声明的变量
control reached end of function without RETURN2F005分支外没有兜底的返回路径

最佳实践 ​

  • 纯值映射优先用简单 CASE,它把「输入值 → 输出值」的对应关系摆得最清楚:

    sql
    CASE 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:

    sql
    CASE
      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:

    sql
    ELSE
      RAISE EXCEPTION '未支持的状态:%', p_status USING ERRCODE = 'P0001';
  • ELSE 分支里带上原值,让报错信息能自解释:

    sql
    ELSE 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 同一原则:

    sql
    CASE
      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 分支结构:

    sql
    SELECT CASE status WHEN 'open' THEN 1 ELSE 0 END AS is_open FROM demo_tickets;
  • 映射超过八支改用配置表,让业务方自行维护,也避免函数重新部署:

    sql
    SELECT d.label FROM demo_status_dict d WHERE d.status = p_status;
  • 把「状态字典」和「状态流转规则」分开。字典用 CASE 或配置表做显示映射,流转规则用 IF 做校验,两者职责不同,混在一起会越来越难改。

  • CASE 分支保持「一支一件事」。每支都只是一条赋值或一条 DML,长度均匀,阅读体验最好。

  • 优先简单 CASE、退化到搜索 CASE、再退化到 IF。这是可读性递减、表达力递增的顺序,按需选择最左侧可行的形态。

下一节

下一节我们学习:LOOP / WHILE / FOR,掌握无条件循环、条件循环与整数 FOR 的边界求值规则,以及带标签的嵌套循环。

参考链接 ​