异常处理(EXCEPTION)
PL/pgSQL 的 BEGIN ... EXCEPTION ... END 让函数可以在语句失败后继续运行、把错误翻译成业务语义、或者做补偿动作。它强大但有明确的代价:带 EXCEPTION 的块本质是一个子事务(subtransaction),进入与退出都有开销,而且块内已做的数据修改会在异常发生时被整体回滚。理解这两点,才能写出既健壮又不拖慢系统的错误处理代码。
前置知识
阅读本节前,建议先了解:隐式游标(FOR IN)
基础概念
EXCEPTION 块的结构
BEGIN
statements
EXCEPTION
WHEN condition_name [ OR condition_name ... ] THEN
handler_statements
WHEN other_condition THEN
handler_statements
END;BEGIN 到 EXCEPTION 之间的语句是被保护区。只有在这段区域内发生的错误才会被本块的 handler 捕获;EXCEPTION 段内部再出错,则向外层传播。
捕获的作用域:就近匹配
函数最外层块
├─ BEGIN ← 块 A
│ 语句1 ← 若出错,找 A 的 EXCEPTION
│ BEGIN ← 块 B
│ 语句2 ← 若出错,先找 B 的 EXCEPTION
│ EXCEPTION WHEN ... THEN
│ END;
│ 语句3
└─ EXCEPTION WHEN ... THEN
END;规则很直接:错误先在当前块找 handler;找不到就往上一层块的 EXCEPTION 段找;一直找到函数最外层;仍找不到就报给调用者。
子事务的本质
这是本节最重要的概念:
BEGIN -- 不是子事务
...
BEGIN -- ★ 子事务开始(带 EXCEPTION 的块)
INSERT ...;
UPDATE ...;
EXCEPTION -- 出错时,回滚到 ★ 的位置
WHEN others THEN ...
END; -- ★ 子事务结束
END;BEGIN ... EXCEPTION ... END 相当于 SQL 的 SAVEPOINT / ROLLBACK TO SAVEPOINT:
| 动作 | 等价 SQL |
|---|---|
进入带 EXCEPTION 的块 | SAVEPOINT s |
| 块内某语句报错 | ROLLBACK TO SAVEPOINT s |
| 块正常结束 | RELEASE SAVEPOINT s |
性能警告:子事务不便宜
每次进入带 EXCEPTION 的块都要分配子事务资源,退出要释放。在循环体里放带 EXCEPTION 的块,会让子事务开销乘以行数——这是 PL/pgSQL 里最典型的性能陷阱之一。不写 EXCEPTION 就完全没有这项开销。
关键概念速查
| 概念 | 说明 |
|---|---|
| 条件名(condition name) | unique_violation 这类可读名称,见错误码附录 |
SQLSTATE | 五字符错误码,如 23505 |
others | 通配所有未被前面 WHEN 匹配的错误 |
GET STACKED DIAGNOSTICS | 在 handler 里读取错误的详细信息 |
RAISE; | 在 handler 里原样重新抛出当前错误 |
| 回滚范围 | 捕获异常后,块内已做的修改被回滚到块开始前 |
语法与用法
完整语法
[ <<label>> ]
[ DECLARE
declarations ]
BEGIN
statements
EXCEPTION
WHEN condition [ OR condition ... ] THEN
handler_statements
[ WHEN condition [ OR condition ... ] THEN
handler_statements
... ]
END [ label ];条件名与 SQLSTATE 的写法
EXCEPTION
WHEN unique_violation THEN ... -- 用条件名
WHEN SQLSTATE '23505' THEN ... -- 用 SQLSTATE
WHEN division_by_zero OR no_data_found THEN ... -- 多个条件用 OR
WHEN others THEN ... -- 通配
END;GET STACKED DIAGNOSTICS
GET STACKED DIAGNOSTICS variable { = | := } item [ , ... ];只能在 EXCEPTION 段里使用,用来读取「当前正在处理的错误」的详细信息。
EXCEPTION
WHEN unique_violation THEN
GET STACKED DIAGNOSTICS
v_msg = MESSAGE_TEXT,
v_state = RETURNED_SQLSTATE,
v_detail = PG_EXCEPTION_DETAIL,
v_hint = PG_EXCEPTION_HINT,
v_context = PG_EXCEPTION_CONTEXT,
v_constraint = CONSTRAINT_NAME,
v_table = TABLE_NAME,
v_column = COLUMN_NAME;
RAISE WARNING '% | % | %', v_state, v_table, v_msg;
END;RAISE; 重新抛出
EXCEPTION
WHEN others THEN
INSERT INTO error_log(...) VALUES (...);
RAISE; -- 原样重新抛出,保留原始 SQLSTATE 与消息
END;详细说明
捕获范围:只有被保护区的语句受保护
CREATE OR REPLACE FUNCTION guarded()
RETURNS void
LANGUAGE plpgsql AS $$
BEGIN
RAISE NOTICE 'A: 不在此块的保护区之外';
BEGIN
-- 保护区开始
INSERT INTO t (id) VALUES (1);
INSERT INTO t (id) VALUES (1); -- 违反主键
RAISE NOTICE 'B: 不会执行到这里';
EXCEPTION
WHEN unique_violation THEN
RAISE NOTICE 'C: 捕获到唯一约束冲突';
END;
RAISE NOTICE 'D: 回到外层,继续执行';
END;
$$;SELECT guarded();
NOTICE: A: 不在此块的保护区之外
NOTICE: C: 捕获到唯一约束冲突
NOTICE: D: 回到外层,继续执行
guarded
---------
(1 row)关键点:第二条 INSERT 报错时,第一条 INSERT 的效果也被回滚了(见下节)。
异常被捕获后,块内修改会被回滚
这是很多人第一次写异常处理时踩的坑:
CREATE OR REPLACE FUNCTION partial_rollback_demo()
RETURNS void
LANGUAGE plpgsql AS $$
BEGIN
BEGIN
INSERT INTO t (id, note) VALUES (9001, '第一行');
INSERT INTO t (id, note) VALUES (9002, '第二行');
INSERT INTO t (id, note) VALUES (9002, '重复主键'); -- 报错
EXCEPTION
WHEN unique_violation THEN
-- 9001 也不会留下!
RAISE NOTICE '捕获异常,块内所有修改已被回滚';
END;
END;
$$;SELECT partial_rollback_demo();
NOTICE: 捕获异常,块内所有修改已被回滚
partial_rollback_demo
-----------------------
(1 row)
SELECT count(*) FROM t WHERE id IN (9001, 9002);
count
-------
0想要「部分保留」必须自己设计
捕获异常后块内修改全被回滚。若想让「第一条插入成功、第二条失败」这种语义成立,需要把每条语句放进各自独立的 BEGIN ... EXCEPTION 块,或改用 ON CONFLICT DO NOTHING 之类的非异常路径。
-- 独立子事务:第一条保留,第二条失败不影响它
BEGIN
INSERT INTO t (id, note) VALUES (9001, '第一行');
EXCEPTION WHEN unique_violation THEN NULL;
END;
BEGIN
INSERT INTO t (id, note) VALUES (9002, '第二行');
EXCEPTION WHEN unique_violation THEN NULL;
END;子事务开销:不要在循环体里滥用
-- 不推荐:每次循环都建一个子事务
FOR r IN SELECT * FROM rows_to_import LOOP
BEGIN
INSERT INTO target VALUES (r.*);
EXCEPTION
WHEN unique_violation THEN
INSERT INTO target_conflict VALUES (r.*);
END;
END LOOP;10 万行就是 10 万次子事务建立/释放。更省的替代:
-- 推荐:用 ON CONFLICT 走非异常路径
INSERT INTO target
SELECT * FROM rows_to_import
ON CONFLICT DO NOTHING; -- 或 DO UPDATE SET ...
INSERT INTO target_conflict
SELECT r.* FROM rows_to_import r
WHERE EXISTS (SELECT 1 FROM target t WHERE t.id = r.id);-- 推荐:确实需要异常处理时,把块放在循环外
BEGIN
INSERT INTO target SELECT * FROM rows_to_import ON CONFLICT DO NOTHING;
EXCEPTION
WHEN others THEN
RAISE WARNING '批量导入失败,已整体回滚';
RAISE;
END;判据
只有「错误真的会发生且你需要分支」时才用 EXCEPTION。如果错误能用 ON CONFLICT、DISTINCT ON、WHERE NOT EXISTS、先查后插等方式避免,就不要制造异常。
错误条件名与 SQLSTATE 对照
常用条件名(condition name)与 SQLSTATE:
| 条件名 | SQLSTATE | 触发场景 |
|---|---|---|
unique_violation | 23505 | 唯一索引/主键冲突 |
foreign_key_violation | 23503 | 外键引用违规 |
check_violation | 23514 | CHECK 约束不满足 |
not_null_violation | 23502 | NOT NULL 列被写 NULL |
exclusion_violation | 23P01 | EXCLUDE 约束冲突 |
division_by_zero | 22012 | 除以零 |
no_data_found | P0002 | SELECT INTO 无行 |
too_many_rows | P0003 | SELECT INTO 返回多行 |
raise_exception | P0001 | RAISE EXCEPTION 未指定 SQLSTATE 时的默认码 |
assert_failure | P0004 | ASSERT 失败 |
others | (无) | 通配所有其他错误 |
EXCEPTION
WHEN not_null_violation OR check_violation THEN
RAISE EXCEPTION '数据校验失败' USING ERRCODE = '23514';
WHEN foreign_key_violation THEN
RAISE EXCEPTION '引用了不存在的父记录' USING ERRCODE = '23503';
WHEN others THEN
RAISE;
END;no_data_found 与 too_many_rows 的来源
它们由 SELECT INTO(无 STRICT 时只报 too_many_rows;加 STRICT 后两者都可能)以及 FETCH 之外的赋值语句抛出。普通 SELECT INTO 无行时不报错,只是把目标置 NULL、FOUND 置 false。
GET STACKED DIAGNOSTICS 的可用项
在 EXCEPTION 段里能读取的诊断项:
| 诊断项 | 含义 |
|---|---|
MESSAGE_TEXT | 主错误消息文本 |
RETURNED_SQLSTATE | 错误的 SQLSTATE |
COLUMN_NAME | 相关列名 |
CONSTRAINT_NAME | 相关约束名 |
PG_DATATYPE_NAME | 相关数据类型名 |
TABLE_NAME | 相关表名 |
SCHEMA_NAME | 相关 schema 名 |
PG_EXCEPTION_DETAIL | 错误的 DETAIL 文本 |
PG_EXCEPTION_HINT | 错误的 HINT 文本 |
PG_EXCEPTION_CONTEXT | 错误上下文(调用栈/执行位置) |
CREATE OR REPLACE FUNCTION diagnose_conflict(p_id int)
RETURNS text
LANGUAGE plpgsql AS $$
DECLARE
v_state text;
v_msg text;
v_detail text;
v_hint text;
v_constraint text;
v_table text;
v_column text;
v_context text;
BEGIN
INSERT INTO demo (id, email) VALUES (p_id, 'a@example.com');
RETURN 'ok';
EXCEPTION
WHEN others THEN
GET STACKED DIAGNOSTICS
v_state = RETURNED_SQLSTATE,
v_msg = MESSAGE_TEXT,
v_detail = PG_EXCEPTION_DETAIL,
v_hint = PG_EXCEPTION_HINT,
v_constraint = CONSTRAINT_NAME,
v_table = TABLE_NAME,
v_column = COLUMN_NAME,
v_context = PG_EXCEPTION_CONTEXT;
RETURN format('sqlstate=%s table=%s column=%s constraint=%s msg=%s hint=%s detail=%s context=%s',
v_state, v_table, v_column, v_constraint, v_msg, v_hint, v_detail, v_context);
END;
$$;SELECT diagnose_conflict(1);
-- 输出(单行,实际内容较长,这里折行展示)
sqlstate=23505 table=demo column= constraint=demo_pkey msg=duplicate key value violates
unique constraint "demo_pkey" hint= detail=Key (id)=(1) already exists.
context=SQL statement "INSERT INTO demo (id, email) VALUES (p_id, 'a@example.com')" ...变量名会与诊断项重名
GET STACKED DIAGNOSTICS 的第一个参数是变量,不能写成 SQLSTATE = ...(因为 SQLSTATE 是诊断项名但不是可赋值变量)。所以要用 RETURNED_SQLSTATE 这个项名去读 SQLSTATE。
「吞掉」还是「重抛」:handler 的取舍
三种 handler 策略:
| 策略 | 写法 | 适用 |
|---|---|---|
| 吞掉(swallow) | handler 里不 RAISE,继续走 | 错误可安全忽略(如幂等插入) |
| 转换(translate) | RAISE EXCEPTION ... USING ERRCODE = ... | 把底层错误翻译成业务错误码 |
| 重抛(re-raise) | RAISE; | 记日志后仍要让调用方感知失败 |
-- 吞掉:明确知道可以忽略
EXCEPTION
WHEN unique_violation THEN
NULL; -- 幂等语义:已存在就当作成功
END;-- 转换:保留业务语义
EXCEPTION
WHEN foreign_key_violation THEN
RAISE EXCEPTION '客户 % 不存在', p_customer_id
USING ERRCODE = '23503',
HINT = '请先创建客户记录';
END;-- 重抛:先留痕再抛出
EXCEPTION
WHEN others THEN
INSERT INTO error_log (func, message, state, happened_at)
VALUES (TG_NAME, SQLERRM, SQLSTATE, now());
RAISE;
END;不要无声吞掉
EXCEPTION WHEN others THEN NULL; -- 危险:所有错误都消失,问题被隐藏除非确知「所有可能错误都可忽略」,否则至少 RAISE WARNING 记一笔。
SQLERRM 与 SQLSTATE
在 handler 里可以直接使用两个特殊变量:
EXCEPTION
WHEN others THEN
RAISE WARNING '错误码=%, 消息=%', SQLSTATE, SQLERRM;
-- SQLSTATE 是五字符码,SQLERRM 是错误消息文本
END;它们与 GET STACKED DIAGNOSTICS ... MESSAGE_TEXT / RETURNED_SQLSTATE 等价,但更简短。若要拿 DETAIL / HINT / CONTEXT,则必须用 GET STACKED DIAGNOSTICS。
嵌套块与 handler 的匹配顺序
CREATE OR REPLACE FUNCTION nested_demo()
RETURNS text
LANGUAGE plpgsql AS $$
DECLARE
v_result text := 'unknown';
BEGIN
BEGIN
BEGIN
RAISE EXCEPTION 'inner error' USING ERRCODE = 'P0001';
EXCEPTION
WHEN division_by_zero THEN -- 不匹配 P0001
v_result := 'inner-division';
END; -- 内层没匹配 → 向外抛
EXCEPTION
WHEN raise_exception THEN -- 匹配 P0001
v_result := 'outer-raise';
END;
RETURN v_result; -- 返回 'outer-raise'
END;
$$;要点:内层 WHEN 列表不匹配时,异常原样向外传播,不会被内层块「挡住」。others 会挡住一切,因此 others 必须放在最后。
handler 中不能做的两件事与可以做的事
EXCEPTION
WHEN unique_violation THEN
-- ✅ 可以:查表、插日志、改其他表(都在同一个已回滚的子事务之外执行)
INSERT INTO conflict_log (happened_at) VALUES (now());
-- ✅ 可以:读取诊断信息
-- ✅ 可以:重新抛出
-- ❌ 不能:让被回滚的修改"复活"
-- ❌ 不能:在 handler 里再用 GET STACKED DIAGNOSTICS 读上一次的另一个 handler 的错误
END;注意:handler 里做的修改是提交的(它们发生在回滚点之后),因此「把冲突记录写进日志表」这类补偿动作是有效的。
用异常实现「先试后改」的 upsert
在 ON CONFLICT 出现之前,PL/pgSQL 里常见的 upsert 模式依赖异常捕获。现在更推荐 ON CONFLICT,但了解老模式有助于读懂遗留代码:
-- 老模式:先 UPDATE,没有行再 INSERT(靠 FOUND 而非异常)
CREATE OR REPLACE FUNCTION upsert_counter(p_key text, p_delta int)
RETURNS void
LANGUAGE plpgsql AS $$
BEGIN
UPDATE counters SET value = value + p_delta WHERE key = p_key;
IF NOT FOUND THEN
INSERT INTO counters (key, value) VALUES (p_key, p_delta);
END IF;
END;
$$;-- 更稳的写法:一条语句搞定,无异常、无子事务
INSERT INTO counters (key, value)
VALUES (p_key, p_delta)
ON CONFLICT (key) DO UPDATE SET value = counters.value + EXCLUDED.value;实战示例
场景:批量导入,逐行容错并汇总失败原因
需求:把暂存表里的数据导入目标表,允许部分行失败,最终返回成功/失败行数与失败明细。
先把容错集中在一条异常处理的批量语句上,避免逐行子事务:
CREATE TABLE import_staging (
line_no int,
sku text,
price numeric(12,2),
qty int
);
CREATE TABLE products (
sku text PRIMARY KEY,
price numeric(12,2) NOT NULL CHECK (price >= 0),
qty int NOT NULL CHECK (qty >= 0)
);
CREATE TABLE import_error_log (
line_no int,
sqlstate text,
message text,
detail text,
logged_at timestamptz NOT NULL DEFAULT now()
);
CREATE OR REPLACE FUNCTION import_products()
RETURNS TABLE(ok_rows bigint, bad_rows bigint)
LANGUAGE plpgsql AS $$
DECLARE
v_ok bigint := 0;
v_bad bigint := 0;
BEGIN
-- 先做数据校验,把不合格行记下来(不产生异常,无子事务开销)
INSERT INTO import_error_log (line_no, sqlstate, message)
SELECT s.line_no, 'PRE001', 'price/qty 为负或为空'
FROM import_staging s
WHERE s.price IS NULL OR s.qty IS NULL OR s.price < 0 OR s.qty < 0;
GET DIAGNOSTICS v_bad = ROW_COUNT;
-- 合格行批量写入,用 ON CONFLICT 处理重复,不制造异常
WITH valid AS (
SELECT s.*
FROM import_staging s
WHERE s.price IS NOT NULL AND s.qty IS NOT NULL
AND s.price >= 0 AND s.qty >= 0
), ins AS (
INSERT INTO products (sku, price, qty)
SELECT v.sku, v.price, v.qty FROM valid v
ON CONFLICT (sku) DO UPDATE
SET price = EXCLUDED.price,
qty = EXCLUDED.qty
RETURNING 1
)
SELECT count(*) INTO v_ok FROM ins;
RETURN QUERY SELECT v_ok, v_bad;
END;
$$;调用与输出:
INSERT INTO import_staging VALUES
(1, 'SKU-001', 19.90, 100),
(2, 'SKU-002', 5.50, 200),
(3, 'SKU-003', -1.00, 50),
(4, 'SKU-004', 8.80, NULL);
SELECT * FROM import_products();
ok_rows | bad_rows
---------+----------
2 | 2
(1 row)
SELECT line_no, sqlstate, message FROM import_error_log ORDER BY line_no;
line_no | sqlstate | message
---------+----------+--------------------------------
3 | PRE001 | price/qty 为负或为空
4 | PRE001 | price/qty 为负或为空
(2 rows)场景:把数据库错误翻译成业务错误码
需求:一个下单函数,要把「库存不足」「客户不存在」「重复下单」分别翻译成稳定的业务错误码返回给 PHP 层。
CREATE TABLE app_error (
code text PRIMARY KEY,
message text NOT NULL
);
CREATE OR REPLACE FUNCTION place_order(p_customer_id int, p_sku text, p_qty int)
RETURNS text
LANGUAGE plpgsql AS $$
DECLARE
v_stock int;
v_result text;
BEGIN
-- 客户存在性(显式检查优于依赖异常)
IF NOT EXISTS (SELECT 1 FROM customers WHERE id = p_customer_id) THEN
RETURN 'E_CUSTOMER_NOT_FOUND';
END IF;
-- 库存预检
SELECT qty INTO v_stock FROM products WHERE sku = p_sku FOR UPDATE;
IF NOT FOUND THEN
RETURN 'E_SKU_NOT_FOUND';
END IF;
IF v_stock < p_qty THEN
RETURN 'E_OUT_OF_STOCK';
END IF;
BEGIN
-- 保护区:只包住可能因约束冲突失败的核心操作
INSERT INTO orders (customer_id, sku, qty, status)
VALUES (p_customer_id, p_sku, p_qty, 'created');
UPDATE products SET qty = qty - p_qty WHERE sku = p_sku;
v_result := 'OK';
EXCEPTION
WHEN unique_violation THEN
-- 把约束名/表名带进日志,便于定位是哪个唯一约束
GET STACKED DIAGNOSTICS
v_result = CONSTRAINT_NAME;
RAISE WARNING '重复下单,约束=%', v_result;
v_result := 'E_DUPLICATE_ORDER';
WHEN check_violation THEN
v_result := 'E_INVALID_QTY';
WHEN others THEN
-- 未预期的错误:留痕后重抛,让调用方看到真实错误
INSERT INTO app_error (code, message)
VALUES ('E_UNEXPECTED', SQLERRM)
ON CONFLICT (code) DO UPDATE SET message = EXCLUDED.message;
RAISE;
END;
RETURN v_result;
END;
$$;-- 正常下单
SELECT place_order(101, 'SKU-001', 2);
place_order
-------------
OK
(1 row)
-- 库存不足
SELECT place_order(101, 'SKU-001', 999999);
place_order
-----------------
E_OUT_OF_STOCK
(1 row)
-- 重复下单(假设 (customer_id, sku, status) 有唯一约束)
SELECT place_order(101, 'SKU-001', 2);
WARNING: 重复下单,约束=orders_customer_sku_status_key
place_order
------------------------
E_DUPLICATE_ORDER
(1 row)注意事项
- 在循环体里放
EXCEPTION块:子事务开销乘以行数,是最常见的性能陷阱。-- 不推荐sqlFOR r IN SELECT * FROM big_table LOOP BEGIN INSERT INTO t VALUES (r.*); EXCEPTION WHEN others THEN NULL; END; END LOOP; - 误以为块内之前的成功语句会保留:捕获异常后它们同样被回滚。sql
BEGIN INSERT INTO a VALUES (1); -- 出错后这条也白做 INSERT INTO b VALUES (1/0); EXCEPTION WHEN others THEN NULL; END; WHEN others放在前面:后面的WHEN永远不生效。-- 错误!sqlEXCEPTION WHEN others THEN ... WHEN unique_violation THEN ... -- 永远不会执行- 无条件吞掉所有错误:掩盖真实故障。
-- 危险sqlEXCEPTION WHEN others THEN NULL; - handler 里把
SQLSTATE当变量赋值:混淆了「变量」与「诊断项」。sql-- 错误!SQLSTATE 是诊断项名,不是赋值目标 GET STACKED DIAGNOSTICS SQLSTATE = RETURNED_SQLSTATE; -- 正确 GET STACKED DIAGNOSTICS v_state = RETURNED_SQLSTATE; - 在 handler 里再抛新异常而丢掉原始信息:调试时找不到根因。
-- 不推荐sqlEXCEPTION WHEN others THEN RAISE EXCEPTION '操作失败'; -- 丢失 SQLSTATE / DETAIL / CONTEXT END; -- 推荐:保留底层信息 EXCEPTION WHEN others THEN RAISE EXCEPTION '操作失败: % (%)', SQLERRM, SQLSTATE; END; - handler 改数据后忘记返回/继续:handler 执行完会继续到
END之后,不会自动返回错误给调用方——若失败语义需要向上传,必须RAISE。 - 依赖异常做流程控制:把「看起来可能失败」当成正常分支会拖慢性能并掩盖 bug。
- 在
EXCEPTION段里使用GET DIAGNOSTICS(非 STACKED):拿不到异常信息,必须用GET STACKED DIAGNOSTICS。
最佳实践
- 优先用非异常路径表达可预期的冲突:
ON CONFLICT、WHERE NOT EXISTS、先查后改。sqlINSERT INTO t (id, v) VALUES (1, 2) ON CONFLICT (id) DO NOTHING; -- 首选 EXCEPTION只包住真正需要保护的最小代码段:包得越窄,回滚代价与影响面越小。- 把
EXCEPTION移到循环之外:批量操作一次性保护,而不是逐行保护。sqlFOR r IN ... LOOP ... END LOOP; -- 无异常块 -- 需要保护时,把整段放进一个 BEGIN ... EXCEPTION ... END - handler 里一定留痕:至少
RAISE WARNING,重要场景写错误日志表。sqlEXCEPTION WHEN others THEN INSERT INTO error_log(func, message, state) VALUES ('f', SQLERRM, SQLSTATE); RAISE; END; - 能重抛就重抛:调用方需要知道失败;若业务要求「降级成功」,则显式在 handler 里做补偿并返回明确的业务标记。
- 用具体条件名而不是
others:精确的表意让代码自解释,也避免误吞其他错误。sqlEXCEPTION WHEN unique_violation THEN ... WHEN foreign_key_violation THEN ... WHEN others THEN RAISE; -- 兜底重抛 END; - 自定义错误码时避开内置类:用
USING ERRCODE = '...'时选不易与 PostgreSQL 冲突的码段,详见 RAISE 错误报告。 - 把错误详情用
GET STACKED DIAGNOSTICS结构化落库:只写SQLERRM会丢掉约束名、列名、上下文,排障效率差异巨大。 - 为业务错误划分稳定错误码:PHP 层按错误码分支,而不是去匹配消息文本(消息文本可能随版本变化)。
- 在测试里覆盖异常分支:用
DO $$ ... $$或临时表模拟约束冲突,确认 handler 行为与返回码符合预期。
下一节
继续学习:RAISE 错误报告
参考链接
- PostgreSQL 官方文档:Trapping Errors
- PostgreSQL 官方文档:Errors and Messages
- PostgreSQL 官方文档:Appendix A. PostgreSQL Error Codes
- PostgreSQL 官方文档:Obtaining Execution Location Information
- PostgreSQL 官方文档:Transaction Management(子事务)
- PostgreSQL 官方文档:INSERT(ON CONFLICT)
- PostgreSQL 官方文档:SAVEPOINT
- PostgreSQL 官方文档:CREATE TABLE(约束)