BEGIN / COMMIT / ROLLBACK
事务(Transaction)是把多条 SQL 打包成一个「要么全做、要么全不做」的逻辑单元。本节从最基础的三条命令 BEGIN / COMMIT / ROLLBACK 讲起,覆盖事务块内的数据可见性、序列不回退这个经典坑点、PostgreSQL 相对 MySQL 的一大优势——事务性 DDL,以及事务失败后进入 aborted 状态的处理方式。学完后你能准确判断一段代码到底开了几个事务、出错时该 COMMIT 还是 ROLLBACK。
基础概念
事务是一组被数据库当作一个不可分割单元执行的语句。它由一条开启语句、若干条普通语句和一条结束语句构成:
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 返回的标签 | 效果 |
|---|---|---|
COMMIT | COMMIT | 提交,变更对所有后续事务可见 |
ROLLBACK | ROLLBACK | 回滚,丢弃事务内全部变更 |
在 aborted 事务里执行 COMMIT | ROLLBACK | 实际是回滚(PostgreSQL 会改标签提示你) |
一个容易忽略的事实:事务 ID(XID)是惰性分配的。只读事务(没有任何写操作)在 PostgreSQL 中根本不会分配真正的 XID,因此也不会与其它事务产生 XID 环绕(wraparound)相关的压力。这直接影响后续要讲的「只读长事务是否危险」的判断。
语法与用法
-- 开启事务
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 ]最简可运行示例:
BEGIN;
INSERT INTO accounts (id, balance) VALUES (1, 100.00);
UPDATE accounts SET balance = balance - 10.00 WHERE id = 1;
COMMIT;BEGIN
INSERT 0 1
UPDATE 1
COMMIT把 COMMIT 换成 ROLLBACK,效果完全相反:
BEGIN;
INSERT INTO accounts (id, balance) VALUES (2, 50.00);
ROLLBACK;BEGIN
INSERT 0 1
ROLLBACK虽然 INSERT 返回了 INSERT 0 1(看起来像是插进去了),但 ROLLBACK 之后这一行不存在:
SELECT * FROM accounts WHERE id = 2; id | balance
----+---------
(0 rows)详细说明
BEGIN / START TRANSACTION / BEGIN WORK 完全等价
这三个写法在 PostgreSQL 内部走的是同一条解析路径,没有任何行为差别:
BEGIN; -- 最简写法
BEGIN WORK; -- 加上 WORK
BEGIN TRANSACTION; -- 加上 TRANSACTION
START TRANSACTION; -- SQL 标准拼写BEGIN
BEGIN
BEGIN
BEGIN要在开启事务时指定隔离级别或只读属性,必须用后面两种写法之一:
-- 开启事务时直接指定隔离级别与只读属性
BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY;BEGINSTART TRANSACTION 才能带 transaction_mode 吗?
不是。BEGIN 也能带 transaction_mode,例如 BEGIN ISOLATION LEVEL SERIALIZABLE; 是合法的。规范里 BEGIN WORK 与 START TRANSACTION 都允许 transaction_mode 列表,写哪一种看团队习惯即可。
事务块内:自己能看到自己未提交的修改
这是初学者最容易困惑的一点:事务内的修改对本事务自己可见,对其他事务不可见。
BEGIN;
INSERT INTO accounts (id, balance) VALUES (3, 999.00);
-- 本事务内能查到刚插入的行
SELECT * FROM accounts WHERE id = 3;BEGIN
INSERT 0 1
id | balance
----+---------
3 | 999.00
(1 row)现在另开一个会话(第二个 psql 窗口),在事务尚未提交时查询,看不到任何东西——因为 id = 3 这一行的事务还没提交,MVCC 的快照机制把它隐藏了(判定规则见 可见性规则)。只有会话 A 执行 COMMIT 之后,会话 B 才能看到:
会话 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 影响。这是最常被误判的「坑点」。
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;CREATE TABLE
BEGIN
INSERT 0 1
INSERT 0 1
ROLLBACK回滚之后表里是空的,但自增计数器已经前进到 2:
SELECT * FROM orders;
SELECT last_value FROM orders_id_seq; id | note
----+------
(0 rows)
last_value
------------
2
(1 row)下一次插入拿到的 id 是 3,而不是 1:
INSERT INTO orders (note) VALUES ('第三单');
SELECT id, note FROM orders;INSERT 0 1
id | note
----+--------
3 | 第三单
(1 row)这个特性是设计使然,也是必要的:如果序列能回滚,两个并发事务就会拿到同一个 nextval(),主键必然冲突。所以序列只保证「不重复」,不保证「无空洞」。
-- 错误!试图用 ROLLBACK 来「回收」已用的编号,做不到
BEGIN;
INSERT INTO orders (note) VALUES ('临时数据');
ROLLBACK;
-- 下一次插入的 id 仍然跳过刚才那个值,不会复用
INSERT INTO orders (note) VALUES ('正式数据') RETURNING id;BEGIN
INSERT 0 1
ROLLBACK
INSERT 0 1
id
----
4
(1 row)业务上如果要求单号「绝对连续」,就不能依赖序列——这也是为什么「订单号必须连续」的需求在分库分表与高并发下几乎无法廉价满足。序列只保证唯一,不保证连续。
ABORT 是 ROLLBACK 的别名,END 是 COMMIT 的别名
ABORT / ROLLBACK 与 END / COMMIT 是完全同义的别名,但返回的标签统一使用标准拼写:
BEGIN;
INSERT INTO accounts (id, balance) VALUES (10, 1.00);
ABORT;BEGIN
INSERT 0 1
ROLLBACK注意 ABORT 执行后返回的标签是 ROLLBACK 而不是 ABORT——服务端统一用标准标签。同理 END 返回 COMMIT:
BEGIN; INSERT INTO accounts (id, balance) VALUES (11, 1.00); END;BEGIN
INSERT 0 1
COMMIT日常编码统一用 COMMIT / ROLLBACK,可读性最好;ABORT 只在某些迁移脚本里见到。
事务中的 DDL:PostgreSQL 支持事务性 DDL
这是 PostgreSQL 相对 MySQL 的一个结构性优势。在 PostgreSQL 里,CREATE TABLE、ALTER TABLE、DROP TABLE、CREATE INDEX 等所有 DDL 都可以放在事务块里,并且可以被回滚。
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; -- 上面的建表、加列、建索引全部撤销BEGIN
CREATE TABLE
ALTER TABLE
CREATE INDEX
ROLLBACK验证表确实不存在——to_regclass() 返回 NULL 说明表已被回滚掉:
SELECT to_regclass('tmp_report') AS table_oid; table_oid
-----------
(1 row)这个能力在灰度迁移场景里非常关键——可以先在事务里改好结构、跑完验证 SQL,发现不对立即 ROLLBACK:
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;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(中止)状态。此时事务不会自动回滚,但后续任何语句都会立刻失败:
BEGIN;
INSERT INTO accounts (id, balance) VALUES (20, 1.00);
-- 错误!违反主键唯一性,事务进入 aborted 状态
INSERT INTO accounts (id, balance) VALUES (20, 2.00);BEGIN
INSERT 0 1
ERROR: duplicate key value violates unique constraint "accounts_pkey"
DETAIL: Key (id)=(20) already exists.此时再执行任何语句,都会收到同一个错误:
SELECT count(*) FROM accounts;ERROR: current transaction is aborted, commands ignored until end of transaction block这不是「这条语句写错了」,而是「事务已经废了,你只能结束它」。出于安全考虑,PostgreSQL 拒绝在损坏的事务里继续执行——因为一条失败的语句可能只完成了一半工作。
正确做法是 ROLLBACK(或回滚到保存点),之后就能正常执行了:
ROLLBACK;
-- 现在可以正常执行了
SELECT count(*) FROM accounts;ROLLBACK
count
-------
4
(1 row)在 aborted 状态下执行 COMMIT,PostgreSQL 会悄悄把它当成 ROLLBACK,并返回 ROLLBACK 标签提醒你:
BEGIN;
INSERT INTO accounts (id, balance) VALUES (20, 1.00); -- 假设再次冲突
COMMIT;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 事务里继续执行 | 25P02 | in_failed_sql_transaction |
在事务块外执行 SAVEPOINT | 25P01 | no_active_sql_transaction |
| 事务中改变隔离级别 | 25001 | active_sql_transaction |
psql 提示符与 \echo:看清当前事务状态
psql 的默认提示符里的 %R 会实时反映事务状态,这是判断「我到底在不在事务里」最直观的手段:
| 提示符形态 | %R | 含义 |
|---|---|---|
mydb=# | = | 不在事务块中 |
mydb=*# | * | 在事务块中 |
mydb=!# | ! | 事务已 aborted,等待 ROLLBACK |
mydb=?# | ? | 事务状态不确定(连接异常) |
配合 \echo 可以在脚本执行时打印进度,明确事务边界:
\echo '--- 开始事务 ---'
BEGIN;
\echo '--- 写入数据 ---'
INSERT INTO accounts (id, balance) VALUES (30, 5.00);
\echo '--- 提交 ---'
COMMIT;
\echo '--- 完成 ---'--- 开始事务 ---
BEGIN
--- 写入数据 ---
INSERT 0 1
--- 提交 ---
COMMIT
--- 完成 ---也可以直接在 SQL 里查当前事务 ID 与状态;只读事务不会分配 XID,因此这里是 NULL,一旦执行写入就会分配:
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;BEGIN
xid | has_xid
---------+---------
|
(1 row)
INSERT 0 1
xid
--------
1234567
(1 row)嵌套 BEGIN 不会开启新事务(只有警告)
一个连接同一时刻只能有一个顶层事务。再执行 BEGIN 不会「嵌套」,只会给出警告并且被忽略:
BEGIN;
BEGIN;BEGIN
WARNING: there is already a transaction in progress
BEGIN反过来,没有事务时执行 COMMIT 或 ROLLBACK 也会警告,但不会报错:
COMMIT;
ROLLBACK;WARNING: there is no transaction in progress
COMMIT
WARNING: there is no transaction in progress
ROLLBACK真正需要「事务内局部回滚」时,要用保存点,而不是嵌套 BEGIN:
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) 正常提交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 | 等待备库重放完成 | 无,且备库可立即读 |
SHOW synchronous_commit; synchronous_commit
--------------------
on
(1 row)临时降低持久性以换取吞吐(例如批量导入):
-- 只影响当前会话,且必须放在事务开始之前才不报错
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 周期)内的提交可能丢失SET
BEGIN
INSERT 0 10000
COMMIT不要在生产环境全局设成 off
synchronous_commit = off 会让「已提交但未落盘」的事务在断电或内核崩溃时永久丢失,违反 ACID 的持久性(Durability)。它只适合「可容忍少量丢失」的导入、日志类场景,且应在会话级临时设置。详细原理见 事务的 ACID 特性。
组提交(group commit)是让高并发下的 fsync 不那么昂贵的机制:多个几乎同时提交的事务会共用一次 fsync,因此 synchronous_commit = on 并不意味着「每个事务一次磁盘同步」。
-- 观察提交时的 WAL 写入情况需要开启 track_wal_io_timing(有一定开销)
SELECT wal_records, wal_fpi, wal_bytes
FROM pg_stat_wal; wal_records | wal_fpi | wal_bytes
-------------+---------+-----------
10432 | 3 | 2145872
(1 row)只读事务:READ ONLY 的意义
只读事务是一种声明式约束:它禁止本事务写入,但对读还有优化价值。
BEGIN READ ONLY;
SELECT count(*) FROM orders;
-- 错误!只读事务不允许写入
INSERT INTO orders (note) VALUES ('x');BEGIN
count
-------
3
(1 row)
ERROR: cannot execute INSERT in a read-only transaction在 SERIALIZABLE 隔离级别下,只读事务还能配合 DEFERRABLE 拿到「安全快照」,用于长报表查询,避免被误判为串行化冲突(见 设置隔离级别):
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE READ ONLY DEFERRABLE;BEGIN实战示例
场景:实现一个银行转账——A 账户扣款、B 账户入账,两步必须同时成功,且余额不能为负。这是理解事务最经典也最贴切的案例。
第 1 步:建表并初始化
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);DROP TABLE
CREATE TABLE
INSERT 0 2第 2 步:正常转账(提交)
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;BEGIN
UPDATE 1
UPDATE 1
COMMIT
owner | balance
-------+---------
Alice | 700.00
Bob | 500.00
(2 rows)第 3 步:转账中断——第一步成功、第二步失败
用一个双会话时间线演示「扣款成功但入账失败」时的危险。假设 Bob 的账户被另一个会话冻结导致更新失败:
会话 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 没收到。所以必须先做一致性校验再决定提交(应用层检查两行是否都受影响,或直接用异常回滚):
会话 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 步:余额不足时的回滚(约束兜底)
BEGIN;
-- Alice 只剩 700,转 5000 会触发 CHECK 约束
UPDATE accounts SET balance = balance - 5000.00 WHERE owner = 'Alice';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,后续语句全部失败,只能回滚:
SELECT 1; -- ERROR: current transaction is aborted, ...
ROLLBACK;ERROR: current transaction is aborted, commands ignored until end of transaction block
ROLLBACK第 5 步:确认最终状态并清理
SELECT owner, balance FROM accounts ORDER BY id;
DROP TABLE accounts; owner | balance
-------+---------
Alice | 700.00
Bob | 500.00
(2 rows)
DROP TABLE注意事项
- 事务内出错后不要继续执行语句。 事务已进入 aborted 状态,后续语句全部报
25P02,必须ROLLBACK(或ROLLBACK TO SAVEPOINT)。
-- 错误!此处任何语句都会失败
SELECT count(*) FROM accounts;ROLLBACK不会回收序列的自增值。 不要指望用回滚来「复用」编号,否则并发下会出现主键冲突。aborted 状态下COMMIT等于ROLLBACK(返回标签会是ROLLBACK),脚本里务必检查。
-- 错误认知:以为回滚后 id 会从 1 重新开始
BEGIN; INSERT INTO orders (note) VALUES ('x'); ROLLBACK;
INSERT INTO orders (note) VALUES ('y') RETURNING id; -- 实际拿到的是 2,不是 1- 不要在一个连接上「嵌套」
BEGIN。 第二个BEGIN只会产生警告并被忽略,真正的事务边界仍是最外层那次;同时长事务会拖住 VACUUM、造成表膨胀并长期持锁,事务开启后应尽快结束,绝不要在事务中间等待用户输入或调用慢速外部接口。
-- 错误!不会开启新事务
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。
-- 不推荐在生产全局设置
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)、外键、唯一约束能拦住绝大部分非法中间态,比应用层判断更可靠。
-- 推荐:只在事务里做数据库操作
BEGIN;
UPDATE ...;
UPDATE ...;
COMMIT;
-- 事务外做通知
-- 约束兜底
balance numeric(12,2) NOT NULL CHECK (balance >= 0)- 脚本里显式声明事务边界,并开启
ON_ERROR_STOP,避免「部分成功部分失败」的静默灾难;用\echo标注事务阶段,让长脚本日志一眼看出边界。
\set ON_ERROR_STOP on
\echo '>>> BEGIN transfer'
BEGIN;
-- ...
\echo '>>> COMMIT transfer'
COMMIT;- 迁移脚本善用事务性 DDL:在事务里改结构、跑验证 SQL,不通过就
ROLLBACK;大批量导入则可临时降级持久性换吞吐,但务必在会话级设置、导入后恢复。
-- 迁移:先验证再提交
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优化。
BEGIN READ ONLY;
SELECT ...;
COMMIT;- 上线前用
pg_stat_activity复查是否存在长时间未结束的事务(详见 隐式与显式事务)。
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 保存点