PostgreSQL 的数据行(Tuple)头部信息中,xmin、xmax、cmin 和 cmax 分别代表什么?
在 PostgreSQL 中,xmin、xmax、cmin 和 cmax 是数据行(Tuple/元组)头部信息(Tuple Header)中极其重要的四个系统字段。它们是 PostgreSQL 实现 MVCC(多版本并发控制) 的核心基石。
PostgreSQL 在更新数据时,并不会直接覆盖原有的数据,而是插入一条新数据,并标记老数据被“删除”。这四个字段就是用来记录和追踪这些数据版本的“生命周期”的。
以下是它们的详细解释:
1. xmin (Insert Transaction ID)
- 含义: 插入事务的 ID。它记录了是哪一个事务(Transaction ID,简称 XID)创建(插入)了这条数据行。
- 触发时机: 当执行
INSERT或者UPDATE操作时,新产生的数据行会被打上当前事务的 ID 作为xmin。 - 作用: 它标志着这条数据的 “诞生”。只有当系统的当前事务 ID 大于或等于
xmin,且xmin对应的事务已经提交时,这条数据对当前事务才是可见的。
2. xmax (Delete Transaction ID)
- 含义: 删除/更新事务的 ID,或者行锁事务 ID。它记录了是哪一个事务删除了这条数据行。
- 触发时机:
- 默认情况下,新插入的数据
xmax为 0(表示未被删除)。 - 当执行
DELETE操作时,该行的xmax会被设置为执行删除的事务 ID。 - 当执行
UPDATE操作时,旧数据行的xmax会被设置为当前事务 ID(同时新插入一行,其xmin为当前事务 ID)。 - 当执行
SELECT ... FOR UPDATE(行级锁)时,xmax也会被记录为加锁的事务 ID。
- 默认情况下,新插入的数据
- 作用: 它标志着这条数据的 “死亡”。如果一条数据的
xmax不为 0,且对应的事务已经提交,那么这条数据就被视为已删除,对后续的事务不可见。
3. cmin (Command ID for Insert)
- 含义: 插入命令 ID。它记录了在
xmin所在的事务内部,是第几个 SQL 命令插入了这条数据。 - 触发时机: 与
xmin同步产生。事务内的第一个命令 ID 为 0,第二个为 1,依此类推。 - 作用: 用于同一个事务内部的可见性判断。例如,在一个事务中,你先执行了
INSERT,然后紧接着在这个事务里执行SELECT,SELECT之所以能看到刚刚INSERT的数据,就是通过比对当前命令 ID 和数据的cmin来实现的。
4. cmax (Command ID for Delete)
- 含义: 删除命令 ID。它记录了在
xmax所在的事务内部,是第几个 SQL 命令删除或更新了这条数据。 - 触发时机: 当数据被
DELETE或UPDATE时产生。 - 作用: 同样用于同一个事务内部的可见性判断。如果在一个事务中删除了某行,该事务后续的
SELECT命令就不应该再看到这行数据,这就是通过比对cmax实现的。
💡 底层优化注意(硬核知识):
为了节省存储空间,在 PostgreSQL 的物理存储结构(t_tinfo结构体)中,cmin和cmax实际上共用同一个 32 位的物理字段(称为t_cid)。
因为在绝大多数情况下,一个事务不可能既插入了一条数据,又在同一个事务里删除了它(如果真的发生了,PostgreSQL 会在内存中生成一个叫 Combo CID 的映射表来解决冲突),所以它们交替使用同一个物理空间是安全的。
📌 一个完整的生命周期示例
假设有一张表 test_table,我们通过三个事务来看看这些值的变化:
事务 100:插入一条数据
sql
BEGIN; -- 假设分配的事务ID为 100
INSERT INTO test_table (id, val) VALUES (1, 'A'); -- 命令ID为 0
COMMIT;
- 此时底层数据行(Tuple 1):
xmin= 100,xmax= 0,cmin= 0
事务 101:更新这条数据
sql
BEGIN; -- 假设分配的事务ID为 101
UPDATE test_table SET val = 'B' WHERE id = 1; -- 命令ID为 0
COMMIT;
- 此时底层会有两条数据行:
- 旧行(Tuple 1):
xmin= 100,xmax= 101,cmin= 0,cmax= 0 (已标记死亡) - 新行(Tuple 2):
xmin= 101,xmax= 0,cmin= 0 (新诞生)
- 旧行(Tuple 1):
事务 102:删除这条数据
sql
BEGIN; -- 假设分配的事务ID为 102
DELETE FROM test_table WHERE id = 1; -- 命令ID为 0
COMMIT;
- 此时底层数据行(Tuple 2):
xmin= 101,xmax= 102,cmin= 0,cmax= 0 (已标记死亡) - (最终,这些被标记为死亡的旧元组(Tuple 1 和 Tuple 2),会在 PostgreSQL 的
VACUUM后台进程运行时被真正的物理清理掉。)
🔍 如何在 SQL 中查看它们?
你可以直接在普通的 SELECT 语句中查询这些隐藏的系统列:
sql
SELECT xmin, xmax, cmin, cmax, * FROM test_table;
右滑查看面试常问