PostgreSQL 在抛出错误时,除了常见的 ERROR: 前缀和消息文本,还会返回一个五位字符组成的 SQLSTATE。这个代码与官方文档里的 ERRCODE 条件名一一对应,例如 23505 对应 unique_violation,表示唯一约束冲突。实际定位问题时,如果只依赖本地化消息文本,一旦数据库语言环境改变,程序判断就会失效;而 SQLSTATE 是稳定、可预期的,适合写入异常处理逻辑。

本文先梳理错误码的编码结构,再列出高频错误码对照表,最后说明如何在命令行、日志和应用代码中快速获取这些代码,并用它们驱动更可靠的异常处理。
SQLSTATE 与 ERRCODE 的编码结构
SQLSTATE 是 SQL 标准规定的五位状态码,前两个字符表示错误类别,后三个字符表示该类别下的具体条件。PostgreSQL 在实现上遵循了这套规则,并为每个 SQLSTATE 分配了一个可读性更好的 ERRCODE 条件名。比如 23505 属于类别 23(完整性约束违反),具体条件是 505,对应的条件名是 unique_violation。
从使用角度看,ERRCODE 与 SQLSTATE 是一组等价信息:ERRCODE 方便人阅读和书写,SQLSTATE 更适合程序判断。PostgreSQL 在错误日志与应用驱动中通常会同时暴露这两者。需要强调的是,类别号具有快速定位的价值,例如看到 42 开头的错误,基本可以确定是语法或访问规则问题;看到 23 开头,则是约束或数据完整性相关;看到 40 开头,往往与事务回滚或并发冲突有关。熟悉这些类别能减少排查时的搜索范围。
PostgreSQL 源码中的 errcodes.txt 维护完整的映射关系,官方文档也提供按类别排列的错误码列表。并不是所有五位代码都来自 SQL 标准,一些 P 开头的类别是 PostgreSQL 自身扩展,例如 P0001 是 PL/pgSQL 中的 raise_exception。在日常开发中更常接触的还是标准类别的错误码。
高频错误码对照与触发场景
下面是一张高频错误码对照表,覆盖约束违反、事务回滚、语法访问、连接和资源等常见类别。抓取错误码时,可以先从这张表快速判断问题方向。
| SQLSTATE | ERRCODE | 含义 | 典型触发场景 |
|---|---|---|---|
| 23505 | unique_violation | 唯一约束冲突 | INSERT 或 UPDATE 遇到重复键值 |
| 23503 | foreign_key_violation | 外键约束违反 | 插入子表时父表无对应记录,或删除仍有引用的父记录 |
| 23502 | not_null_violation | 非空约束冲突 | 向 NOT NULL 列写入 NULL |
| 23514 | check_violation | 检查约束冲突 | 数据不满足 CHECK 条件 |
| 40001 | serialization_failure | 串行化失败 | 可重复读或可串行化隔离级别下并发写冲突 |
| 40P01 | deadlock_detected | 检测到死锁 | 两个事务互相等待对方持有的锁 |
| 42601 | syntax_error | 语法错误 | SQL 语句书写不符合语法规则 |
| 42703 | undefined_column | 未定义列 | 查询引用了不存在的列名 |
| 42P01 | undefined_table | 未定义表 | FROM 子句引用了不存在的表 |
| 22012 | division_by_zero | 除零错误 | 整数除法或取模分母为 0 |
| 22P02 | invalid_text_representation | 文本表示无效 | 类型转换失败,如将 abc 转为 integer |
| 22001 | string_data_right_truncation | 字符串右截断 | 写入内容超过字符列长度 |
| 53300 | too_many_connections | 连接数过多 | 连接数超过 max_connections 限制 |
| 57014 | query_canceled | 查询被取消 | 用户主动取消或达到 statement_timeout |
| 53100 | disk_full | 磁盘满 | 写数据文件、临时文件或 WAL 时空间不足 |
在这张表中,23505 和 23503 是写入链路最常见的两类错误。比如用户导入数据时,重复记录会触发 unique_violation;删除主表记录时如果还有子表引用,就会触发 foreign_key_violation。对于前者,可以在应用层做预检查,也可以直接利用 INSERT ... ON CONFLICT DO NOTHING 或 DO UPDATE 把冲突转成幂等操作。对于后者,通常需要先处理子表数据,再删除主表记录。
并发方面的 40001 与 40P01 需要特别区分。死锁检测会回滚其中一个事务,数据库会自动处理,但应用必须实现重试。串行化失败则主要出现在高隔离级别下,重试幅度可能要覆盖整个事务逻辑,而不仅是单条 SQL。资源类错误如 53300 常被误认为是应用连接池配置问题,实际上首先应查看数据库当前连接数,并确认 max_connections 是否留有足够余量。
如何从不同来源获取错误码
在 psql 交互终端中,默认的错误输出只显示 ERROR: 和消息文本,看不到 SQLSTATE。可以通过 \set VERBOSITY verbose 让错误信息包含错误码、位置等更多细节。这个设置对当前会话有效,适合临时排查。
\set VERBOSITY verbose SELECT * FROM no_such_table;
设置后,psql 会返回类似下面的信息:错误码 42P01 出现在消息前端,紧接着是错误文本,然后是行号和 LOCATION 等内部定位信息。虽然 LOCATION 一般只在源码排查时有用,但它能帮助确认问题是不是由 PostgreSQL 内部的某个解析或执行阶段抛出。
对于服务器日志,同样可以通过参数 log_error_verbosity 控制详细程度。将其设置为 verbose 后,PostgreSQL 会在日志文件中记录 SQLSTATE、源文件和行号。生产环境通常建议保持 default 的日志量,但在做复杂故障复盘时可以临时开启,收集足够上下文后再调整回来。
应用层获取错误码则更直接。以 Python 的 psycopg2 驱动为例,捕获异常后可以通过 pgcode 属性拿到 SQLSTATE,通过 pgerror 拿到人类可读消息。Java 的 JDBC 驱动则通过 SQLException.getSQLState() 提供同样能力。下面分别给出最小示例。
import psycopg2
conn = psycopg2.connect(dbname="appdb", user="appuser")
cur = conn.cursor()
try:
cur.execute("INSERT INTO users(email) VALUES ('a@ipipp.com')")
conn.commit()
except psycopg2.Error as e:
print(e.pgcode) # 23505
print(e.pgerror) # 完整错误消息
print(e.diag.message_primary)
conn.rollback()
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
public class PgErrorDemo {
public static void main(String[] args) {
try (Connection conn = DriverManager.getConnection("jdbc:postgresql://localhost/appdb", "appuser", "secret");
Statement stmt = conn.createStatement()) {
stmt.executeUpdate("INSERT INTO users(email) VALUES ('a@ipipp.com')");
} catch (SQLException e) {
System.out.println(e.getSQLState()); // 23505
System.out.println(e.getMessage());
}
}
}
多数 PostgreSQL 驱动在异常对象内部已经解析好 SQLSTATE,不需要再手工从消息文本中截取。这样就能把错误码作为一个稳定的机器可读字段保存到日志系统或告警规则中。
错误码驱动的异常处理实践
在数据库内部编写 PL/pgSQL 函数或过程时,可以用 EXCEPTION WHEN 直接捕获 ERRCODE 条件名,也可以使用 SQLSTATE '23505' 这种写法。下面示例在插入用户时捕获唯一约束冲突,并输出一个更友好的提示,而不是把原始错误直接抛出。
DO $$
DECLARE
v_err_code text;
BEGIN
INSERT INTO users(email) VALUES ('a@ipipp.com');
EXCEPTION
WHEN unique_violation THEN
v_err_code := SQLSTATE;
RAISE NOTICE 'Duplicate entry, code: %', v_err_code;
-- 这里可以执行记录日志或返回自定义响应
END
$$;
需要注意的是,PL/pgSQL 的异常块是通过子事务实现的,每进入一个带 EXCEPTION 的块,PostgreSQL 都需要额外的保存点开销。如果在一个高频写入路径上对每条 INSERT 都包一层异常捕获,性能会有明显下降。更好的做法是先用约束检查或 ON CONFLICT 处理可预见的冲突,把异常捕获留给真正的意外场景。
在应用层,错误码的更大价值是建立统一的映射关系。比如把 23505 映射为“邮箱已被注册”,把 23503 映射为“关联数据不存在”,把 40001 和 40P01 映射为“系统繁忙,请稍后重试”。这样做的好处是,无论数据库返回的本地化消息如何变化,用户看到的提示语都能保持一致,同时便于监控系统按照错误码聚合统计。不要把数据库原始消息直接展示给最终用户,这既可能泄露表结构信息,也会让错误提示风格难以统一。
最后再强调一点:错误码虽然稳定,但并不是所有异常都能被预设处理。对于 XX000 这类内部错误,或者无法识别的 SQLSTATE,应保留完整错误上下文并记录日志,而不是简单吞掉。合理的策略是:能识别的高频错误码走用户友好的分支处理,未知错误码统一走兜底处理并触发告警。
PostgreSQL错误码ERRCODESQLSTATE修改时间:2026-09-28 15:58:04