条件分支(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 Logic | NULL 参与比较的结果仍是 NULL |
| 短路求值 | Short-Circuit Evaluation | AND / OR 提前确定结果即停止求值 |
ELSIF | Else-If | 继续检查下一个条件,可多个 |
ELSEIF | —— | ELSIF 的等价拼写,官方明确支持 |
三值逻辑与 IF 的对应关系:
布尔表达式的求值结果 IF 的行为
─────────────────────────────────────────
true → 进入 THEN 分支
false → 跳过 THEN,检查 ELSIF 或走 ELSE
NULL → 与 false 同样处理:跳过 THEN
(这点与「未知不等于假」的直觉相反)最典型的陷阱是「取反判断」:
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) 归一化。
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; t | f | n
---------------------------------+------------------------------------+----------------------
THEN 分支:p_flag 为 true | ELSE 分支:p_flag 为 false | ELSE 分支:p_flag 为 NULL
(1 row)语法与用法
三种形态的完整语法:
-- 形态一: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 ≠ ELSIF | ELSE IF 会开启一个嵌套的 IF,需要两个 END IF |
最小可运行示例——三种形态各一个:
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; a | b | c
-------+-------+--------
满分 | 及格 | 不及格
(1 row)详细说明
IF-THEN:最简单的守卫式判断
IF-THEN 只回答一个问题:「条件成立时要做的事是什么」。条件不成立时什么都不做,继续往下执行。它最常见的用法是守卫(Guard)——在真正干活之前挡掉不合法输入。
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; ok
------
500
(1 row)用守卫而不是把校验散在业务逻辑里,好处是「参数是否合法」这件事在函数开头就一次性确定,后面不必反复判断。
IF-THEN-ELSE:二选一
ELSE 分支承接「条件不为真」的所有情况——包括 false 和 NULL。它保证了无论输入是什么,函数总有一条路可走,这对「必须有返回值」的标量函数尤其重要。
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; 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 不会被求值。
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; s95 | s85 | s65 | s10 | snull
-----+-----+-----+-----+--------
A | B | D | F | 未录入
(1 row)顺序至关重要。如果把 p_score >= 60 放在最前面,那么所有 ≥60 的分数都会被它截获,后面的 >= 90 永远没有机会执行:
-- 错误!条件从宽到严,宽条件会吃掉所有小条件
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)。
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; pos | neg | zero | nul
-----+------+------+----------
正数 | 负数 | 零 | NULL 或其它
(1 row)对应的 IF-THEN-ELSE 也有同样的语义:
IF p_val > 0 THEN
v_msg := '正数';
ELSE
v_msg := '不是正数或为 NULL'; -- NULL 走这里
END IF;如果你需要区分「明确为假」与「未知」,必须显式判断 IS NULL:
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 |
这条规则可以拿来保护「有副作用的表达式」和「可能报错的表达式」:
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; a | b | c
--------------+-------------+--------------
数组长度 3 | 空数组或 NULL | 空数组或 NULL
(1 row)注意短路只在单个布尔表达式内部有效,不能跨 IF 与 ELSIF 传递「已知信息」——ELSIF 的求值环境是完全新的。
嵌套 IF 与 ELSIF 的取舍
ELSIF 本质上就是「扁平化的嵌套 ELSE IF」。两者语义等价,但可读性差别很大。
-- 推荐: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;-- 不推荐:深层嵌套,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 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;-- 需要多个语句、局部变量、提前 RETURN 时用 IF
IF p_score >= 90 THEN
INSERT INTO demo_audit_log(msg) VALUES ('优秀');
RETURN 'A';
END IF;| 场景 | 推荐手段 |
|---|---|
| 纯值映射、可内联在 SQL 中 | SQL CASE 表达式 |
| 需要执行多条语句 | IF 语句 |
需要提前 RETURN / RAISE | IF 语句 |
| 分支很多且只是选值 | 查表(配置表 / VALUES 子查询) |
分支数超过五六个时,考虑把「条件 → 结果」的映射放进一张配置表,用一次查询代替一长串 ELSIF——查询可以走索引,也便于业务方自行维护。
IF 与 RETURN / RAISE 的组合
RETURN 与 RAISE 会立即终止当前函数,因此它们天然充当 IF 链的终点,可以省掉一个 ELSE。
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 更易读。
实战示例
场景一:阶梯折扣计价
需求:根据订单金额与客户等级计算折扣率,折扣率取值规则由业务给定,需要清晰可审计。
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; vip_big | gold_mid | normal_small
---------+----------+--------------
7800.00 | 4680.00 | 100.00
(1 row)-- 审计表记录下每次计算用的折扣率,便于事后复核
SELECT customer_id, amount, tier, rate
FROM demo_discount_log
ORDER BY log_id; 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 直接取消,其余状态禁止取消。
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; step1 | step2
-------------------------+---------------------------
订单 1:pending → paid | 订单 1:paid → shipped
(1 row)非法流转会带上当前状态,定位问题不需要再去查表:
SELECT app_advance_order(2, 'cancelled');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 ";" | 42601 | IF 条件; THEN 或漏写 END IF 的分号 |
syntax error at or near "END" | 42601 | END IF 数量不匹配 |
control reached end of function without RETURN | 2F005 | 有返回类型的函数未覆盖所有分支 |
case not found | 20000 | 见下一节 CASE,ELSE 缺失且无匹配 |
null value not allowed | 22004 | 判断链落空后把 NULL 赋给 NOT NULL 变量 |
最佳实践
把参数校验集中成开头的守卫段,让主逻辑运行在「前提已成立」的假设下:
sqlIF p_qty IS NULL OR p_qty <= 0 THEN RAISE EXCEPTION '数量非法:%', p_qty USING ERRCODE = '22023'; END IF;ELSIF链从最严格的条件开始排,把最不可能命中的放最前,最宽松的放最后:sqlIF p_score >= 90 THEN ... ELSIF p_score >= 80 THEN ... ELSE ... END IF;需要区分「未知」与「假」时显式判
IS NULL,不要指望NOT或<>:sqlIF p_flag IS NULL THEN RAISE EXCEPTION '参数缺失'; ELSIF p_flag THEN ... END IF;用提前
RETURN取代嵌套ELSE,让主逻辑保持浅缩进:sqlIF NOT FOUND THEN RETURN format('id=%s 不存在', p_id); END IF; -- 主逻辑不再缩进纯值映射优先用 SQL
CASE表达式,可以内联在SELECT/UPDATE里,省掉一整段 PL/pgSQL:sqlSELECT 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,让调用方能按错误类别处理,而不是解析错误文本:sqlRAISE EXCEPTION '参数非法' USING ERRCODE = '22023'; -- invalid_parameter_value分支数超过五六个就考虑配置表。把「条件 → 结果」放进表里,用一次走索引的查询代替长
ELSIF,业务方也能自行维护。对复杂的条件表达式抽成局部变量,让
IF本身保持可读:sqlv_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 的触发条件。