RAISE 错误报告
RAISE 是 PL/pgSQL 里唯一能主动「说话」的语句:它可以输出调试日志,也可以抛出错误中止事务。同一个关键字覆盖 DEBUG 到 EXCEPTION 六个严重级别,通过 USING 还能附带 DETAIL、HINT 与自定义 ERRCODE。掌握 RAISE 的六种语法变体与消息措辞规范,函数报错才会既对调用方友好、又便于排障。
前置知识
阅读本节前,建议先了解:异常处理(EXCEPTION)
基础概念
RAISE 的两类用途
| 用途 | 典型级别 | 效果 |
|---|---|---|
| 输出信息(调试、进度、告警) | DEBUG / LOG / INFO / NOTICE / WARNING | 只发消息,事务继续 |
| 抛出错误(中止操作) | EXCEPTION | 报错并中止当前事务(可被 handler 捕获) |
默认级别是 EXCEPTION——所以 RAISE '出错了'; 等价于 RAISE EXCEPTION '出错了';,直接抛错。
六个严重级别与可见性
DEBUG ── 最低,通常只写服务器日志(需把 log_min_messages 调到 debug 才麻烦)
LOG ── 写服务器日志;客户端一般看不到
INFO ── 总是发给客户端
NOTICE ── 默认发给客户端(最常用于调试输出)
WARNING ── 发给客户端,客户端通常显示为警告
EXCEPTION ── 抛错并中止当前事务可见性由两个配置参数控制:
| 参数 | 作用 |
|---|---|
client_min_messages | 控制哪些级别发到客户端 |
log_min_messages | 控制哪些级别写进服务器日志 |
-- 会话级调整:让 NOTICE 显示、让 DEBUG 也显示
SHOW client_min_messages; -- 默认 notice
SET client_min_messages = debug;六个语法变体
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 段里使用,作用是原样重新抛出当前正在处理的错误。
语法与用法
格式串形式
RAISE NOTICE 'Calling cs_create_job(%)', v_job_id;format 必须是简单字符串字面量(不能是表达式);% 会被后续参数的字符串表示依次替换,%% 输出一个字面量 %。占位符数量必须与参数个数一致,否则函数编译时就报错。
RAISE NOTICE '订单 % 金额 %,处理耗时 % ms', v_id, v_amount, v_ms;
RAISE WARNING '进度 %%%', round(v_done * 100.0 / v_total, 1); -- 输出 "进度 45%"条件名形式
RAISE division_by_zero;
RAISE WARNING SQLSTATE '22012';用条件名时,SQLSTATE 由该条件决定;USING MESSAGE = ... 可以补上消息文本。
-- 两种等价写法
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 | 关联对象名 | 全部 |
RAISE EXCEPTION 'Nonexistent ID --> %', user_id
USING HINT = 'Please check your user ID';RAISE EXCEPTION '订单金额不能为负: %', p_amount
USING ERRCODE = '23514',
DETAIL = format('收到 amount=%s', p_amount),
HINT = '请传入 >= 0 的金额',
TABLE = 'orders',
COLUMN = 'amount',
CONSTRAINT = 'orders_amount_check';RAISE; 重新抛出
EXCEPTION
WHEN others THEN
INSERT INTO error_log(msg, state) VALUES (SQLERRM, SQLSTATE);
RAISE; -- 保留原始消息与 SQLSTATE
END;与旧版本的差异
PostgreSQL 9.1 之前,不带参数的 RAISE 被解释为「重抛包含当前 handler 的那个块的错误」,导致嵌套 handler 无法捕获它。现在语义已修正为「重抛当前 handler 正在处理的这条错误」。
ASSERT 语句
ASSERT condition [ , message ];ASSERT 是插入调试断言的简写:条件为 false 或 NULL 时抛 ASSERT_FAILURE。它用于检测程序 bug,不用于报告普通错误场景——后者要用 RAISE。
ASSERT v_total >= 0, '累计金额不应为负';行为由布尔参数 plpgsql.check_asserts 控制,默认 on;设为 off 时 ASSERT 完全不执行。
详细说明
默认级别是 EXCEPTION
-- 这三条等价
RAISE 'something went wrong';
RAISE EXCEPTION 'something went wrong';
RAISE USING MESSAGE = 'something went wrong';别把 RAISE 'xxx' 当调试输出
省略级别会直接抛错中止事务。调试输出一定显式写 RAISE NOTICE / RAISE DEBUG。
% 占位符的规则与常见错误
-- 正确:3 个 % 对 3 个参数
RAISE NOTICE '% / % / %', v_a, v_b, v_c;
-- 正确:百分比用 %%
RAISE NOTICE '完成度 % %%', v_pct;-- 错误!% 数量与参数数量不匹配 —— 编译期报错
-- ERROR: too many parameters specified for RAISERAISE NOTICE '数量 %', a, b; -- 参数多于占位符
RAISE NOTICE '% %', a; -- 占位符多于参数NULL 参数会渲染成空字符串(不是字符串 NULL):
RAISE NOTICE '[%]', NULL;
NOTICE: []用 USING ERRCODE 自定义 SQLSTATE
SQLSTATE 不必是预定义码——可以是任意「五位数字和/或大写 ASCII 字母」的组合(00000 除外)。但有两条重要约定:
- 避开以三个 0 结尾的码:那是类别码(category code),只能通过捕获整个类别来捕获,语义上不适合作为具体错误的码。
- 自定义码不要撞上内置类:内置类如
22(数据异常)、23(完整性约束违规)、42(语法错误或访问规则违规)、P0(PL/pgSQL 错误)等。自定义业务错误建议使用自定义类(例如P0之外的「未分配」区段),避免与 PostgreSQL 未来分配的码冲突。
-- 用自定义码抛出业务错误
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 件 |
三条经验规则:
- 消息说事实,
DETAIL说数据,HINT说办法。 - 不要用消息文本做程序分支(版本升级可能改文案);要用稳定的
ERRCODE。 - 不要把敏感信息写进消息(密码、完整身份证号、内部 IP),消息可能被记入日志并被低权限用户看到。
-- 分层组织信息
RAISE EXCEPTION '库存不足'
USING ERRCODE = '23514',
DETAIL = format('sku=%s, need=%s, stock=%s', p_sku, p_qty, v_stock),
HINT = '请先补货或减少下单数量';SELECT place_order('SKU-001', 10);
ERROR: 库存不足
DETAIL: sku=SKU-001, need=10, stock=3
HINT: 请先补货或减少下单数量RAISE NOTICE 用于调试输出
NOTICE 是最常用的调试级别:默认对客户端可见,又不会中止事务。
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;
$$;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 的用法与开关
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;
$$;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,结果为 NULLASSERT 不是参数校验
ASSERT 会被 plpgsql.check_asserts = off 关掉,因此不能用它做业务参数校验。业务规则用 IF ... THEN RAISE EXCEPTION ... END IF;。
在 handler 里区分「吞掉」与「重抛」
BEGIN
INSERT INTO counters (key, value) VALUES (p_key, 1);
EXCEPTION
WHEN unique_violation THEN
-- 幂等语义:不重抛,静默当成功
NULL;
END;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;BEGIN
INSERT INTO counters (key, value) VALUES (p_key, 1);
EXCEPTION
WHEN others THEN
RAISE; -- 重抛:调用方需要知道失败
END;用 USING 携带对象名,便于调用方定位
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 级别会中止事务;其余级别只发消息。但要注意:
BEGIN
RAISE NOTICE '这条会输出';
INSERT INTO t VALUES (1);
RAISE WARNING '这条也会输出';
RAISE EXCEPTION '这条抛错,整个事务回滚';
-- 上面的 INSERT 也一起回滚
END;特殊情况:在带 EXCEPTION 的块内,WARNING / NOTICE 不会被回滚——它们不是数据变更。但块内 INSERT INTO log 这类写操作在异常发生时会被回滚(见 异常处理(EXCEPTION))。
实战示例
场景:带完整诊断信息的库存扣减函数
需求:一个扣减库存的函数,要区分「SKU 不存在」「库存不足」「并发冲突」三类失败,每类都给出明确的消息、细节与建议,并带稳定错误码。
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;
$$;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 的整数场景:把错误码读进审计表并做降级处理
需求:一个「调用外部流程」的模拟函数,失败时把错误码与详情写入审计表,然后返回业务降级标记而不是中断整批任务。
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;
$$;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'会抛错中止事务。-- 错误!sqlRAISE 'debug info'; -- 等价于 RAISE EXCEPTION,直接报错 RAISE NOTICE 'debug info'; -- 才是调试输出 %数量与参数不匹配:编译期即报错。sqlRAISE NOTICE '% %', a; -- ERROR: too many/few parameters- 在
USING里用MESSAGE配格式串变体:MESSAGE不能用于已给出消息的变体。sql-- 错误! RAISE EXCEPTION 'x=%', v USING MESSAGE = 'y'; - 在条件名/SQLSTATE 变体里再用
ERRCODE:错误码已给出。-- 错误!sqlRAISE unique_violation USING ERRCODE = '23505'; -- 重复指定 - 用 ASSERT 做业务校验:
plpgsql.check_asserts = off时会被绕过。-- 危险sqlASSERT p_qty > 0; -- 生产环境可能被关闭 IF p_qty <= 0 THEN RAISE EXCEPTION ...; END IF; -- 应该用这个 - 在
EXCEPTION段之外使用裸RAISE;:语法错误。sqlBEGIN RAISE; -- ERROR: RAISE without parameters cannot be used outside an exception handler - 用类别码(以 000 结尾)作为具体错误码:只能整类捕获,语义不合适。
-- 不推荐sqlRAISE EXCEPTION 'x' USING ERRCODE = '22000'; - 用消息文本做程序分支:文案可能随版本/语言变化,调用方会碎。sql
-- 不推荐(客户端按文本匹配) -- 推荐:按稳定的 ERRCODE 分支 - 消息里泄露敏感信息:错误消息会进日志,可能被低权限角色看到。sql
RAISE EXCEPTION '密码校验失败: %', p_password; -- 危险!
最佳实践
- 调试输出统一用
NOTICE,细节用DEBUG:DEBUG在生产默认不可见,正好适合细粒度追踪。 - 错误消息用完整的业务语义句子,
DETAIL放数据、HINT放办法:sqlRAISE 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,生产保持默认。sqlSET client_min_messages = debug; -- 会话级,临时排障 - 对外函数(被 PHP 调用的)错误码要写进接口文档:与
SQLSTATE一起给出,方便前端做提示文案映射。
下一节
继续学习:触发器概述
参考链接
- PostgreSQL 官方文档:Reporting Errors and Messages(RAISE)
- PostgreSQL 官方文档:Checking Assertions(ASSERT)
- PostgreSQL 官方文档:Appendix A. PostgreSQL Error Codes
- PostgreSQL 官方文档:Trapping Errors(EXCEPTION)
- PostgreSQL 官方文档:client_min_messages
- PostgreSQL 官方文档:log_min_messages
- PostgreSQL 官方文档:CREATE FUNCTION
- PostgreSQL 官方文档:Data Type Formatting Functions(format)