Skip to content

RAISE 错误报告 ​

RAISE 是 PL/pgSQL 里唯一能主动「说话」的语句:它可以输出调试日志,也可以抛出错误中止事务。同一个关键字覆盖 DEBUG 到 EXCEPTION 六个严重级别,通过 USING 还能附带 DETAIL、HINT 与自定义 ERRCODE。掌握 RAISE 的六种语法变体与消息措辞规范,函数报错才会既对调用方友好、又便于排障。

前置知识

阅读本节前,建议先了解:异常处理(EXCEPTION)

基础概念 ​

RAISE 的两类用途 ​

用途典型级别效果
输出信息(调试、进度、告警)DEBUG / LOG / INFO / NOTICE / WARNING只发消息,事务继续
抛出错误(中止操作)EXCEPTION报错并中止当前事务(可被 handler 捕获)

默认级别是 EXCEPTION——所以 RAISE '出错了'; 等价于 RAISE EXCEPTION '出错了';,直接抛错。

六个严重级别与可见性 ​

text
DEBUG   ── 最低,通常只写服务器日志(需把 log_min_messages 调到 debug 才麻烦)
LOG     ── 写服务器日志;客户端一般看不到
INFO    ── 总是发给客户端
NOTICE  ── 默认发给客户端(最常用于调试输出)
WARNING ── 发给客户端,客户端通常显示为警告
EXCEPTION ── 抛错并中止当前事务

可见性由两个配置参数控制:

参数作用
client_min_messages控制哪些级别发到客户端
log_min_messages控制哪些级别写进服务器日志
sql
-- 会话级调整:让 NOTICE 显示、让 DEBUG 也显示
SHOW client_min_messages;         -- 默认 notice
SET client_min_messages = debug;

六个语法变体 ​

text
RAISE [ level ] 'format' [, expression [, ... ]] [ USING option = expression [, ... ] ];
RAISE [ level ] condition_name [ USING option = expression [, ... ] ];
RAISE [ level ] SQLSTATE 'sqlstate' [ USING option = expression [, ... ] ];
RAISE [ level ] USING option = expression [, ... ];
RAISE ;

末行那个不带任何参数的 RAISE; 只能在 EXCEPTION 段里使用,作用是原样重新抛出当前正在处理的错误。

语法与用法 ​

格式串形式 ​

sql
RAISE NOTICE 'Calling cs_create_job(%)', v_job_id;

format 必须是简单字符串字面量(不能是表达式);% 会被后续参数的字符串表示依次替换,%% 输出一个字面量 %。占位符数量必须与参数个数一致,否则函数编译时就报错。

sql
RAISE NOTICE '订单 % 金额 %,处理耗时 % ms', v_id, v_amount, v_ms;
RAISE WARNING '进度 %%%', round(v_done * 100.0 / v_total, 1);   -- 输出 "进度 45%"

条件名形式 ​

sql
RAISE division_by_zero;
RAISE WARNING SQLSTATE '22012';

用条件名时,SQLSTATE 由该条件决定;USING MESSAGE = ... 可以补上消息文本。

sql
-- 两种等价写法
RAISE '订单号 % 重复', v_id USING ERRCODE = 'unique_violation';
RAISE '订单号 % 重复', v_id USING ERRCODE = '23505';
-- 第三种等价写法
RAISE unique_violation USING MESSAGE = '订单号 ' || v_id || ' 重复';

USING 选项 ​

选项含义可用变体
MESSAGE设置错误消息文本不能用于「格式串」变体(消息已给出)
DETAIL补充细节信息全部
HINT给出修复建议全部
ERRCODE指定 SQLSTATE(条件名或五字符码)不能用于「条件名」与「SQLSTATE」变体(已给出)
COLUMN / CONSTRAINT / DATATYPE / TABLE / SCHEMA关联对象名全部
sql
RAISE EXCEPTION 'Nonexistent ID --> %', user_id
  USING HINT = 'Please check your user ID';
sql
RAISE EXCEPTION '订单金额不能为负: %', p_amount
  USING ERRCODE    = '23514',
        DETAIL     = format('收到 amount=%s', p_amount),
        HINT       = '请传入 >= 0 的金额',
        TABLE      = 'orders',
        COLUMN     = 'amount',
        CONSTRAINT = 'orders_amount_check';

RAISE; 重新抛出 ​

sql
EXCEPTION
  WHEN others THEN
    INSERT INTO error_log(msg, state) VALUES (SQLERRM, SQLSTATE);
    RAISE;                -- 保留原始消息与 SQLSTATE
END;

与旧版本的差异

PostgreSQL 9.1 之前,不带参数的 RAISE 被解释为「重抛包含当前 handler 的那个块的错误」,导致嵌套 handler 无法捕获它。现在语义已修正为「重抛当前 handler 正在处理的这条错误」。

ASSERT 语句 ​

text
ASSERT condition [ , message ];

ASSERT 是插入调试断言的简写:条件为 false 或 NULL 时抛 ASSERT_FAILURE。它用于检测程序 bug,不用于报告普通错误场景——后者要用 RAISE。

sql
ASSERT v_total >= 0, '累计金额不应为负';

行为由布尔参数 plpgsql.check_asserts 控制,默认 on;设为 off 时 ASSERT 完全不执行。

详细说明 ​

默认级别是 EXCEPTION ​

sql
-- 这三条等价
RAISE 'something went wrong';
RAISE EXCEPTION 'something went wrong';
RAISE USING MESSAGE = 'something went wrong';

别把 RAISE 'xxx' 当调试输出

省略级别会直接抛错中止事务。调试输出一定显式写 RAISE NOTICE / RAISE DEBUG。

% 占位符的规则与常见错误 ​

sql
-- 正确:3 个 % 对 3 个参数
RAISE NOTICE '% / % / %', v_a, v_b, v_c;

-- 正确:百分比用 %%
RAISE NOTICE '完成度 % %%', v_pct;
text
-- 错误!% 数量与参数数量不匹配 —— 编译期报错
-- ERROR:  too many parameters specified for RAISE
text
RAISE NOTICE '数量 %', a, b;   -- 参数多于占位符
RAISE NOTICE '% %', a;         -- 占位符多于参数

NULL 参数会渲染成空字符串(不是字符串 NULL):

text
RAISE NOTICE '[%]', NULL;
NOTICE:  []

用 USING ERRCODE 自定义 SQLSTATE ​

SQLSTATE 不必是预定义码——可以是任意「五位数字和/或大写 ASCII 字母」的组合(00000 除外)。但有两条重要约定:

  • 避开以三个 0 结尾的码:那是类别码(category code),只能通过捕获整个类别来捕获,语义上不适合作为具体错误的码。
  • 自定义码不要撞上内置类:内置类如 22(数据异常)、23(完整性约束违规)、42(语法错误或访问规则违规)、P0(PL/pgSQL 错误)等。自定义业务错误建议使用自定义类(例如 P0 之外的「未分配」区段),避免与 PostgreSQL 未来分配的码冲突。
sql
-- 用自定义码抛出业务错误
RAISE EXCEPTION '配额已用尽,剩余 % 次', v_left
  USING ERRCODE = 'P0001',            -- 默认的 raise_exception 码
        HINT    = '请升级套餐或等待下个月重置';

-- 用自定义类表示应用域错误(示例:以 A 开头的字符类,需自行确保全项目一致)
RAISE EXCEPTION '配额已用尽' USING ERRCODE = 'APP01';

ERRCODE 的合法性

ERRCODE 的值必须是合法的五位 SQLSTATE。全项目要形成统一约定,并在文档里维护一份错误码表,否则调用方无法稳定分支。

错误消息措辞规范 ​

好的错误消息应当让用户/开发者一眼知道「哪里不对、怎么改」:

反例正例
错误订单 12345 不存在
invalid input金额必须 >= 0,收到 -5.00
操作失败库存不足:SKU-001 需要 10 件,现有 3 件

三条经验规则:

  1. 消息说事实,DETAIL 说数据,HINT 说办法。
  2. 不要用消息文本做程序分支(版本升级可能改文案);要用稳定的 ERRCODE。
  3. 不要把敏感信息写进消息(密码、完整身份证号、内部 IP),消息可能被记入日志并被低权限用户看到。
sql
-- 分层组织信息
RAISE EXCEPTION '库存不足'
  USING ERRCODE = '23514',
        DETAIL  = format('sku=%s, need=%s, stock=%s', p_sku, p_qty, v_stock),
        HINT    = '请先补货或减少下单数量';
text
SELECT place_order('SKU-001', 10);
ERROR:  库存不足
DETAIL:  sku=SKU-001, need=10, stock=3
HINT:  请先补货或减少下单数量

RAISE NOTICE 用于调试输出 ​

NOTICE 是最常用的调试级别:默认对客户端可见,又不会中止事务。

sql
CREATE OR REPLACE FUNCTION debug_demo(p_id int)
RETURNS int
LANGUAGE plpgsql AS $$
DECLARE
  v_row record;
BEGIN
  RAISE DEBUG '开始处理 id=%', p_id;         -- 需 client_min_messages=debug 才可见

  SELECT * INTO v_row FROM items WHERE id = p_id;
  IF NOT FOUND THEN
    RAISE LOG 'id=% 未找到,按默认处理', p_id;  -- 只进服务器日志
    RETURN 0;
  END IF;

  RAISE NOTICE 'id=% name=% 处理中', p_id, v_row.name;
  RETURN 1;
END;
$$;
text
SET client_min_messages = debug;
SELECT debug_demo(1);
DEBUG:  开始处理 id=1
NOTICE:  id=1 name=apple 处理中
 debug_demo
------------
          1
(1 row)

各级别的选择建议:

级别何时用
DEBUG开发期细节,生产环境应关闭(client_min_messages 默认不含 debug)
LOG只要进日志、不需要打扰客户端时
INFO需要让客户端无条件看到的提示(不受 client_min_messages 限制)
NOTICE常规进度与调试输出
WARNING可以继续但值得注意的情况(如「已忽略非法行」)
EXCEPTION必须中止操作

INFO 与 NOTICE 的差别

INFO 与 NOTICE 都默认发给客户端,但 INFO 的消息不受 client_min_messages 过滤(INFO 总是发送)。要「无论如何都让调用方看到」时用 INFO。

ASSERT 的用法与开关 ​

sql
CREATE OR REPLACE FUNCTION avg_amount(p_rows int[])
RETURNS numeric
LANGUAGE plpgsql AS $$
DECLARE
  v_sum numeric := 0;
  v_i   int;
BEGIN
  ASSERT array_length(p_rows, 1) > 0, '数组不能为空';

  FOREACH v_i IN ARRAY p_rows LOOP
    v_sum := v_sum + v_i;
  END LOOP;

  ASSERT p_rows IS NOT NULL;   -- 无消息时用默认文案 "assertion failed"
  RETURN v_sum / array_length(p_rows, 1);
END;
$$;
text
SET plpgsql.check_asserts = on;
SELECT avg_amount('{}'::int[]);
ERROR:  assertion failed: 数组不能为空
CONTEXT:  PL/pgSQL function avg_amount(integer[]) line 6 at ASSERT

SET plpgsql.check_asserts = off;
SELECT avg_amount('{}'::int[]);
-- 不再报断言错误,随后的 array_length 为 NULL,结果为 NULL

ASSERT 不是参数校验

ASSERT 会被 plpgsql.check_asserts = off 关掉,因此不能用它做业务参数校验。业务规则用 IF ... THEN RAISE EXCEPTION ... END IF;。

在 handler 里区分「吞掉」与「重抛」 ​

sql
BEGIN
  INSERT INTO counters (key, value) VALUES (p_key, 1);
EXCEPTION
  WHEN unique_violation THEN
    -- 幂等语义:不重抛,静默当成功
    NULL;
END;
sql
BEGIN
  INSERT INTO counters (key, value) VALUES (p_key, 1);
EXCEPTION
  WHEN unique_violation THEN
    RAISE WARNING 'key=% 已存在,改为累加', p_key;
    UPDATE counters SET value = value + 1 WHERE key = p_key;
END;
sql
BEGIN
  INSERT INTO counters (key, value) VALUES (p_key, 1);
EXCEPTION
  WHEN others THEN
    RAISE;      -- 重抛:调用方需要知道失败
END;

用 USING 携带对象名,便于调用方定位 ​

sql
CREATE OR REPLACE FUNCTION add_column_checked(p_table text, p_col text, p_type text)
RETURNS void
LANGUAGE plpgsql AS $$
BEGIN
  IF NOT EXISTS (
    SELECT 1 FROM information_schema.tables
    WHERE table_schema = 'public' AND table_name = p_table
  ) THEN
    RAISE EXCEPTION '表不存在: %', p_table
      USING ERRCODE = '42P01',       -- undefined_table
            TABLE   = p_table,
            HINT    = '请确认表名与 schema 是否正确';
  END IF;

  EXECUTE format('ALTER TABLE %I ADD COLUMN %I %s', p_table, p_col, p_type);
END;
$$;

调用方在 GET STACKED DIAGNOSTICS 里能直接拿到 TABLE_NAME,无需从消息文本里抠字符串。

日志与事务的关系 ​

只有 EXCEPTION 级别会中止事务;其余级别只发消息。但要注意:

sql
BEGIN
  RAISE NOTICE '这条会输出';
  INSERT INTO t VALUES (1);

  RAISE WARNING '这条也会输出';

  RAISE EXCEPTION '这条抛错,整个事务回滚';
  -- 上面的 INSERT 也一起回滚
END;

特殊情况:在带 EXCEPTION 的块内,WARNING / NOTICE 不会被回滚——它们不是数据变更。但块内 INSERT INTO log 这类写操作在异常发生时会被回滚(见 异常处理(EXCEPTION))。

实战示例 ​

场景:带完整诊断信息的库存扣减函数 ​

需求:一个扣减库存的函数,要区分「SKU 不存在」「库存不足」「并发冲突」三类失败,每类都给出明确的消息、细节与建议,并带稳定错误码。

sql
CREATE TABLE products (
  sku    text PRIMARY KEY,
  name   text NOT NULL,
  qty    int  NOT NULL CHECK (qty >= 0)
);

CREATE OR REPLACE FUNCTION deduct_stock(p_sku text, p_qty int)
RETURNS int
LANGUAGE plpgsql AS $$
DECLARE
  v_qty int;
BEGIN
  -- 参数校验:非法输入用 check_violation
  IF p_qty IS NULL OR p_qty <= 0 THEN
    RAISE EXCEPTION '扣减数量必须为正整数'
      USING ERRCODE = '23514',
            DETAIL  = format('收到 qty=%s', p_qty),
            HINT    = '请传入 >= 1 的整数';
  END IF;

  -- 锁行并读取当前库存
  SELECT qty INTO v_qty FROM products WHERE sku = p_sku FOR UPDATE;

  IF NOT FOUND THEN
    RAISE EXCEPTION '商品不存在: %', p_sku
      USING ERRCODE = '23503',
            TABLE   = 'products',
            COLUMN  = 'sku',
            HINT    = '请检查 SKU 拼写或先创建商品';
  END IF;

  IF v_qty < p_qty THEN
    RAISE EXCEPTION '库存不足: %', p_sku
      USING ERRCODE = '23514',
            DETAIL  = format('sku=%s, need=%s, stock=%s', p_sku, p_qty, v_qty),
            HINT    = '请先补货或减少扣减数量',
            TABLE   = 'products',
            COLUMN  = 'qty';
  END IF;

  UPDATE products SET qty = qty - p_qty WHERE sku = p_sku;
  RAISE DEBUG '扣减成功 sku=% qty=% -> %', p_sku, v_qty, v_qty - p_qty;

  RETURN v_qty - p_qty;
END;
$$;
text
INSERT INTO products VALUES ('SKU-001', '苹果', 100);

-- 正常扣减
SELECT deduct_stock('SKU-001', 30);
 deduct_stock
--------------
           70
(1 row)

-- SKU 不存在
SELECT deduct_stock('SKU-999', 1);
ERROR:  商品不存在: SKU-999
HINT:  请检查 SKU 拼写或先创建商品

-- 库存不足
SELECT deduct_stock('SKU-001', 1000);
ERROR:  库存不足: SKU-001
DETAIL:  sku=SKU-001, need=1000, stock=70
HINT:  请先补货或减少扣减数量

-- 参数非法
SELECT deduct_stock('SKU-001', 0);
ERROR:  扣减数量必须为正整数
DETAIL:  收到 qty=0
HINT:  请传入 >= 1 的整数

场景:把错误码读进审计表并做降级处理 ​

需求:一个「调用外部流程」的模拟函数,失败时把错误码与详情写入审计表,然后返回业务降级标记而不是中断整批任务。

sql
CREATE TABLE job_audit (
  job_id     bigint,
  step       text,
  level      text,
  sqlstate   text,
  message    text,
  detail     text,
  hint       text,
  logged_at  timestamptz NOT NULL DEFAULT now()
);

CREATE OR REPLACE FUNCTION run_job_step(p_job_id bigint, p_sku text, p_qty int)
RETURNS text
LANGUAGE plpgsql AS $$
DECLARE
  v_state   text;
  v_msg     text;
  v_detail  text;
  v_hint    text;
  v_left    int;
BEGIN
  BEGIN
    v_left := deduct_stock(p_sku, p_qty);
    RETURN format('OK:%s', v_left);

  EXCEPTION
    WHEN sqlstate '23503' THEN          -- 商品不存在
      GET STACKED DIAGNOSTICS
        v_state = RETURNED_SQLSTATE, v_msg = MESSAGE_TEXT,
        v_detail = PG_EXCEPTION_DETAIL, v_hint = PG_EXCEPTION_HINT;

      INSERT INTO job_audit (job_id, step, level, sqlstate, message, detail, hint)
      VALUES (p_job_id, 'deduct_stock', 'WARN', v_state, v_msg, v_detail, v_hint);

      RETURN 'SKIP:SKU_NOT_FOUND';

    WHEN sqlstate '23514' THEN          -- 库存不足 / 参数非法
      GET STACKED DIAGNOSTICS
        v_state = RETURNED_SQLSTATE, v_msg = MESSAGE_TEXT,
        v_detail = PG_EXCEPTION_DETAIL, v_hint = PG_EXCEPTION_HINT;

      INSERT INTO job_audit (job_id, step, level, sqlstate, message, detail, hint)
      VALUES (p_job_id, 'deduct_stock', 'WARN', v_state, v_msg, v_detail, v_hint);

      RETURN 'SKIP:INSUFFICIENT';

    WHEN others THEN
      -- 未预期错误:落审计后重抛,让批次调度器感知
      GET STACKED DIAGNOSTICS
        v_state = RETURNED_SQLSTATE, v_msg = MESSAGE_TEXT,
        v_detail = PG_EXCEPTION_DETAIL, v_hint = PG_EXCEPTION_HINT;

      INSERT INTO job_audit (job_id, step, level, sqlstate, message, detail, hint)
      VALUES (p_job_id, 'deduct_stock', 'ERROR', v_state, v_msg, v_detail, v_hint);

      RAISE;
  END;
END;
$$;
text
SELECT run_job_step(1001, 'SKU-001', 10);
  run_job_step
---------------
 OK:60
(1 row)

SELECT run_job_step(1002, 'SKU-999', 10);
    run_job_step
--------------------
 SKIP:SKU_NOT_FOUND
(1 row)

SELECT run_job_step(1003, 'SKU-001', 99999);
  run_job_step
-----------------
 SKIP:INSUFFICIENT
(1 row)

SELECT job_id, level, sqlstate, message, detail FROM job_audit ORDER BY job_id;
 job_id | level | sqlstate |     message      |              detail
--------+-------+----------+------------------+-----------------------------------
   1002 | WARN  | 23503    | 商品不存在: SKU-999 | 
   1003 | WARN  | 23514    | 库存不足: SKU-001  | sku=SKU-001, need=99999, stock=60
(2 rows)

注意事项 ​

  • 省略级别当成调试输出:RAISE 'msg' 会抛错中止事务。-- 错误!
    sql
    RAISE 'debug info';        -- 等价于 RAISE EXCEPTION,直接报错
    RAISE NOTICE 'debug info'; -- 才是调试输出
  • % 数量与参数不匹配:编译期即报错。
    sql
    RAISE NOTICE '% %', a;     -- ERROR: too many/few parameters
  • 在 USING 里用 MESSAGE 配格式串变体:MESSAGE 不能用于已给出消息的变体。
    sql
    -- 错误!
    RAISE EXCEPTION 'x=%', v USING MESSAGE = 'y';
  • 在条件名/SQLSTATE 变体里再用 ERRCODE:错误码已给出。-- 错误!
    sql
    RAISE unique_violation USING ERRCODE = '23505';   -- 重复指定
  • 用 ASSERT 做业务校验:plpgsql.check_asserts = off 时会被绕过。-- 危险
    sql
    ASSERT p_qty > 0;                  -- 生产环境可能被关闭
    IF p_qty <= 0 THEN RAISE EXCEPTION ...; END IF;   -- 应该用这个
  • 在 EXCEPTION 段之外使用裸 RAISE;:语法错误。
    sql
    BEGIN
      RAISE;                          -- ERROR: RAISE without parameters cannot be used outside an exception handler
  • 用类别码(以 000 结尾)作为具体错误码:只能整类捕获,语义不合适。-- 不推荐
    sql
    RAISE EXCEPTION 'x' USING ERRCODE = '22000';
  • 用消息文本做程序分支:文案可能随版本/语言变化,调用方会碎。
    sql
    -- 不推荐(客户端按文本匹配)
    -- 推荐:按稳定的 ERRCODE 分支
  • 消息里泄露敏感信息:错误消息会进日志,可能被低权限角色看到。
    sql
    RAISE EXCEPTION '密码校验失败: %', p_password;   -- 危险!

最佳实践 ​

  • 调试输出统一用 NOTICE,细节用 DEBUG:DEBUG 在生产默认不可见,正好适合细粒度追踪。
  • 错误消息用完整的业务语义句子,DETAIL 放数据、HINT 放办法:
    sql
    RAISE EXCEPTION '库存不足: %', p_sku
      USING DETAIL = format('need=%s stock=%s', p_qty, v_stock),
            HINT   = '请先补货';
  • 为每个业务错误定义稳定的 ERRCODE 并维护错误码表:调用方按码分支,不解析文本。
  • 自定义码避开内置类与类别码(000 结尾):优先用未分配的码段,并在团队文档中登记。
  • handler 里优先 RAISE; 重抛:只有确实能降级时才吞掉,并至少 RAISE WARNING 留痕。
  • 给约束类错误补齐 TABLE / COLUMN / CONSTRAINT:调用方可以结构化定位问题,无需字符串解析。
  • ASSERT 只用于内部不变量检查:参数校验、业务规则一律用 RAISE EXCEPTION。
  • 消息文本保持语言一致:整个项目统一中文或统一英文,避免调用方要做多语言匹配。
  • 不要在热路径里 RAISE NOTICE 大量输出:消息要经过格式化与发送,逐行输出会显著拖慢循环。
    sql
    -- 不推荐:每行都发 NOTICE
    -- 推荐:每 1000 行发一次
    IF v_n % 1000 = 0 THEN RAISE NOTICE '已处理 % 行', v_n; END IF;
  • 用 client_min_messages 而非改代码来调整输出:测试环境调 debug,生产保持默认。
    sql
    SET client_min_messages = debug;    -- 会话级,临时排障
  • 对外函数(被 PHP 调用的)错误码要写进接口文档:与 SQLSTATE 一起给出,方便前端做提示文案映射。

下一节

继续学习:触发器概述

参考链接 ​