Skip to content

BEGIN / COMMIT / ROLLBACK ​

事务(Transaction)是把多条 SQL 打包成一个「要么全做、要么全不做」的逻辑单元。本节从最基础的三条命令 BEGIN / COMMIT / ROLLBACK 讲起,覆盖事务块内的数据可见性、序列不回退这个经典坑点、PostgreSQL 相对 MySQL 的一大优势——事务性 DDL,以及事务失败后进入 aborted 状态的处理方式。学完后你能准确判断一段代码到底开了几个事务、出错时该 COMMIT 还是 ROLLBACK。

前置知识

阅读本节前,建议先了解:MVCC 概述、WAL 概述

基础概念 ​

事务是一组被数据库当作一个不可分割单元执行的语句。它由一条开启语句、若干条普通语句和一条结束语句构成:

text
BEGIN ──> 语句 1 ──> 语句 2 ──> ... ──> COMMIT   (全部生效)
                                  └──> ROLLBACK  (全部撤销)

几个必须记住的事实:

事实说明
BEGIN 与 START TRANSACTION 完全等价只是拼写不同,行为一致
COMMIT 与 END 等价END 是 SQL 标准拼写,少见但不报错
ROLLBACK 与 ABORT 等价ABORT 是 PostgreSQL 保留的别名
事务与连接绑定同一个事务里的语句必须在同一个会话/连接上执行
事务内出错后不能继续执行语句必须 ROLLBACK(或回滚到保存点)才能恢复
PostgreSQL 中所有 DDL 都可回滚这是相对 MySQL 最显著的差异之一
序列(Sequence)不受事务控制ROLLBACK 不会回收已分配的自增值

事务的三种终态与命令标签(command tag):

命令psql 返回的标签效果
COMMITCOMMIT提交,变更对所有后续事务可见
ROLLBACKROLLBACK回滚,丢弃事务内全部变更
在 aborted 事务里执行 COMMITROLLBACK实际是回滚(PostgreSQL 会改标签提示你)

一个容易忽略的事实:事务 ID(XID)是惰性分配的。只读事务(没有任何写操作)在 PostgreSQL 中根本不会分配真正的 XID,因此也不会与其它事务产生 XID 环绕(wraparound)相关的压力。这直接影响后续要讲的「只读长事务是否危险」的判断。

语法与用法 ​

text
-- 开启事务
BEGIN [ WORK | TRANSACTION ] [ transaction_mode [, ...] ]
START TRANSACTION [ transaction_mode [, ...] ]

-- transaction_mode 可以是:
--   ISOLATION LEVEL { SERIALIZABLE | REPEATABLE READ | READ COMMITTED | READ UNCOMMITTED }
--   READ WRITE | READ ONLY
--   [ NOT ] DEFERRABLE

-- 提交事务(三种等价写法)
COMMIT [ WORK | TRANSACTION ]
END [ WORK | TRANSACTION ]

-- 回滚事务(三种等价写法)
ROLLBACK [ WORK | TRANSACTION ]
ABORT [ WORK | TRANSACTION ]

最简可运行示例:

sql
BEGIN;

INSERT INTO accounts (id, balance) VALUES (1, 100.00);
UPDATE accounts SET balance = balance - 10.00 WHERE id = 1;

COMMIT;
text
BEGIN
INSERT 0 1
UPDATE 1
COMMIT

把 COMMIT 换成 ROLLBACK,效果完全相反:

sql
BEGIN;

INSERT INTO accounts (id, balance) VALUES (2, 50.00);

ROLLBACK;
text
BEGIN
INSERT 0 1
ROLLBACK

虽然 INSERT 返回了 INSERT 0 1(看起来像是插进去了),但 ROLLBACK 之后这一行不存在:

sql
SELECT * FROM accounts WHERE id = 2;
text
 id | balance
----+---------
(0 rows)

详细说明 ​

BEGIN / START TRANSACTION / BEGIN WORK 完全等价 ​

这三个写法在 PostgreSQL 内部走的是同一条解析路径,没有任何行为差别:

sql
BEGIN;                 -- 最简写法
BEGIN WORK;            -- 加上 WORK
BEGIN TRANSACTION;     -- 加上 TRANSACTION
START TRANSACTION;     -- SQL 标准拼写
text
BEGIN
BEGIN
BEGIN
BEGIN

要在开启事务时指定隔离级别或只读属性,必须用后面两种写法之一:

sql
-- 开启事务时直接指定隔离级别与只读属性
BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY;
text
BEGIN

START TRANSACTION 才能带 transaction_mode 吗?

不是。BEGIN 也能带 transaction_mode,例如 BEGIN ISOLATION LEVEL SERIALIZABLE; 是合法的。规范里 BEGIN WORK 与 START TRANSACTION 都允许 transaction_mode 列表,写哪一种看团队习惯即可。

事务块内:自己能看到自己未提交的修改 ​

这是初学者最容易困惑的一点:事务内的修改对本事务自己可见,对其他事务不可见。

sql
BEGIN;

INSERT INTO accounts (id, balance) VALUES (3, 999.00);

-- 本事务内能查到刚插入的行
SELECT * FROM accounts WHERE id = 3;
text
BEGIN
INSERT 0 1
 id | balance
----+---------
  3 |  999.00
(1 row)

现在另开一个会话(第二个 psql 窗口),在事务尚未提交时查询,看不到任何东西——因为 id = 3 这一行的事务还没提交,MVCC 的快照机制把它隐藏了(判定规则见 可见性规则)。只有会话 A 执行 COMMIT 之后,会话 B 才能看到:

text
会话 B(事务未提交时):
SELECT * FROM accounts WHERE id = 3;
 id | balance
----+---------
(0 rows)

会话 A:
COMMIT;
COMMIT

会话 B(提交之后):
SELECT * FROM accounts WHERE id = 3;
 id | balance
----+---------
  3 |  999.00
(1 row)

结论:未提交的数据永远不会「泄漏」给别的连接,PostgreSQL 也不支持脏读(详见 脏读 / 不可重复读 / 幻读)。

ROLLBACK 之后序列不会回退(重要坑点) ​

序列(Sequence)是独立于事务的对象,它的 nextval() 调用会被立即持久化,不受 ROLLBACK 影响。这是最常被误判的「坑点」。

sql
CREATE TABLE orders (
  id   bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  note text
);

BEGIN;
INSERT INTO orders (note) VALUES ('第一单');
INSERT INTO orders (note) VALUES ('第二单');
ROLLBACK;
text
CREATE TABLE
BEGIN
INSERT 0 1
INSERT 0 1
ROLLBACK

回滚之后表里是空的,但自增计数器已经前进到 2:

sql
SELECT * FROM orders;
SELECT last_value FROM orders_id_seq;
text
 id | note
----+------
(0 rows)

 last_value
------------
          2
(1 row)

下一次插入拿到的 id 是 3,而不是 1:

sql
INSERT INTO orders (note) VALUES ('第三单');
SELECT id, note FROM orders;
text
INSERT 0 1
 id | note
----+--------
  3 | 第三单
(1 row)

这个特性是设计使然,也是必要的:如果序列能回滚,两个并发事务就会拿到同一个 nextval(),主键必然冲突。所以序列只保证「不重复」,不保证「无空洞」。

sql
-- 错误!试图用 ROLLBACK 来「回收」已用的编号,做不到
BEGIN;
INSERT INTO orders (note) VALUES ('临时数据');
ROLLBACK;

-- 下一次插入的 id 仍然跳过刚才那个值,不会复用
INSERT INTO orders (note) VALUES ('正式数据') RETURNING id;
text
BEGIN
INSERT 0 1
ROLLBACK
INSERT 0 1
 id
----
  4
(1 row)

业务上如果要求单号「绝对连续」,就不能依赖序列——这也是为什么「订单号必须连续」的需求在分库分表与高并发下几乎无法廉价满足。序列只保证唯一,不保证连续。

ABORT 是 ROLLBACK 的别名,END 是 COMMIT 的别名 ​

ABORT / ROLLBACK 与 END / COMMIT 是完全同义的别名,但返回的标签统一使用标准拼写:

sql
BEGIN;
INSERT INTO accounts (id, balance) VALUES (10, 1.00);
ABORT;
text
BEGIN
INSERT 0 1
ROLLBACK

注意 ABORT 执行后返回的标签是 ROLLBACK 而不是 ABORT——服务端统一用标准标签。同理 END 返回 COMMIT:

sql
BEGIN; INSERT INTO accounts (id, balance) VALUES (11, 1.00); END;
text
BEGIN
INSERT 0 1
COMMIT

日常编码统一用 COMMIT / ROLLBACK,可读性最好;ABORT 只在某些迁移脚本里见到。

事务中的 DDL:PostgreSQL 支持事务性 DDL ​

这是 PostgreSQL 相对 MySQL 的一个结构性优势。在 PostgreSQL 里,CREATE TABLE、ALTER TABLE、DROP TABLE、CREATE INDEX 等所有 DDL 都可以放在事务块里,并且可以被回滚。

sql
BEGIN;

CREATE TABLE tmp_report (
  id      bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  payload jsonb NOT NULL
);

ALTER TABLE tmp_report ADD COLUMN created_at timestamptz NOT NULL DEFAULT now();
CREATE INDEX idx_tmp_report_created ON tmp_report (created_at);

ROLLBACK;   -- 上面的建表、加列、建索引全部撤销
text
BEGIN
CREATE TABLE
ALTER TABLE
CREATE INDEX
ROLLBACK

验证表确实不存在——to_regclass() 返回 NULL 说明表已被回滚掉:

sql
SELECT to_regclass('tmp_report') AS table_oid;
text
 table_oid
-----------
(1 row)

这个能力在灰度迁移场景里非常关键——可以先在事务里改好结构、跑完验证 SQL,发现不对立即 ROLLBACK:

sql
BEGIN;

ALTER TABLE orders ADD COLUMN channel text NOT NULL DEFAULT 'web';

-- 用真实数据验证新结构与旧数据是否兼容
SELECT count(*) FILTER (WHERE channel = 'web') AS web_rows, count(*) AS total FROM orders;

-- 验证通过才提交;不通过就 ROLLBACK
COMMIT;
text
BEGIN
ALTER TABLE
 web_rows | total
----------+-------
        3 |     3
(1 row)

COMMIT

对比 MySQL:MySQL 的 DDL 会隐式提交当前事务,因此上面的写法在 MySQL 里根本达不到「验证不过就回滚」的效果。这是 PostgreSQL 在做数据库迁移工具(如 Flyway、Liquibase)时更省心的重要原因。

少数例外

绝大多数 DDL 可回滚,但少数命令不允许在事务块内执行,会直接报错,例如 VACUUM、CREATE INDEX CONCURRENTLY、CREATE DATABASE、ALTER SYSTEM、REINDEX DATABASE。它们的共同点是无法安全地参与 MVCC。详见 事务自动提交。

事务失败后的 aborted 状态:必须 ROLLBACK 才能继续 ​

事务内任何语句报错后,事务会进入 aborted(中止)状态。此时事务不会自动回滚,但后续任何语句都会立刻失败:

sql
BEGIN;

INSERT INTO accounts (id, balance) VALUES (20, 1.00);

-- 错误!违反主键唯一性,事务进入 aborted 状态
INSERT INTO accounts (id, balance) VALUES (20, 2.00);
text
BEGIN
INSERT 0 1
ERROR:  duplicate key value violates unique constraint "accounts_pkey"
DETAIL:  Key (id)=(20) already exists.

此时再执行任何语句,都会收到同一个错误:

sql
SELECT count(*) FROM accounts;
text
ERROR:  current transaction is aborted, commands ignored until end of transaction block

这不是「这条语句写错了」,而是「事务已经废了,你只能结束它」。出于安全考虑,PostgreSQL 拒绝在损坏的事务里继续执行——因为一条失败的语句可能只完成了一半工作。

正确做法是 ROLLBACK(或回滚到保存点),之后就能正常执行了:

sql
ROLLBACK;

-- 现在可以正常执行了
SELECT count(*) FROM accounts;
text
ROLLBACK
 count
-------
     4
(1 row)

在 aborted 状态下执行 COMMIT,PostgreSQL 会悄悄把它当成 ROLLBACK,并返回 ROLLBACK 标签提醒你:

sql
BEGIN;
INSERT INTO accounts (id, balance) VALUES (20, 1.00);   -- 假设再次冲突
COMMIT;
text
BEGIN
ERROR:  duplicate key value violates unique constraint "accounts_pkey"
DETAIL:  Key (id)=(20) already exists.
ROLLBACK

千万不要忽略 COMMIT 的返回值

如果 COMMIT 返回的是 ROLLBACK,意味着你以为提交了,其实全部丢掉了。在脚本里靠 COMMIT 的标签判断成败是不可靠的,应该设置 \set ON_ERROR_STOP on,或者用保存点做局部容错(见 SAVEPOINT 保存点)。

SQLSTATE 层面的对应关系:

状态SQLSTATE说明
事务内语句报错视具体错误事务被标记为 aborted
在 aborted 事务里继续执行25P02in_failed_sql_transaction
在事务块外执行 SAVEPOINT25P01no_active_sql_transaction
事务中改变隔离级别25001active_sql_transaction

psql 提示符与 \echo:看清当前事务状态 ​

psql 的默认提示符里的 %R 会实时反映事务状态,这是判断「我到底在不在事务里」最直观的手段:

提示符形态%R含义
mydb=#=不在事务块中
mydb=*#*在事务块中
mydb=!#!事务已 aborted,等待 ROLLBACK
mydb=?#?事务状态不确定(连接异常)

配合 \echo 可以在脚本执行时打印进度,明确事务边界:

sql
\echo '--- 开始事务 ---'
BEGIN;
\echo '--- 写入数据 ---'
INSERT INTO accounts (id, balance) VALUES (30, 5.00);
\echo '--- 提交 ---'
COMMIT;
\echo '--- 完成 ---'
text
--- 开始事务 ---
BEGIN
--- 写入数据 ---
INSERT 0 1
--- 提交 ---
COMMIT
--- 完成 ---

也可以直接在 SQL 里查当前事务 ID 与状态;只读事务不会分配 XID,因此这里是 NULL,一旦执行写入就会分配:

sql
BEGIN;
SELECT pg_current_xact_id_if_assigned() AS xid, txid_current_if_assigned() IS NOT NULL AS has_xid;

INSERT INTO accounts (id, balance) VALUES (31, 6.00);
SELECT pg_current_xact_id() AS xid;
text
BEGIN
   xid   | has_xid
---------+---------
         |
(1 row)

INSERT 0 1
   xid
--------
 1234567
(1 row)

嵌套 BEGIN 不会开启新事务(只有警告) ​

一个连接同一时刻只能有一个顶层事务。再执行 BEGIN 不会「嵌套」,只会给出警告并且被忽略:

sql
BEGIN;
BEGIN;
text
BEGIN
WARNING:  there is already a transaction in progress
BEGIN

反过来,没有事务时执行 COMMIT 或 ROLLBACK 也会警告,但不会报错:

sql
COMMIT;
ROLLBACK;
text
WARNING:  there is no transaction in progress
COMMIT
WARNING:  there is no transaction in progress
ROLLBACK

真正需要「事务内局部回滚」时,要用保存点,而不是嵌套 BEGIN:

sql
BEGIN;
INSERT INTO accounts (id, balance) VALUES (40, 1.00);

SAVEPOINT sp1;
INSERT INTO accounts (id, balance) VALUES (40, 2.00);   -- 错误!
ROLLBACK TO SAVEPOINT sp1;                              -- 只撤销这一句

COMMIT;   -- 前面 (40, 1.00) 正常提交
text
BEGIN
INSERT 0 1
SAVEPOINT
ERROR:  duplicate key value violates unique constraint "accounts_pkey"
DETAIL:  Key (id)=(40) already exists.
ROLLBACK
COMMIT

完整用法见 SAVEPOINT 保存点。

COMMIT 与磁盘刷盘:synchronous_commit 的关系 ​

COMMIT 返回成功,并不等价于数据已经写到磁盘。这个语义由 synchronous_commit 参数决定:

取值提交时等待到什么程度崩溃时可能丢失
off不等待 WAL 落盘,只写入操作系统缓冲区最近若干毫秒的已提交事务
local等待本地 WAL 落盘(同步复制场景用)无(单机等价于 on)
remote_write等待备库收到 WAL 并写入其操作系统缓冲主库不丢,备库切换可能丢
on(默认)等待本地 WAL fsync 完成无
remote_apply等待备库重放完成无,且备库可立即读
sql
SHOW synchronous_commit;
text
 synchronous_commit
--------------------
 on
(1 row)

临时降低持久性以换取吞吐(例如批量导入):

sql
-- 只影响当前会话,且必须放在事务开始之前才不报错
SET synchronous_commit = off;

BEGIN;
INSERT INTO orders (note) SELECT 'batch-' || g FROM generate_series(1, 10000) AS g;
COMMIT;   -- 返回很快,但最近约 3 个 WAL 缓冲(wal_writer_delay 周期)内的提交可能丢失
text
SET
BEGIN
INSERT 0 10000
COMMIT

不要在生产环境全局设成 off

synchronous_commit = off 会让「已提交但未落盘」的事务在断电或内核崩溃时永久丢失,违反 ACID 的持久性(Durability)。它只适合「可容忍少量丢失」的导入、日志类场景,且应在会话级临时设置。详细原理见 事务的 ACID 特性。

组提交(group commit)是让高并发下的 fsync 不那么昂贵的机制:多个几乎同时提交的事务会共用一次 fsync,因此 synchronous_commit = on 并不意味着「每个事务一次磁盘同步」。

sql
-- 观察提交时的 WAL 写入情况需要开启 track_wal_io_timing(有一定开销)
SELECT wal_records, wal_fpi, wal_bytes
FROM pg_stat_wal;
text
 wal_records | wal_fpi | wal_bytes
-------------+---------+-----------
       10432 |       3 |   2145872
(1 row)

只读事务:READ ONLY 的意义 ​

只读事务是一种声明式约束:它禁止本事务写入,但对读还有优化价值。

sql
BEGIN READ ONLY;

SELECT count(*) FROM orders;

-- 错误!只读事务不允许写入
INSERT INTO orders (note) VALUES ('x');
text
BEGIN
 count
-------
     3
(1 row)

ERROR:  cannot execute INSERT in a read-only transaction

在 SERIALIZABLE 隔离级别下,只读事务还能配合 DEFERRABLE 拿到「安全快照」,用于长报表查询,避免被误判为串行化冲突(见 设置隔离级别):

sql
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE READ ONLY DEFERRABLE;
text
BEGIN

实战示例 ​

场景:实现一个银行转账——A 账户扣款、B 账户入账,两步必须同时成功,且余额不能为负。这是理解事务最经典也最贴切的案例。

第 1 步:建表并初始化

sql
DROP TABLE IF EXISTS accounts CASCADE;

CREATE TABLE accounts (
  id         bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  owner      text          NOT NULL,
  balance    numeric(12,2) NOT NULL CHECK (balance >= 0),   -- 数据库兜底:不允许透支
  updated_at timestamptz   NOT NULL DEFAULT now()
);

INSERT INTO accounts (owner, balance) VALUES
  ('Alice', 1000.00),
  ('Bob',    200.00);
text
DROP TABLE
CREATE TABLE
INSERT 0 2

第 2 步:正常转账(提交)

sql
BEGIN;

UPDATE accounts SET balance = balance - 300.00, updated_at = now() WHERE owner = 'Alice';
UPDATE accounts SET balance = balance + 300.00, updated_at = now() WHERE owner = 'Bob';

COMMIT;

SELECT owner, balance FROM accounts ORDER BY id;
text
BEGIN
UPDATE 1
UPDATE 1
COMMIT
 owner | balance
-------+---------
 Alice |  700.00
 Bob   |  500.00
(2 rows)

第 3 步:转账中断——第一步成功、第二步失败

用一个双会话时间线演示「扣款成功但入账失败」时的危险。假设 Bob 的账户被另一个会话冻结导致更新失败:

text
会话 A:
BEGIN;
UPDATE accounts SET balance = balance - 100.00 WHERE owner = 'Alice';
UPDATE accounts SET balance = balance + 100.00 WHERE owner = 'Bob' AND 1 = 0;  -- 更新 0 行
BEGIN
UPDATE 1
UPDATE 0

会话 B(会话 A 尚未提交,看到的仍是旧值):
SELECT owner, balance FROM accounts ORDER BY id;
 owner | balance
-------+---------
 Alice |  700.00
 Bob   |  500.00
(2 rows)

如果会话 A 此刻 COMMIT,就会出现「钱凭空少了 100」——A 被扣了、B 没收到。所以必须先做一致性校验再决定提交(应用层检查两行是否都受影响,或直接用异常回滚):

text
会话 A:
-- 校验:扣款行受影响、入账行受影响,否则回滚
ROLLBACK;
ROLLBACK

会话 B(会话 A 回滚后):
SELECT owner, balance FROM accounts ORDER BY id;
 owner | balance
-------+---------
 Alice |  700.00
 Bob   |  500.00
(2 rows)

余额保持不变——ROLLBACK 把已经执行的扣款也撤销了。这就是原子性(Atomicity)的直接体现。

第 4 步:余额不足时的回滚(约束兜底)

sql
BEGIN;

-- Alice 只剩 700,转 5000 会触发 CHECK 约束
UPDATE accounts SET balance = balance - 5000.00 WHERE owner = 'Alice';
text
BEGIN
ERROR:  new row for relation "accounts" violates check constraint "accounts_balance_check"
DETAIL:  Failing row contains (1, Alice, -4300.00, 2026-09-28 10:00:00+08).

约束直接拦住非法状态,事务进入 aborted,后续语句全部失败,只能回滚:

sql
SELECT 1;    -- ERROR:  current transaction is aborted, ...
ROLLBACK;
text
ERROR:  current transaction is aborted, commands ignored until end of transaction block
ROLLBACK

第 5 步:确认最终状态并清理

sql
SELECT owner, balance FROM accounts ORDER BY id;
DROP TABLE accounts;
text
 owner | balance
-------+---------
 Alice |  700.00
 Bob   |  500.00
(2 rows)

DROP TABLE

注意事项 ​

  • 事务内出错后不要继续执行语句。 事务已进入 aborted 状态,后续语句全部报 25P02,必须 ROLLBACK(或 ROLLBACK TO SAVEPOINT)。
sql
-- 错误!此处任何语句都会失败
SELECT count(*) FROM accounts;
  • ROLLBACK 不会回收序列的自增值。 不要指望用回滚来「复用」编号,否则并发下会出现主键冲突。aborted 状态下 COMMIT 等于 ROLLBACK(返回标签会是 ROLLBACK),脚本里务必检查。
sql
-- 错误认知:以为回滚后 id 会从 1 重新开始
BEGIN; INSERT INTO orders (note) VALUES ('x'); ROLLBACK;
INSERT INTO orders (note) VALUES ('y') RETURNING id;   -- 实际拿到的是 2,不是 1
  • 不要在一个连接上「嵌套」BEGIN。 第二个 BEGIN 只会产生警告并被忽略,真正的事务边界仍是最外层那次;同时长事务会拖住 VACUUM、造成表膨胀并长期持锁,事务开启后应尽快结束,绝不要在事务中间等待用户输入或调用慢速外部接口。
sql
-- 错误!不会开启新事务
BEGIN; BEGIN; ...; COMMIT;   -- 这里 COMMIT 提交的是最外层事务

-- 不推荐:事务里执行耗时的外部调用
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE owner = 'Alice';
-- ... 中间去做一次耗时数秒的 HTTP 请求 ...
COMMIT;
  • synchronous_commit = off 会削弱持久性。 只应在会话级、针对可容忍少量丢失的场景临时使用,不要全局设置;同理部分命令不能在事务块里执行,例如 VACUUM、CREATE INDEX CONCURRENTLY、CREATE DATABASE、ALTER SYSTEM。
sql
-- 不推荐在生产全局设置
ALTER SYSTEM SET synchronous_commit = off;

-- 错误!VACUUM cannot run inside a transaction block
BEGIN; VACUUM accounts; COMMIT;
  • COMMIT 成功不代表立刻可被其他事务「看见内容」的即时性假设成立——更准确的说法是:提交后其他事务的新快照才能看到,已经在跑的 REPEATABLE READ 事务仍看到旧快照。

  • 事务必须绑定同一连接。 把 BEGIN 和 COMMIT 发到不同连接(连接池常见事故)会得到「事务不存在」或「语句不在事务中」的怪异行为。

最佳实践 ​

  • 用 BEGIN / COMMIT 的规范拼写,避免 ABORT / END 这类别名造成的阅读歧义;并保持事务短小,把网络调用、文件 IO、日志上报挪到事务外;同时让数据库约束做最后一道防线,CHECK (balance >= 0)、外键、唯一约束能拦住绝大部分非法中间态,比应用层判断更可靠。
sql
-- 推荐:只在事务里做数据库操作
BEGIN;
UPDATE ...;
UPDATE ...;
COMMIT;
-- 事务外做通知

-- 约束兜底
balance numeric(12,2) NOT NULL CHECK (balance >= 0)
  • 脚本里显式声明事务边界,并开启 ON_ERROR_STOP,避免「部分成功部分失败」的静默灾难;用 \echo 标注事务阶段,让长脚本日志一眼看出边界。
sql
\set ON_ERROR_STOP on
\echo '>>> BEGIN transfer'
BEGIN;
-- ...
\echo '>>> COMMIT transfer'
COMMIT;
  • 迁移脚本善用事务性 DDL:在事务里改结构、跑验证 SQL,不通过就 ROLLBACK;大批量导入则可临时降级持久性换吞吐,但务必在会话级设置、导入后恢复。
sql
-- 迁移:先验证再提交
BEGIN;
ALTER TABLE orders ADD COLUMN channel text NOT NULL DEFAULT 'web';
SELECT count(*) FROM orders WHERE channel IS NULL;   -- 验证
COMMIT;

-- 导入:会话级临时降级
SET synchronous_commit = off;
-- COPY / INSERT ...
SET synchronous_commit = on;
  • 只读业务显式声明 READ ONLY,既防误写,也便于在 SERIALIZABLE 下使用 DEFERRABLE 优化。
sql
BEGIN READ ONLY;
SELECT ...;
COMMIT;
  • 上线前用 pg_stat_activity 复查是否存在长时间未结束的事务(详见 隐式与显式事务)。
sql
SELECT pid, state, now() - xact_start AS xact_age, query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_start;

下一节

继续学习:SAVEPOINT 保存点

参考链接 ​