# 指南

> 阅读 PostgreSQL 诊断、恢复事务状态以及选择客户端边界的通用方法。
---

## 先读协议诊断 {#read-diagnostics}

先确认协议消息类型。PostgreSQL 用 `ErrorResponse` 表示错误，用 `NoticeResponse` 表示通知；两者都携带以零字节结束的带标识字段。`C` 是不可本地化的 SQLSTATE，`M` 是主消息，`D`/`H` 分别是可选 detail 与 hint。`S` 可能本地化，`V` 是服务器提供时的未本地化 severity。schema（`s`）、table（`t`）、column（`c`）、constraint（`n`）、position（`P`）、context（`W`）、源码文件（`F`）、行号（`L`）和 routine（`R`）等字段都是有条件的；有就保留，没有就不要推断。参见 [PostgreSQL 18 错误与通知字段](https://www.postgresql.org/docs/18/protocol-error-fields.html) 和[消息格式](https://www.postgresql.org/docs/18/protocol-message-formats.html)。

目录类别只是分类。决定语句是否失败前，要先读取实际协议严重性和消息类型。`WARNING` 或 `NOTICE` 可以作为通知送达，同时命令仍然完成；`ERROR` 作为错误送达，通常会改变显式事务状态。成功的 `CommandComplete` 只有命令标签，不携带 ErrorResponse SQLSTATE。把服务器版本和操作一起保留，因为相同 SQLSTATE 可能对应不同源码路径。

## 恢复正确的事务边界 {#transaction-recovery}

自动提交为每条语句建立独立事务边界。语句错误后，连接通常仍可执行下一条语句。显式事务中的未处理 `ERROR` 会使事务进入失败状态；之后普通命令会被 PostgreSQL 以 `25P02` 拒绝，直到回滚。`25P02` 是后续观察，不是第一条错误的替代品。实际边界应参照 [ROLLBACK](https://www.postgresql.org/docs/18/sql-rollback.html) 和 [SAVEPOINT](https://www.postgresql.org/docs/18/sql-savepoint.html) 文档。

如果一个已经存在的保存点包围了可选内部单元，则执行 `ROLLBACK TO SAVEPOINT name`，然后继续外层事务或释放保存点；保存点以前的工作会保留。没有这样的保存点时，`ROLLBACK` 会结束失败事务，重试必须从新事务开始。已接受的 [25P02 案例](../../data/cases/25p02.json) 展示根错误 `23505`、后续 `25P02` 和恢复；[23505 证据](../../data/evidence/23505.json) 记录更窄的保存点边界。

PL/pgSQL 的 `EXCEPTION` 块提供另一种窄边界。进入处理器前，块内变更会回滚；块以前的外层变更保留。处理器中，`SQLSTATE` 和 `SQLERRM` 表示当前异常；`GET STACKED DIAGNOSTICS` 可读取 `RETURNED_SQLSTATE`、主消息、detail、hint、context 和对象字段。参见[错误捕获](https://www.postgresql.org/docs/18/plpgsql-control-structures.html#PLPGSQL-ERROR-TRAPPING)、[堆叠诊断](https://www.postgresql.org/docs/18/plpgsql-control-structures.html#PLPGSQL-GET-DIAGNOSTICS) 和[RAISE](https://www.postgresql.org/docs/18/plpgsql-errors-and-messages.html)。处理器应只包围能够安全分类的操作；`WHEN OTHERS` 不应抹掉原始字段。

## 只有在结果边界明确时才重试完整单元 {#retry}

重试是业务决策，不是 SQLSTATE 类别的属性。若序列化失败或死锁等原因具有瞬时性且操作可安全重复，应从新快照重试整个事务。不要只重放最后一条语句，因为前面的读取、写入、锁、通知或外部调用可能共同构成一个业务单元。客户端超时或在收到结果前失联时，服务器可能已经提交但客户端没有响应，完成状态不确定；对外部可见操作应使用幂等键、持久业务键或状态查询后再重复。

遇到权限错误、对象不存在或无效数据时，先检查并修复权限、对象或输入，再判断修正后的操作是否安全。已经完成命令的警告不应触发自动重放。保留第一条诊断和事务状态；随后出现的 `25P02` 应作为后果记录。退避和尝试次数上限可以控制负载，却不能使非幂等操作变得安全。服务器明确报告超时或取消表示操作失败；客户端在获知结果前失联则属于完成状态不确定的另一类情况。

## 关联应用与服务器日志 {#logging}

在格式化消息前，记录 SQLSTATE、未本地化 severity、主消息、detail、hint、对象名称、position/context、服务器版本、可用时的 backend PID，以及客户端事务状态。添加应用请求或幂等键和时间戳，便于在服务器日志中找到同一操作。PID、session 标识和 query ID 是关联线索，不能替代协议字段。

CSV 和 JSON 服务器日志是结构化输出，但形状不等同于 ErrorResponse。PostgreSQL 18 的 `csvlog` 列包含 severity、SQLSTATE、message、detail、hint、context、query、源码位置、application name、backend type 和 query ID。`jsonlog` 输出 JSON，并可能省略 null 字段；处理器应忽略未来新增字段。按[日志文档](https://www.postgresql.org/docs/18/runtime-config-logging.html)配置 `log_destination` 和 `logging_collector`，再按 PID/session/request 关联，不要假定 CSV 列名或 JSON 键名与协议字段相同。

## PL/pgSQL 与自定义条件 {#plpgsql}

`RAISE` 可以报告 `DEBUG`、`LOG`、`INFO`、`NOTICE`、`WARNING` 或 `EXCEPTION`；`EXCEPTION` 通常中止当前事务，其他级别只发送消息。它可以指定条件名或五字符 SQLSTATE，并通过 `USING` 设置 `MESSAGE`、`DETAIL`、`HINT`、`SCHEMA`、`TABLE`、`COLUMN`、`DATATYPE` 或 `CONSTRAINT`。PostgreSQL 允许使用除 `00000` 外、由数字和大写 ASCII 字母组成的任意五字符代码，包括自定义代码。末三位全为零的代码是类别码，只能用整个类别捕获。参见 [RAISE 语法](https://www.postgresql.org/docs/18/plpgsql-errors-and-messages.html#PLPGSQL-ERRORS-AND-MESSAGES-RAISE)。

命名处理器匹配指定条件及其别名；类处理器范围更宽。`WHEN OTHERS` 捕获除 `QUERY_CANCELED` 和 `ASSERT_FAILURE` 以外的所有错误条件；通知级消息不是异常，因此不会被它捕获。PL/pgSQL 主动抛出的自定义代码只能证明该函数有意报告，不能证明 core、contrib、FDW、ECPG 或 driver 自然产生同一代码。

## 客户端如何暴露诊断 {#clients}

下表固定到已检查的发行版或主文档；不声称本项目对六个客户端都做过运行测试。

| 客户端 | 检查版本/来源 | 暴露的诊断 | 应保留的边界 |
| --- | --- | --- | --- |
| libpq | PostgreSQL 18.6，commit [`724edf9b`](https://github.com/postgres/postgres/tree/724edf9bde9d356724ad384a2e196edc3c9f80f7) | [`PQresultErrorField`](https://github.com/postgres/postgres/blob/724edf9bde9d356724ad384a2e196edc3c9f80f7/src/interfaces/libpq/libpq-fe.h#L588) 从错误或警告 `PGresult` 读取字段；[`PQstatus`](https://github.com/postgres/postgres/blob/724edf9bde9d356724ad384a2e196edc3c9f80f7/src/interfaces/libpq/libpq-fe.h#L407) 描述连接状态。 | 启动失败可能没有 `PGresult`；结果字段 API 不是通用启动错误 API，应分开记录连接文本和服务器日志。 |
| psycopg | 3.3.5，tag 解析到 commit [`ea542c95`](https://github.com/psycopg/psycopg/tree/ea542c9534cc9841d50450e0c156b549cc6c805a) | [`Error`](https://github.com/psycopg/psycopg/blob/ea542c9534cc9841d50450e0c156b549cc6c805a/psycopg/psycopg/errors.py#L267-L302) 暴露 `sqlstate`、`diag`、`pgconn` 和 `pgresult`；同一源码构造具名 SQLSTATE 异常类。 | 连接错误可能是 `sqlstate=None`，失败查询则可能有结果诊断。 |
| PostgreSQL JDBC | pgjdbc `REL42.7.8`，commit [`9a5492d9`](https://github.com/pgjdbc/pgjdbc/tree/9a5492d99ce43507e318ce4bb56030f74b773d48) | [`PSQLException.java`](https://github.com/pgjdbc/pgjdbc/blob/9a5492d99ce43507e318ce4bb56030f74b773d48/pgjdbc/src/main/java/org/postgresql/util/PSQLException.java#L30-L38) 将 `ServerErrorMessage.getSQLState()` 映射到 JDBC `getSQLState()`，并保留服务器消息。 | 客户端和传输异常也会被包装；要同时看异常类型和 server-message 是否存在。 |
| pgx/pgconn | pgx `v5.7.6`，commit [`a2fca037`](https://github.com/jackc/pgx/tree/a2fca037434a0a7096b095d4ed87cdffb03b626e) | [`errors.go`](https://github.com/jackc/pgx/blob/a2fca037434a0a7096b095d4ed87cdffb03b626e/pgconn/errors.go#L48-L60) 为服务器错误定义 `PgError.SQLState()`。 | 连接、context 和解析错误是其他 Go error 类型或包装；使用 `errors.As`。 |
| node-postgres / pg-protocol | node-postgres `pg@8.16.3`，commit [`8f8e7315`](https://github.com/brianc/node-postgres/tree/8f8e7315e8f7c1bb01e98fdb41c8c92585510782) | [`messages.ts`](https://github.com/brianc/node-postgres/blob/8f8e7315e8f7c1bb01e98fdb41c8c92585510782/packages/pg-protocol/src/messages.ts#L77-L115) 与 [`parser.ts`](https://github.com/brianc/node-postgres/blob/8f8e7315e8f7c1bb01e98fdb41c8c92585510782/packages/pg-protocol/src/parser.ts#L357-L386) 将 `E`/`N` 字段解析为数据库错误和通知消息。 | `code`、severity、detail、hint、对象字段来自服务器诊断；socket 错误是独立 JavaScript error，可能没有 SQLSTATE。 |
| Npgsql | Npgsql `v10.0.0`，commit [`a1802184`](https://github.com/npgsql/npgsql/tree/a18021849f244716d3b68eefd705677f131f9ace) | [`PostgresException.cs`](https://github.com/npgsql/npgsql/blob/a18021849f244716d3b68eefd705677f131f9ace/src/Npgsql/PostgresException.cs#L44-L75) 暴露 `SqlState`；[诊断指南](https://www.npgsql.org/doc/diagnostics/exceptions_notices.html) 区分通知和异常类型。 | `NpgsqlException` 可以包装网络/客户端失败；通知通过 `Notice` 事件到达，不是命令错误。 |

跨客户端看，具名异常类、常量和包装类型只是便利映射；跨客户端证据仍是协议 SQLSTATE 和字段。客户端错误消息或 null SQLSTATE 不会覆盖服务器日志中确认的代码。已有 psycopg 3.3.5/libpq 18.6 启动记录中 driver 为 null、服务器为 `28P01`/`53300`，只证明这一栈的边界，不能外推到其他五个客户端。

---

反链：

- [SQLSTATE 大典](/zh/)
