事务的 ACID 特性
ACID 是事务(transaction)的四个基本保证:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。PostgreSQL 并不是靠一句「ACID 数据库」的标签来实现它们,而是用 WAL(预写式日志)、MVCC(多版本并发控制)、约束系统、锁管理与刷盘策略共同兑现。本节把每条特性拆开,讲清它在 PostgreSQL 18 里对应的具体机制、关键参数(fsync、synchronous_commit)以及在崩溃恢复时的行为。
前置知识
阅读本节前,建议先了解:BEGIN / COMMIT / ROLLBACK、SAVEPOINT 保存点、WAL 概述
基础概念
四条特性针对的是不同的问题,缺一不可:
| 特性 | 回答的问题 | 破坏后果 | PostgreSQL 的实现机制 |
|---|---|---|---|
| 原子性 Atomicity | 事务内的语句要么全做,要么全不做? | 转账扣了款却没入账 | 事务状态 + WAL 回滚 + MVCC 可见性 |
| 一致性 Consistency | 事务前后数据都满足业务规则? | 余额为负、外键悬空 | 约束(CHECK/UNIQUE/FK)、触发器、级联 |
| 隔离性 Isolation | 并发事务互不干扰? | 读到别人未提交的数据 | MVCC 快照 + 行锁/表锁 + 隔离级别 |
| 持久性 Durability | 提交后的数据不会丢? | 断电后已提交订单消失 | WAL 顺序写 + fsync + 检查点 |
一个最直观的对照——「事务能保证什么」与「事务不能保证什么」:
| 能保证 | 不能保证 |
|---|---|
| 事务内多语句的原子性 | 应用层逻辑本身的正确性 |
| 提交后的数据在崩溃后依然存在(默认配置下) | 磁盘物理损坏、RAID 卡缓存丢数据 |
| 已提交数据对并发事务的可见性规则 | 网络分区下的跨节点一致性 |
| 约束在事务提交前强制生效 | 两个事务加起来违反业务规则(写偏斜,见后续章节) |
各特性的验证方式:
原子性 → 中途报错后 ROLLBACK,检查数据是否回到原状
一致性 → 插入违反 CHECK / UNIQUE / FOREIGN KEY 的数据,看是否被拒绝
隔离性 → 两个会话并发读写同一行,观察各自看到的值
持久性 → 提交后立刻 kill -9 数据库进程,重启后检查数据是否存在语法与用法
ACID 没有专属语法,它由下面这组事务控制语句与配置参数共同兑现:
-- 事务控制(ACID 的原子性、隔离性入口)
BEGIN [ WORK | TRANSACTION ] [ ISOLATION LEVEL level ] [ READ ONLY | READ WRITE ];
COMMIT [ WORK | TRANSACTION ];
ROLLBACK [ WORK | TRANSACTION ];
SAVEPOINT savepoint_name;
ROLLBACK TO SAVEPOINT savepoint_name;
RELEASE SAVEPOINT savepoint_name;
-- 持久性相关的服务器与事务级开关
SET synchronous_commit TO { on | off | local | remote_write | remote_apply };
-- 两阶段提交(ACID 跨库延伸)
PREPARE TRANSACTION 'gid';
COMMIT PREPARED 'gid';
ROLLBACK PREPARED 'gid';最简可运行示例——在显式事务中做一次完整转账,并顺带查看当前隔离级别:
BEGIN;
UPDATE accounts SET balance = balance - 100.00 WHERE owner = 'Alice';
UPDATE accounts SET balance = balance + 100.00 WHERE owner = 'Bob';
COMMIT;BEGIN
UPDATE 1
UPDATE 1
COMMITSHOW transaction_isolation; transaction_isolation
-----------------------
read committed
(1 row)详细说明
原子性(Atomicity):全有或全无
原子性由「事务状态机 + WAL 撤销信息」共同保证。事务执行期间,PostgreSQL 在堆表(heap)中写入新版本元组,同时把撤销所需的信息(旧版本、事务 ID、命令 ID)保留在元组头部。若事务最终 ROLLBACK,这些元组被标记为中止(aborted),任何快照都不会再看到它们;VACUUM 之后空间被回收。
BEGIN;
UPDATE accounts SET balance = balance - 100.00 WHERE owner = 'Alice';
-- 假设下一步失败
UPDATE accounts SET balance = balance - 100.00 WHERE owner = 'Nobody'; -- UPDATE 0
INSERT INTO accounts (owner, balance) VALUES ('Alice', 0); -- 唯一约束冲突
ROLLBACK; -- 扣款动作一并撤销BEGIN
UPDATE 1
UPDATE 0
ERROR: duplicate key value violates unique constraint "accounts_owner_key"
DETAIL: Key (owner)=(Alice) already exists.
ROLLBACK关键是回滚不需要「反向补偿 SQL」:PostgreSQL 不会去执行一条 +100 的 UPDATE,而是直接让数据库回到事务开始前的可见状态。这也是原子性比手写补偿更可靠的根本原因。
-- 验证:回滚后 Alice 的余额没有变化
SELECT owner, balance FROM accounts WHERE owner = 'Alice'; owner | balance
-------+---------
Alice | 1000.00
(1 row)一致性(Consistency):约束、触发器与外键级联
一致性指数据库从一个合法状态迁移到另一个合法状态。PostgreSQL 把它拆成两层:数据库层强制的结构一致性(约束、类型、外键)和应用层负责的业务一致性(由你在事务中编排语句)。
数据库层的一致性工具:
CREATE TABLE accounts (
id bigint PRIMARY KEY,
owner text NOT NULL UNIQUE,
balance numeric(12,2) NOT NULL CHECK (balance >= 0)
);
CREATE TABLE transfers (
id bigserial PRIMARY KEY,
from_id bigint NOT NULL REFERENCES accounts(id) ON DELETE RESTRICT,
to_id bigint NOT NULL REFERENCES accounts(id) ON DELETE RESTRICT,
amount numeric(12,2) NOT NULL CHECK (amount > 0),
created_at timestamptz NOT NULL DEFAULT now()
);CREATE TABLE
CREATE TABLE-- 一致性被强制:余额为负直接被拒绝
INSERT INTO accounts (id, owner, balance) VALUES (10, 'Eve', -1.00);ERROR: new row for relation "accounts" violates check constraint "accounts_balance_check"
DETAIL: Failing row contains (10, Eve, -1.00).-- 一致性被强制:外键悬空直接被拒绝
INSERT INTO transfers (from_id, to_id, amount) VALUES (999, 10, 100.00);ERROR: insert or update on table "transfers" violates foreign key constraint "transfers_from_id_fkey"
DETAIL: Key (from_id)=(999) is not present in table "accounts".约束默认在语句结束时立即检查,也可以声明为 DEFERRABLE INITIALLY DEFERRED 推迟到 COMMIT 时检查——这对「先插子表再插父表」或互相引用的数据很有用:
ALTER TABLE transfers
ADD CONSTRAINT transfers_check_amount CHECK (amount <= 1000000) NOT VALID;
-- NOT VALID:只约束新数据,不扫描历史数据(后续可用 VALIDATE CONSTRAINT 校验)ALTER TABLE注意:约束只能保证「单点规则」,不能保证「跨行聚合规则」。例如「两人转账后总余额不变」这类规则,SQL 的 CHECK 无法表达(它不能读其它行),只能靠正确的 SQL 语句(同时更新双方)或触发器来维护。
隔离性(Isolation):MVCC 加锁
隔离性解决的是并发事务互相干扰的问题。PostgreSQL 采用 MVCC:读操作不阻塞写、写操作不阻塞读,每个查询根据隔离级别拿到一个快照(snapshot),只看到快照时点已提交的数据。
-- 事务 A(写)
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE owner = 'Alice';
-- 尚未 COMMIT
-- 事务 B(读,READ COMMITTED 默认)
SELECT balance FROM accounts WHERE owner = 'Alice';
-- 看到的仍是旧值(A 未提交,MVCC 通过元组的事务状态过滤掉)两边看到的视图之所以不同,是因为快照里的可见性规则(见 可见性规则):一个元组只有在其创建事务已提交、且删除事务未提交(或尚未开始时)才可见。
-- 查看当前会话的隔离级别
SHOW transaction_isolation;
-- 也可以会话级或事务级修改
SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN ISOLATION LEVEL SERIALIZABLE;写写冲突则由行级锁兜底:第二个 UPDATE 同一行的事务会被阻塞,直到第一个事务提交或回滚。若要「读出来就锁定」,需要显式 SELECT ... FOR UPDATE:
BEGIN;
SELECT balance FROM accounts WHERE owner = 'Alice' FOR UPDATE; -- 加行锁
UPDATE accounts SET balance = balance - 100 WHERE owner = 'Alice';
COMMIT;BEGIN
balance
---------
1000.00
(1 row)
UPDATE 1
COMMIT隔离性是 ACID 中最容易「打折」的一条:PostgreSQL 提供了四个隔离级别,标准允许的部分异常(如幻读、写偏斜)只在更高级别下才被消除,详见后续隔离级别章节。
持久性(Durability):WAL、fsync 与检查点
持久性回答「提交成功后断电会怎样」。PostgreSQL 用 WAL(Write-Ahead Logging) 实现:修改数据页之前,必须先把描述该修改的 WAL 记录写入 pg_wal 目录,并在 COMMIT 返回前把 WAL 刷到持久存储。数据页(heap)本身可以延迟落盘,因为它可以从 WAL 重放出来。
COMMIT 的完整落盘路径:
客户端 COMMIT
│
├─ 1. 生成 commit WAL 记录,写入 WAL 缓冲区
├─ 2. WAL writer / backend 把记录写入 pg_wal 文件(write)
├─ 3. fsync(pg_wal 文件) ← 真正的「持久化点」
├─ 4. 写 CLOG(提交日志)标记事务已提交
└─ 5. 返回 command tag: COMMIT
数据页(表文件)此时可能还只在 shared_buffers 里,
由 checkpoint 或后台 writer 在之后异步刷盘。WAL 的关键参数:
| 参数 | 默认值 | 作用 |
|---|---|---|
fsync | on | 是否把 WAL 真正 fsync 到磁盘,关闭后果自负 |
synchronous_commit | on | 提交时 WAL 需要刷到什么程度才返回成功 |
wal_level | replica | WAL 记录详细程度(minimal / replica / logical) |
full_page_writes | on | 检查点后首次修改整页写入 WAL,防部分写损坏 |
checkpoint_timeout | 5min | 检查点间隔,决定崩溃恢复需要重放多少 WAL |
检查点(checkpoint)把内存中的脏页刷盘,并在 WAL 中标记一个「此后不再需要回放更早记录」的起点。检查点越频繁,崩溃恢复越快,但刷盘压力越大。
-- 手动触发检查点(生产环境慎用,会产生 IO 尖峰)
CHECKPOINT;CHECKPOINTsynchronous_commit:持久性与性能的旋钮
fsync = on 是「WAL 一定会落盘」的总开关,而 synchronous_commit 决定事务提交时是否要等待 WAL 落盘。把它调低可以在吞吐与一致性之间做权衡:
| 取值 | 提交时等待 | 崩溃可能丢失 | 说明 |
|---|---|---|---|
on(默认) | 本地 WAL 刷盘 | 无(已提交事务不丢) | 最安全 |
remote_apply | 本地刷盘 + 同步备机已重放 | 无 | 最强,延迟最高(需同步复制配置) |
remote_write | 本地刷盘 + 同步备机已写入 | 备机宕机可能丢 | 折中 |
local | 本地刷盘(不等备机) | 无本地丢失 | 同步复制场景下的本机保证 |
off | 不等待 | 极端情况下可能丢最近若干事务 | 吞吐最高,适合可容忍少量丢失的场景 |
-- 会话级降低持久性要求
SET synchronous_commit TO off;
BEGIN;
INSERT INTO logs (msg) VALUES ('这条记录在崩溃时可能丢失');
COMMIT;SET
BEGIN
INSERT 0 1
COMMIT这里有三个常见误解必须澄清:
synchronous_commit = off不等于事务会丢失原子性。 提交后数据库不会「看起来提交了一半」;最坏情况是最近几个已报告成功的事务在崩溃后消失,数据库本身仍是一致的。synchronous_commit = off不等于「提交立即返回但 WAL 永不落盘」。 WAL 仍会在后台刷盘,只是客户端不再等待。- 关闭
fsync(fsync = off)是另一回事,危险得多。 它可能导致崩溃后数据库无法启动或数据文件损坏,只应在从备份重建的临时环境使用。
# postgresql.conf:用牺牲少量持久性换取批量导入吞吐
synchronous_commit = off
fsync = on组提交(group commit):为什么高并发下 fsync 没那么贵
看到「每个 COMMIT 都要 fsync」很容易担心性能,但 PostgreSQL 实现了组提交:当一个后端正在刷 WAL 时,其它并发提交的事务会把记录追加到同一批 WAL 里,一次 fsync 就为多个事务完成持久化。
组提交示意(同一时刻有 3 个事务要提交):
t0 T1 写入 commit 记录 ──┐
t1 T2 写入 commit 记录 ──┼──► 合并成一次 fsync ──► 三者同时返回 COMMIT
t2 T3 写入 commit 记录 ──┘
结果:3 个事务只付了 1 次 fsync 的成本。这带来两个实践结论:
- 高并发短事务的提交开销远低于直觉,不要因为「怕 fsync」而主动降低
synchronous_commit。 - 串行执行的短事务最吃亏:每个提交都得单独等一次刷盘。批量写入时用「较大事务 + 较少提交次数」优于「每行一个事务」。
-- 推荐:把批量写入收进一个有合理大小的事务,减少提交次数
BEGIN;
INSERT INTO metrics (name, value)
SELECT 'm' || g, g FROM generate_series(1, 10000) AS g;
COMMIT;BEGIN
INSERT 0 10000
COMMIT崩溃恢复:PostgreSQL 如何在宕机后自愈
持久性的真正考验是崩溃恢复(crash recovery)。数据库重启时,会找到最后一次检查点的位置,然后重放(replay)该位置之后的所有 WAL 记录,把数据页恢复到崩溃前的一致状态:
崩溃恢复流程:
1. 读取 pg_control,定位最后一个 checkpoint 的 REDO 位置
2. 从 REDO 位置开始按顺序重放 WAL
3. 遇到 commit 记录 → 该事务的修改变为已提交(可见)
4. 遇到未提交记录 → 直接丢弃(原子性在恢复时同样成立)
5. 重放到 WAL 末尾 → 视为恢复完成,写入新的 checkpoint
6. 数据库进入接受连接状态这套机制保证了原子性与持久性在崩溃场景下依然成立:已提交的事务被重放出来,未提交的事务被丢弃,不会出现「半个事务」。
# 查看恢复进度(恢复过程中可用)
psql -c "SELECT pg_is_in_recovery(), pg_last_wal_replay_lsn(), pg_last_wal_receive_lsn();" pg_is_in_recovery | pg_last_wal_replay_lsn | pg_last_wal_receive_lsn
-------------------+------------------------+------------------------
t | 0/1A2B3C8 | 0/1A2B3C8
(1 row)崩溃恢复的耗时与「最后一个检查点之后积累的 WAL 量」成正比,这解释了为什么 checkpoint_timeout 与 max_wal_size 是持久性相关的性能参数。
ACID 与 CAP / BASE 的边界
ACID 描述的是单个数据库实例内的事务保证,它不涉及分布式系统。谈分布式时常见的 CAP 与 BASE 是另一个层面的话题:
| 模型 | 核心主张 | 与 ACID 的关系 |
|---|---|---|
| ACID | 单机事务的原子/一致/隔离/持久 | 本文讨论的对象 |
| CAP | 一致性、可用性、分区容错三者不可兼得 | 单机 PostgreSQL 本质是 CP:网络分区时牺牲可用性 |
| BASE | 基本可用、软状态、最终一致 | 与 ACID 相反的设计取向,常见于分布式 KV |
需要特别澄清一个常见误解:PostgreSQL 的「ACID」并不自动跨越多个数据库实例。在主从复制架构下,主库提交即视为持久(synchronous_commit = on 时至少保证本地落盘);备库上的数据是否持久、何时可见,取决于复制模式(异步 / 同步)与 synchronous_commit 的设置。
-- 查看当前同步提交设置
SHOW synchronous_commit; synchronous_commit
--------------------
on
(1 row)跨库原子性:两阶段提交(2PC)
如果一次业务要写两个独立的 PostgreSQL 实例(或在同一实例中用不同连接写不同库),单机事务爱莫能助,此时需要两阶段提交(two-phase commit, 2PC):先把事务「准备」好并持久化,确认所有参与者都准备好之后再统一提交。
-- 前置条件:max_prepared_transactions 必须大于 0(默认 0 表示禁用)
SHOW max_prepared_transactions; max_prepared_transactions
---------------------------
0
(1 row)-- 阶段一:准备(数据已持久化,但对外不可见)
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE owner = 'Alice';
PREPARE TRANSACTION 'transfer-alice-to-bob';BEGIN
UPDATE 1
PREPARE TRANSACTION-- 阶段二:确认所有参与者准备完毕后提交
COMMIT PREPARED 'transfer-alice-to-bob';COMMIT PREPARED2PC 的代价与风险必须清楚:
- 事务在
PREPARE后长期持锁,直到COMMIT PREPARED/ROLLBACK PREPARED,期间无人能改这些行。 - prepare 后崩溃会留下「悬挂事务」,需要用
pg_prepared_xacts排查并人工决断:
SELECT gid, prepared, owner, database FROM pg_prepared_xacts; gid | prepared | owner | database
-----+----------+-------+----------
(0 rows)- 2PC 不能解决协调者单点问题:如果应用在
PREPARE之后、COMMIT PREPARED之前崩溃,需要外部机制(如 Saga、事务协调器)来最终决定提交还是回滚。
因此多数业务更倾向于避免跨库写(用同一个库、或用异步补偿/事件驱动),而不是默认使用 2PC。
实战示例
场景:用一个完整的转账流程,逐条验证 ACID 四项保证。
第 1 步:准备数据
DROP TABLE IF EXISTS transfers, accounts;
CREATE TABLE accounts (
id bigint PRIMARY KEY,
owner text NOT NULL UNIQUE,
balance numeric(12,2) NOT NULL CHECK (balance >= 0)
);
CREATE TABLE transfers (
id bigserial PRIMARY KEY,
from_id bigint NOT NULL REFERENCES accounts(id),
to_id bigint NOT NULL REFERENCES accounts(id),
amount numeric(12,2) NOT NULL CHECK (amount > 0),
created_at timestamptz NOT NULL DEFAULT now()
);
INSERT INTO accounts (id, owner, balance) VALUES
(1, 'Alice', 1000.00),
(2, 'Bob', 500.00);DROP TABLE
CREATE TABLE
CREATE TABLE
INSERT 0 2第 2 步:正常转账(验证原子性与一致性)
BEGIN;
UPDATE accounts SET balance = balance - 200.00 WHERE id = 1;
UPDATE accounts SET balance = balance + 200.00 WHERE id = 2;
INSERT INTO transfers (from_id, to_id, amount) VALUES (1, 2, 200.00);
COMMIT;BEGIN
UPDATE 1
UPDATE 1
INSERT 0 1
COMMITSELECT owner, balance FROM accounts ORDER BY id;
SELECT sum(balance) AS total FROM accounts; owner | balance
-------+---------
Alice | 800.00
Bob | 700.00
(2 rows)
total
--------
1500.00
(1 row)总额仍是 1500.00,说明「转出与转入必须同时生效」这条一致性规则被事务守住了。
第 3 步:失败后回滚(再次验证原子性)
BEGIN;
UPDATE accounts SET balance = balance - 5000.00 WHERE id = 1; -- 会触发 CHECK 失败
UPDATE accounts SET balance = balance + 5000.00 WHERE id = 2;
COMMIT;BEGIN
ERROR: new row for relation "accounts" violates check constraint "accounts_balance_check"
DETAIL: Failing row contains (1, Alice, -4200.00).
-- 后续语句被忽略,COMMIT 实际等价于 ROLLBACKSELECT owner, balance FROM accounts ORDER BY id; owner | balance
-------+---------
Alice | 800.00
Bob | 700.00
(2 rows)没有出现「Alice 少了 5000、Bob 没变」这种半成品状态——约束失败触发了语句级与事务级回滚。
第 4 步:验证隔离性
会话 A:
BEGIN;
UPDATE accounts SET balance = balance - 100.00 WHERE id = 1;
会话 B(READ COMMITTED):
SELECT balance FROM accounts WHERE id = 1; -- 仍看到 800.00(A 未提交)-- 会话 B 输出
balance
---------
800.00
(1 row)第 5 步:验证持久性与清理
-- 会话 A 提交后,B 才能看到新值
COMMIT;COMMITSELECT balance FROM accounts WHERE id = 1; -- 700.00
DROP TABLE transfers, accounts; balance
---------
700.00
(1 row)
DROP TABLE注意事项
- ACID 建立在「默认配置」之上。 关闭
fsync、把synchronous_commit调到off、使用非持久化的存储(如未开O_DIRECT的虚拟机临时盘),都会削弱持久性。错误!认为「PostgreSQL 是 ACID 数据库,所以怎么配都不会丢数据」。
-- 会话级降低持久性:明确知道代价再用
SET synchronous_commit TO off;
-- 错误!把 fsync 关掉来表达「性能优先」
-- ALTER SYSTEM SET fsync = off; -- 崩溃可能导致数据文件损坏- 一致性不等于「数据库帮你检查业务规则」。 CHECK / UNIQUE / FK 只能表达单行或单条引用的规则;「两人余额之和不变」「库存与订单数量匹配」这类跨行不变式,SQL 约束表达不了。不推荐把这类规则寄托于「数据库会保证」。
-- 错误!以为 CHECK 能表达「转账前后总额不变」
-- ALTER TABLE accounts ADD CONSTRAINT total_ok CHECK (sum(balance) = 1500);
-- ERROR: aggregate functions are not allowed in check constraints- 隔离级别决定你在 ACID 的「I」上买到多少。 默认的 READ COMMITTED 允许不可重复读与幻读;只有 SERIALIZABLE 才承诺「等价于某种串行执行」。错误!认为「用了事务就没有并发问题」。
-- 默认级别下,同一事务内两次读取可能得到不同结果
BEGIN;
SELECT balance FROM accounts WHERE id = 1;
-- 期间另一个事务提交了修改
SELECT balance FROM accounts WHERE id = 1; -- 可能与上一次不同
COMMIT;- 2PC 默认是关闭的,而且不能滥用。
max_prepared_transactions默认0,PREPARE TRANSACTION会直接报错;开启后悬挂事务会长期持锁。
-- 错误!未开启 2PC 时直接准备事务
BEGIN;
UPDATE accounts SET balance = balance - 1 WHERE id = 1;
PREPARE TRANSACTION 'tx1';ERROR: prepared transactions are disabled
HINT: Set max_prepared_transactions to a nonzero value.最佳实践
- 保持
fsync = on与synchronous_commit = on作为默认,只在明确能接受少量丢失的场景(日志表、监控埋点、可重建的导入任务)逐会话或逐事务调低。
-- 只对可容忍丢失的写入降低持久性
BEGIN;
SET LOCAL synchronous_commit TO off;
INSERT INTO metrics (name, value) VALUES ('heartbeat', 1);
COMMIT;
-- 事务结束后自动恢复为服务器默认值- 把业务不变式尽量下沉到约束,让数据库在
COMMIT前就拒绝不合法数据;跨行规则用触发器或「正确的语句编排」维护,并写测试覆盖。
-- 推荐:用外键 + CHECK 把结构一致性交给数据库
ALTER TABLE transfers
ADD CONSTRAINT transfers_amount_positive CHECK (amount > 0);
ALTER TABLE transfers
ADD CONSTRAINT transfers_distinct_accounts CHECK (from_id <> to_id);- 控制事务粒度:短事务 + 合理批次。 既能享受组提交带来的 fsync 摊销,又能减少锁持有时间和 VACUUM 压力。
-- 推荐:分批提交,而不是百万行一个大事务
DO $$
DECLARE i int;
BEGIN
FOR i IN 0..99 LOOP
INSERT INTO metrics (name, value)
SELECT 'm' || g, g FROM generate_series(i * 1000 + 1, (i + 1) * 1000) AS g;
COMMIT; -- 每批提交一次
END LOOP;
END $$;- 谨慎使用 2PC。 若必须跨库写,先用
pg_prepared_xacts建立监控与人工处理流程,并考虑用事件驱动 + 幂等补偿替代。
-- 监控悬挂事务(应长期为 0 行)
SELECT gid, prepared, owner, database
FROM pg_prepared_xacts
ORDER BY prepared;下一节
继续学习:事务自动提交