Skip to content

条件分支(IF / THEN / ELSIF / ELSE) ​

过程化代码的价值,一半体现在「按条件走不同分支」上。PL/pgSQL 的条件语句是 IF 系列,共三种形态:IF-THEN、IF-THEN-ELSE、IF-THEN-ELSIF。它们看起来和别的语言差不多,但有两个 PostgreSQL 特有的细节必须记牢:条件为 NULL 时走 ELSE 分支,以及 END IF 后面的分号不能省。本节把三种形态的语法、求值顺序、短路规则、与 SQL CASE 的取舍一次讲清。

前置知识

阅读本节前,建议先了解:执行动态 SQL(EXECUTE)

基础概念 ​

布尔表达式(Boolean Expression)是 IF 的入口:它求值为 true、false 或 NULL 三值之一。PL/pgSQL 只在结果为 true 时进入 THEN 分支;false 与 NULL 都走 ELSE。

概念术语说明
分支Branch条件成立时执行的一段语句
布尔表达式Boolean Expression求值为 true / false / NULL
三值逻辑Three-Valued LogicNULL 参与比较的结果仍是 NULL
短路求值Short-Circuit EvaluationAND / OR 提前确定结果即停止求值
ELSIFElse-If继续检查下一个条件,可多个
ELSEIF——ELSIF 的等价拼写,官方明确支持

三值逻辑与 IF 的对应关系:

text
布尔表达式的求值结果         IF 的行为
─────────────────────────────────────────
true                   →   进入 THEN 分支
false                  →   跳过 THEN,检查 ELSIF 或走 ELSE
NULL                   →   与 false 同样处理:跳过 THEN
                            (这点与「未知不等于假」的直觉相反)

最典型的陷阱是「取反判断」:

text
v_flag 的值   v_flag = false   v_flag <> true   NOT v_flag
──────────────────────────────────────────────────────────
true          false            false            false
false         true             true             true
NULL          NULL             NULL             NULL   ← 三个判断都得不到 true

所以 IF NOT v_flag THEN ... END IF; 在 v_flag 为 NULL 时不会进入分支。要处理「非真即假」的语义,得显式写 IF v_flag IS NOT TRUE THEN,或先做 coalesce(v_flag, false) 归一化。

sql
CREATE OR REPLACE FUNCTION demo_null_cond(p_flag boolean)
RETURNS text
LANGUAGE plpgsql
IMMUTABLE
AS $$
BEGIN
  IF p_flag THEN
    RETURN 'THEN 分支:p_flag 为 true';
  ELSE
    RETURN format('ELSE 分支:p_flag 为 %s', coalesce(p_flag::text, 'NULL'));
  END IF;
END;
$$;

SELECT demo_null_cond(true)  AS t,
       demo_null_cond(false) AS f,
       demo_null_cond(NULL)  AS n;
text
                t                |                 f                  |          n
---------------------------------+------------------------------------+----------------------
 THEN 分支:p_flag 为 true        | ELSE 分支:p_flag 为 false          | ELSE 分支:p_flag 为 NULL
(1 row)

语法与用法 ​

三种形态的完整语法:

text
-- 形态一:IF-THEN
IF 布尔表达式 THEN
    语句;
END IF;

-- 形态二:IF-THEN-ELSE
IF 布尔表达式 THEN
    语句;
ELSE
    语句;
END IF;

-- 形态三:IF-THEN-ELSIF(ELSIF 可重复多次,ELSE 可选)
IF 布尔表达式 THEN
    语句;
[ ELSIF 布尔表达式 THEN
    语句; ]
[ ELSIF 布尔表达式 THEN
    语句; ]
[ ELSE
    语句; ]
END IF;

三个语法硬性规则:

规则说明
END IF 后必须有分号与块内 END 一致,分号是语句终结符
IF 与 THEN 之间不能有分号IF 条件; THEN 是语法错误
ELSE IF ≠ ELSIFELSE IF 会开启一个嵌套的 IF,需要两个 END IF

最小可运行示例——三种形态各一个:

sql
CREATE TABLE IF NOT EXISTS demo_grades(
  student_id bigint PRIMARY KEY,
  score      integer NOT NULL CHECK (score BETWEEN 0 AND 100)
);

CREATE OR REPLACE FUNCTION demo_if_forms(p_score integer)
RETURNS text
LANGUAGE plpgsql
IMMUTABLE
AS $$
BEGIN
  -- 形态一:只在满足条件时做点事
  IF p_score = 100 THEN
    RETURN '满分';
  END IF;

  -- 形态二:二选一
  IF p_score >= 60 THEN
    RETURN '及格';
  ELSE
    RETURN '不及格';
  END IF;
END;
$$;

SELECT demo_if_forms(100) AS a, demo_if_forms(75) AS b, demo_if_forms(30) AS c;
text
   a   |   b   |    c
-------+-------+--------
 满分  | 及格  | 不及格
(1 row)

详细说明 ​

IF-THEN:最简单的守卫式判断 ​

IF-THEN 只回答一个问题:「条件成立时要做的事是什么」。条件不成立时什么都不做,继续往下执行。它最常见的用法是守卫(Guard)——在真正干活之前挡掉不合法输入。

sql
CREATE OR REPLACE FUNCTION demo_guard(p_qty integer)
RETURNS integer
LANGUAGE plpgsql
IMMUTABLE
AS $$
BEGIN
  IF p_qty IS NULL THEN
    RAISE EXCEPTION '数量不能为空' USING ERRCODE = '22004';
  END IF;

  IF p_qty <= 0 THEN
    RAISE EXCEPTION '数量必须为正数,收到 %', p_qty USING ERRCODE = '22023';
  END IF;

  IF p_qty > 10000 THEN
    RAISE EXCEPTION '单次数量不得超过 10000,收到 %', p_qty USING ERRCODE = '22023';
  END IF;

  RETURN p_qty * 100;      -- 走到这里说明参数已合法
END;
$$;

SELECT demo_guard(5) AS ok;
text
  ok
------
 500
(1 row)

用守卫而不是把校验散在业务逻辑里,好处是「参数是否合法」这件事在函数开头就一次性确定,后面不必反复判断。

IF-THEN-ELSE:二选一 ​

ELSE 分支承接「条件不为真」的所有情况——包括 false 和 NULL。它保证了无论输入是什么,函数总有一条路可走,这对「必须有返回值」的标量函数尤其重要。

sql
CREATE OR REPLACE FUNCTION demo_else_branch(p_score integer)
RETURNS text
LANGUAGE plpgsql
IMMUTABLE
AS $$
BEGIN
  IF p_score IS NOT NULL AND p_score >= 60 THEN
    RETURN '及格';
  ELSE
    -- 到这里可能是 <60,也可能是 NULL
    RETURN format('不及格(原值:%s)', coalesce(p_score::text, 'NULL'));
  END IF;
END;
$$;

SELECT demo_else_branch(80)   AS a,
       demo_else_branch(59)   AS b,
       demo_else_branch(NULL) AS c;
text
   a   |       b        |        c
-------+----------------+------------------
 及格  | 不及格(原值:59) | 不及格(原值:NULL)
(1 row)

如果函数声明了返回类型,而所有分支都没命中导致控制流走到函数末尾,会抛 control reached end of function without RETURN(SQLSTATE 2F005)。给 IF-THEN 补上 ELSE 是消除这类错误最直接的办法。

IF-THEN-ELSIF:多路分支 ​

ELSIF 用来把一连串互斥条件排成一条链。关键机制是顺序检查、命中即停:第一个为 true 的条件对应的分支执行后,控制直接跳到 END IF 之后,后面的 ELSIF 不会被求值。

sql
CREATE OR REPLACE FUNCTION demo_grade(p_score integer)
RETURNS text
LANGUAGE plpgsql
IMMUTABLE
AS $$
BEGIN
  IF p_score IS NULL THEN
    RETURN '未录入';
  ELSIF p_score >= 90 THEN
    RETURN 'A';
  ELSIF p_score >= 80 THEN
    RETURN 'B';
  ELSIF p_score >= 70 THEN
    RETURN 'C';
  ELSIF p_score >= 60 THEN
    RETURN 'D';
  ELSE
    RETURN 'F';
  END IF;
END;
$$;

SELECT demo_grade(95) AS s95, demo_grade(85) AS s85,
       demo_grade(65) AS s65, demo_grade(10) AS s10,
       demo_grade(NULL) AS snull;
text
 s95 | s85 | s65 | s10 | snull
-----+-----+-----+-----+--------
 A   | B   | D   | F   | 未录入
(1 row)

顺序至关重要。如果把 p_score >= 60 放在最前面,那么所有 ≥60 的分数都会被它截获,后面的 >= 90 永远没有机会执行:

sql
-- 错误!条件从宽到严,宽条件会吃掉所有小条件
IF p_score >= 60 THEN
  RETURN 'D';                -- 95 分也会走到这里
ELSIF p_score >= 90 THEN
  RETURN 'A';                -- 永远不可达
END IF;

规则是:从最严格(最窄)的条件往最宽松的方向排。

条件为 NULL 时发生了什么 ​

这是 PL/pgSQL 条件语句中仅次于分号问题的第二号陷阱。IF 只在表达式严格为 true 时进入 THEN;NULL 与 false 完全相同地走 ELSE(或继续检查下一个 ELSIF)。

sql
CREATE OR REPLACE FUNCTION demo_null_branch(p_val integer)
RETURNS text
LANGUAGE plpgsql
IMMUTABLE
AS $$
DECLARE
  v_msg text;
BEGIN
  -- 这三个条件在 p_val 为 NULL 时全部求值为 NULL
  IF p_val > 0 THEN
    v_msg := '正数';
  ELSIF p_val < 0 THEN
    v_msg := '负数';
  ELSIF p_val = 0 THEN
    v_msg := '零';
  ELSE
    v_msg := 'NULL 或其它';   -- NULL 会落到这里
  END IF;
  RETURN v_msg;
END;
$$;

SELECT demo_null_branch(5) AS pos,
       demo_null_branch(-5) AS neg,
       demo_null_branch(0) AS zero,
       demo_null_branch(NULL) AS nul;
text
 pos | neg  | zero |   nul
-----+------+------+----------
 正数 | 负数 | 零   | NULL 或其它
(1 row)

对应的 IF-THEN-ELSE 也有同样的语义:

sql
IF p_val > 0 THEN
  v_msg := '正数';
ELSE
  v_msg := '不是正数或为 NULL';    -- NULL 走这里
END IF;

如果你需要区分「明确为假」与「未知」,必须显式判断 IS NULL:

sql
IF p_val IS NULL THEN
  v_msg := '未知';
ELSIF p_val > 0 THEN
  v_msg := '正数';
ELSE
  v_msg := '零或负数';
END IF;

短路求值与求值顺序 ​

PL/pgSQL 的布尔表达式遵循 SQL 的短路规则:AND 在左侧为 false 时不再求值右侧;OR 在左侧为 true 时不再求值右侧。注意 NULL 不触发短路——NULL AND x 仍需求值 x 才能确定结果。

表达式左侧是否求值右侧结果
false AND x假否false
true AND x真是取决于 x
NULL AND x未知是除非 x 为 false,否则 NULL
true OR x真否true
false OR x假是取决于 x
NULL OR x未知是除非 x 为 true,否则 NULL

这条规则可以拿来保护「有副作用的表达式」和「可能报错的表达式」:

sql
CREATE OR REPLACE FUNCTION demo_short_circuit(p_arr integer[])
RETURNS text
LANGUAGE plpgsql
IMMUTABLE
AS $$
DECLARE
  v_len integer;
BEGIN
  -- 推荐:先判空再取长度,短路保证 array_length 不会在 NULL 上执行
  IF p_arr IS NOT NULL AND array_length(p_arr, 1) > 0 THEN
    RETURN format('数组长度 %s', array_length(p_arr, 1));
  END IF;
  RETURN '空数组或 NULL';
END;
$$;

SELECT demo_short_circuit(ARRAY[1,2,3]) AS a,
       demo_short_circuit(ARRAY[]::integer[]) AS b,
       demo_short_circuit(NULL) AS c;
text
      a       |      b      |      c
--------------+-------------+--------------
 数组长度 3    | 空数组或 NULL | 空数组或 NULL
(1 row)

注意短路只在单个布尔表达式内部有效,不能跨 IF 与 ELSIF 传递「已知信息」——ELSIF 的求值环境是完全新的。

嵌套 IF 与 ELSIF 的取舍 ​

ELSIF 本质上就是「扁平化的嵌套 ELSE IF」。两者语义等价,但可读性差别很大。

sql
-- 推荐:ELSIF 链,缩进层级恒定
IF v_type = 'A' THEN
  v_rate := 0.10;
ELSIF v_type = 'B' THEN
  v_rate := 0.20;
ELSIF v_type = 'C' THEN
  v_rate := 0.30;
ELSE
  v_rate := 0;
END IF;
sql
-- 不推荐:深层嵌套,END IF 数量容易数错
IF v_type = 'A' THEN
  v_rate := 0.10;
ELSE
  IF v_type = 'B' THEN      -- 注意这里是 ELSE IF,开启了一个新 IF
    v_rate := 0.20;
  ELSE
    IF v_type = 'C' THEN
      v_rate := 0.30;
    ELSE
      v_rate := 0;
    END IF;                 -- 需要三个 END IF
  END IF;
END IF;

写 ELSE IF(中间有空格)是合法的,但它不是 ELSIF。语法上它会开启一个嵌套的 IF,因而必须配一个额外的 END IF。若少了 END IF,报错会指向很远的位置,极难定位。

写法是否等价于 ELSIF需要的 END IF 数量
ELSIF 条件 THEN是1(只结束最外层)
ELSEIF 条件 THEN是1
ELSE IF 条件 THEN否,是「ELSE + 嵌套 IF」每层 1 个,共 n 个

IF 与 SQL 表达式 CASE 的取舍 ​

如果只是「把某个值映射成另一个值」,用 SQL 的 CASE 表达式往往比 IF 语句更简洁,因为它可以内联在 SELECT / UPDATE 里。PL/pgSQL 的 IF 是语句,不能出现在表达式位置。

sql
-- 推荐:纯值映射用 SQL CASE 表达式,可以内联在 UPDATE 中
UPDATE demo_grades
   SET score = score
 WHERE student_id = 1;

SELECT student_id,
       CASE
         WHEN score >= 90 THEN 'A'
         WHEN score >= 60 THEN 'D'
         ELSE 'F'
       END AS grade
  FROM demo_grades;
sql
-- 需要多个语句、局部变量、提前 RETURN 时用 IF
IF p_score >= 90 THEN
  INSERT INTO demo_audit_log(msg) VALUES ('优秀');
  RETURN 'A';
END IF;
场景推荐手段
纯值映射、可内联在 SQL 中SQL CASE 表达式
需要执行多条语句IF 语句
需要提前 RETURN / RAISEIF 语句
分支很多且只是选值查表(配置表 / VALUES 子查询)

分支数超过五六个时,考虑把「条件 → 结果」的映射放进一张配置表,用一次查询代替一长串 ELSIF——查询可以走索引,也便于业务方自行维护。

IF 与 RETURN / RAISE 的组合 ​

RETURN 与 RAISE 会立即终止当前函数,因此它们天然充当 IF 链的终点,可以省掉一个 ELSE。

sql
CREATE OR REPLACE FUNCTION demo_early_return(p_id bigint)
RETURNS text
LANGUAGE plpgsql
STABLE
AS $$
DECLARE
  v_name text;
BEGIN
  SELECT name INTO v_name FROM demo_students WHERE id = p_id;

  IF NOT FOUND THEN
    RETURN format('id=%s 不存在', p_id);       -- 提前返回,后续代码不用再缩进
  END IF;

  IF v_name IS NULL THEN
    RAISE EXCEPTION 'id=%s 的名字为空,数据异常', p_id USING ERRCODE = 'P0001';
  END IF;

  RETURN v_name;
END;
$$;

这种「守卫 + 提前返回」的写法把主逻辑保持在最外层缩进,比层层嵌套的 IF / ELSE 更易读。

实战示例 ​

场景一:阶梯折扣计价 ​

需求:根据订单金额与客户等级计算折扣率,折扣率取值规则由业务给定,需要清晰可审计。

sql
CREATE TABLE IF NOT EXISTS demo_discount_log(
  log_id      bigserial PRIMARY KEY,
  customer_id bigint       NOT NULL,
  amount      numeric(12,2) NOT NULL,
  tier        text         NOT NULL,
  rate        numeric(5,4) NOT NULL,
  created_at  timestamptz  NOT NULL DEFAULT now()
);

CREATE OR REPLACE FUNCTION app_calc_discount(
  p_customer_id bigint,
  p_amount      numeric,
  p_tier        text
)
RETURNS numeric
LANGUAGE plpgsql
AS $$
DECLARE
  v_rate numeric(5,4);
BEGIN
  -- 守卫一:金额必须为正
  IF p_amount IS NULL OR p_amount <= 0 THEN
    RAISE EXCEPTION '订单金额必须为正数,收到 %', p_amount USING ERRCODE = '22023';
  END IF;

  -- 守卫二:等级必须合法(显式判 NULL,避免 NULL 短路到 ELSE)
  IF p_tier IS NULL OR p_tier NOT IN ('normal', 'silver', 'gold', 'vip') THEN
    RAISE EXCEPTION '未知客户等级:%', p_tier USING ERRCODE = '22023';
  END IF;

  -- 主逻辑:等级定基准折扣,金额再叠加
  IF p_tier = 'vip' THEN
    v_rate := 0.30;
  ELSIF p_tier = 'gold' THEN
    v_rate := 0.20;
  ELSIF p_tier = 'silver' THEN
    v_rate := 0.10;
  ELSE
    v_rate := 0;                     -- normal
  END IF;

  IF p_amount >= 10000 THEN
    v_rate := v_rate + 0.05;
  ELSIF p_amount >= 5000 THEN
    v_rate := v_rate + 0.02;
  END IF;

  -- 上限保护
  IF v_rate > 0.40 THEN
    v_rate := 0.40;
  END IF;

  INSERT INTO demo_discount_log(customer_id, amount, tier, rate)
  VALUES (p_customer_id, p_amount, p_tier, v_rate);

  RETURN round(p_amount * (1 - v_rate), 2);
END;
$$;

SELECT app_calc_discount(1, 12000, 'vip')    AS vip_big,
       app_calc_discount(2, 6000,  'gold')   AS gold_mid,
       app_calc_discount(3, 100,   'normal') AS normal_small;
text
 vip_big | gold_mid | normal_small
---------+----------+--------------
 7800.00 |  4680.00 |       100.00
(1 row)
sql
-- 审计表记录下每次计算用的折扣率,便于事后复核
SELECT customer_id, amount, tier, rate
  FROM demo_discount_log
 ORDER BY log_id;
text
 customer_id | amount  |  tier  |  rate
-------------+---------+--------+--------
           1 | 12000.00 | vip    | 0.3500
           2 |  6000.00 | gold   | 0.2200
           3 |   100.00 | normal | 0.0000
(3 rows)

注意本例中「等级折扣」与「金额加码」是两段独立的 IF-ELSIF,而不是把四个等级与三档金额拼成十二个 ELSIF。把正交的维度拆成独立判断,分支数从乘积变成求和。

场景二:状态机式的订单流转校验 ​

需求:订单状态只能按 pending → paid → shipped → done 单向推进,允许从 pending 直接取消,其余状态禁止取消。

sql
CREATE TABLE IF NOT EXISTS demo_orders(
  order_id   bigserial PRIMARY KEY,
  status     text        NOT NULL DEFAULT 'pending',
  updated_at timestamptz NOT NULL DEFAULT now(),
  CHECK (status IN ('pending', 'paid', 'shipped', 'done', 'cancelled'))
);

CREATE OR REPLACE FUNCTION app_advance_order(p_order_id bigint, p_target text)
RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
  v_cur text;
BEGIN
  -- 目标状态必须是已知状态
  IF p_target NOT IN ('paid', 'shipped', 'done', 'cancelled') THEN
    RAISE EXCEPTION '非法目标状态:%', p_target USING ERRCODE = '22023';
  END IF;

  SELECT status INTO v_cur FROM demo_orders WHERE order_id = p_order_id FOR UPDATE;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'order % not found', p_order_id USING ERRCODE = 'P0002';
  END IF;

  -- 已终态的直接拒绝
  IF v_cur IN ('done', 'cancelled') THEN
    RAISE EXCEPTION '订单 % 已处于终态 %,不可再变更', p_order_id, v_cur
      USING ERRCODE = 'P0001';
  END IF;

  IF p_target = 'cancelled' THEN
    IF v_cur <> 'pending' THEN
      RAISE EXCEPTION '订单 % 已支付(%),不可直接取消', p_order_id, v_cur
        USING ERRCODE = 'P0001';
    END IF;
  ELSIF p_target = 'paid' THEN
    IF v_cur <> 'pending' THEN
      RAISE EXCEPTION '订单 % 当前为 %,不能再支付', p_order_id, v_cur
        USING ERRCODE = 'P0001';
    END IF;
  ELSIF p_target = 'shipped' THEN
    IF v_cur <> 'paid' THEN
      RAISE EXCEPTION '订单 % 当前为 %,尚未支付不可发货', p_order_id, v_cur
        USING ERRCODE = 'P0001';
    END IF;
  ELSE                        -- p_target = 'done'
    IF v_cur <> 'shipped' THEN
      RAISE EXCEPTION '订单 % 当前为 %,未发货不可完成', p_order_id, v_cur
        USING ERRCODE = 'P0001';
    END IF;
  END IF;

  UPDATE demo_orders
     SET status = p_target, updated_at = now()
   WHERE order_id = p_order_id;

  RETURN format('订单 %s:%s → %s', p_order_id, v_cur, p_target);
END;
$$;

INSERT INTO demo_orders(order_id, status) VALUES (1, 'pending'), (2, 'shipped')
ON CONFLICT (order_id) DO NOTHING;

SELECT app_advance_order(1, 'paid')    AS step1,
       app_advance_order(1, 'shipped') AS step2;
text
          step1          |           step2
-------------------------+---------------------------
 订单 1:pending → paid  | 订单 1:paid → shipped
(1 row)

非法流转会带上当前状态,定位问题不需要再去查表:

sql
SELECT app_advance_order(2, 'cancelled');
text
ERROR:  订单 2 已支付(shipped),不可直接取消
CONTEXT:  PL/pgSQL function app_advance_order(bigint,text) line 24 at RAISE

注意事项 ​

  • END IF 后面的分号绝对不能省。这是 IF 相关报错中最常见的一条。

    sql
    -- 错误!语法错误:missing ";" at end of statement
    -- IF v_ok THEN
    --   RETURN 1
    -- END IF
    
    -- 正确
    IF v_ok THEN
      RETURN 1;
    END IF;
  • IF 与 THEN 之间不能有分号。分号会提前结束语句。

    sql
    -- 错误!syntax error at or near "THEN"
    -- IF v_ok; THEN
    --   RETURN 1;
    -- END IF;
    
    -- 正确
    IF v_ok THEN
      RETURN 1;
    END IF;
  • NULL 条件走 ELSE,不走 THEN。IF NOT v_flag 在 v_flag 为 NULL 时不进入分支。

    sql
    -- 不推荐:v_flag 为 NULL 时不会进入分支,容易被误解成「不为真」
    IF NOT v_flag THEN ... END IF;
    
    -- 推荐:显式表达「不为真」
    IF v_flag IS NOT TRUE THEN ... END IF;
  • ELSIF 链的顺序必须从窄到宽。宽条件放前面会让后面的分支变成死代码。

    sql
    -- 错误!p_score >= 60 会截获所有 ≥60 的输入
    -- IF p_score >= 60 THEN RETURN 'D';
    -- ELSIF p_score >= 90 THEN RETURN 'A';   -- 永远不可达
    
    -- 正确
    IF p_score >= 90 THEN RETURN 'A';
    ELSIF p_score >= 60 THEN RETURN 'D';
    END IF;
  • ELSE IF 不是 ELSIF。它开启嵌套 IF,需要更多的 END IF 配对。

    sql
    -- 不推荐:看起来像 ELSIF,实为嵌套,END IF 数量容易数错
    IF a THEN ... ELSE IF b THEN ... END IF; END IF;
    
    -- 推荐
    IF a THEN ... ELSIF b THEN ... END IF;
  • 有返回类型的函数不能在所有分支都不返回值。走到函数末尾会抛 control reached end of function without RETURN(2F005)。

    sql
    -- 错误!所有条件都不成立时没有返回值
    -- IF p_x > 0 THEN RETURN 'pos'; END IF;
  • NULL 不触发短路。NULL AND 有副作用的表达式 仍会求值右侧。

    sql
    -- 依赖短路保护时,要确保左侧能明确得到 false 或 true
    IF p_arr IS NOT NULL AND array_length(p_arr, 1) > 0 THEN ... END IF;
  • 不要把 IF 当表达式用。IF 是语句,不能写在 SELECT 列表或赋值右侧。

    sql
    -- 错误!IF 不能出现在表达式位置
    -- v_x := IF v_b THEN 1 ELSE 2 END IF;
    
    -- 正确:用 SQL CASE 表达式
    v_x := CASE WHEN v_b THEN 1 ELSE 2 END;
  • 嵌套层数超过三层就该重构。深嵌套会让 END IF 配对困难,也增加分支组合的测试成本。

    sql
    -- 不推荐:四层嵌套
    IF a THEN IF b THEN IF c THEN IF d THEN ... END IF; END IF; END IF; END IF;
  • 判断布尔列时不要写 = true,虽然合法但多此一举,且在列可为 NULL 时更易误读。

    sql
    -- 不推荐
    IF v_active = true THEN ... END IF;
    
    -- 推荐
    IF v_active THEN ... END IF;

常见错误与 SQLSTATE:

错误现象SQLSTATE常见原因
syntax error at or near ";"42601IF 条件; THEN 或漏写 END IF 的分号
syntax error at or near "END"42601END IF 数量不匹配
control reached end of function without RETURN2F005有返回类型的函数未覆盖所有分支
case not found20000见下一节 CASE,ELSE 缺失且无匹配
null value not allowed22004判断链落空后把 NULL 赋给 NOT NULL 变量

最佳实践 ​

  • 把参数校验集中成开头的守卫段,让主逻辑运行在「前提已成立」的假设下:

    sql
    IF p_qty IS NULL OR p_qty <= 0 THEN
      RAISE EXCEPTION '数量非法:%', p_qty USING ERRCODE = '22023';
    END IF;
  • ELSIF 链从最严格的条件开始排,把最不可能命中的放最前,最宽松的放最后:

    sql
    IF p_score >= 90 THEN ...
    ELSIF p_score >= 80 THEN ...
    ELSE ...
    END IF;
  • 需要区分「未知」与「假」时显式判 IS NULL,不要指望 NOT 或 <>:

    sql
    IF p_flag IS NULL THEN
      RAISE EXCEPTION '参数缺失';
    ELSIF p_flag THEN
      ...
    END IF;
  • 用提前 RETURN 取代嵌套 ELSE,让主逻辑保持浅缩进:

    sql
    IF NOT FOUND THEN
      RETURN format('id=%s 不存在', p_id);
    END IF;
    -- 主逻辑不再缩进
  • 纯值映射优先用 SQL CASE 表达式,可以内联在 SELECT / UPDATE 里,省掉一整段 PL/pgSQL:

    sql
    SELECT CASE WHEN score >= 90 THEN 'A' ELSE 'F' END FROM demo_grades;
  • 把互斥条件写成同一条 IF-ELSIF 链,而不是多个并列的 IF。并列 IF 会让「条件是否真的互斥」不可见,也可能重复执行:

    sql
    -- 推荐:命中即停,语义清晰
    IF v_type = 'A' THEN ...
    ELSIF v_type = 'B' THEN ...
    END IF;
    
    -- 不推荐:两条都会检查,且无法保证互斥
    IF v_type = 'A' THEN ... END IF;
    IF v_type = 'B' THEN ... END IF;
  • 正交的业务维度拆成独立的判断块,避免分支数相乘。等级一档、金额一档,两段 IF 就够了。

  • RAISE 时带上出错的具体值。RAISE EXCEPTION '非法状态:%', v_status 比 RAISE EXCEPTION '非法状态' 的排查效率高一个量级。

  • 给 RAISE EXCEPTION 指定合适的 ERRCODE,让调用方能按错误类别处理,而不是解析错误文本:

    sql
    RAISE EXCEPTION '参数非法' USING ERRCODE = '22023';   -- invalid_parameter_value
  • 分支数超过五六个就考虑配置表。把「条件 → 结果」放进表里,用一次走索引的查询代替长 ELSIF,业务方也能自行维护。

  • 对复杂的条件表达式抽成局部变量,让 IF 本身保持可读:

    sql
    v_is_valid := p_qty IS NOT NULL AND p_qty > 0 AND p_qty <= 10000;
    IF v_is_valid THEN ...

下一节

下一节我们学习:CASE 表达式,掌握简单 CASE 与搜索 CASE 两种形态,以及 CASE_NOT_FOUND 的触发条件。

参考链接 ​