理解PostgreSQL中的CMIN和CMAX
核心类比:一个事务就是一个“故事章节”
想象一下,你正在写一本书的一个章节(这相当于数据库中的一个事务)。
- XMIN 和 XMAX 就像是章节编号。它们解决的是:“我应该读第3章还是第5章?” 这类大范围的问题,确保不同读者(不同事务)读到正确章节。
- CMIN 和 CMAX 就像是章节内的段落编号。它们解决的是:在你正在写的当前章节内,“我刚刚在第2段加了一句话,那么在第3段里引用这句话时,必须能找得到它。”
详细情节
场景: 你在一个事务里执行了多条SQL命令。
-
命令1 (
cid= 0):INSERT INTO table (name) VALUES ('张三');- 数据库创建了一行新数据。
- 为了记录这行数据是在这个事务内、由哪个命令创建的,它给这行数据打上了一个标记:
CMIN = 0。 - (
CMIN中的 ‘I’ 可以联想为 Insert)
-
命令2 (
cid= 1):UPDATE table SET name = '李四' WHERE name = '张三';- 注意: PostgreSQL的UPDATE实际上是“先删除,再插入”。
- 步骤1(删除): 它找到
CMIN=0的那行“张三”,然后标记这个删除动作是由当前事务内、命令ID为1的命令执行的。所以它给这行“张三”数据打上CMAX = 1的标记,表示“在此章节的第1段被删除了”。 - 步骤2(插入): 它创建了一行新的“李四”数据。这行新数据的
CMIN = 1,因为它是当前事务内、命令ID为1的命令插入的。
-
命令3 (
cid= 2):SELECT * FROM table;- 现在,事务要查询数据了。它看到了两行数据:
- 一行是 (
CMIN=0, CMAX=1) 的“张三” - 一行是 (
CMIN=1) 的“李四”
- 一行是 (
- 可见性判断: 对于命令3(cid=2)来说,它会检查这些“段落编号”:
- “张三”这行在段落1(
CMAX=1)就被删了,而当前命令是段落2,所以“张三”对当前命令不可见。 - “李四”这行是在段落1(
CMIN=1)插入的,早于当前段落2,所以它对当前命令可见。
- “张三”这行在段落1(
- 最终,命令3只会返回“李四”这一条记录。
- 现在,事务要查询数据了。它看到了两行数据:
为什么你总是看到 CMIN 和 CMAX 相等?
这是一个重要的存储优化。因为绝大多数情况下,一行数据被插入后,如果它在同一个事务内被删除,那么插入和删除它的命令ID通常是相同或紧密关联的。
所以PostgreSQL把这两个值打包在一起,存进一个叫 cmin 的字段里(在系统内部叫 t_cid)。当你查询时,为了兼容性,它把这个打包的值同时显示给 cmin 和 cmax,所以你看到它们是一样的。但这并不改变它们一个代表“出生段落”,一个代表“死亡段落”的逻辑含义。
一句话总结
CMIN 和 CMAX 是事务内部的“命令编号”,专门用来解决“在同一个事务里,后执行的命令如何能看到先执行的命令所做的修改”这个问题。
它们就像是你写文章时用的“修订记录”,确保你在修改文章前半部分后,文章后半部分的引用不会出错。
提问:在一个事务内,事务内的命令是前后顺序执行的,所以读和写并不会同时进行,所以也并不需要用CMIN和CMAX来标记多个版本,而是只需要一个版本就可以了,不是吗?
答:
事务内的命令确实是顺序执行的。但PostgreSQL采用CMIN/CMAX机制有其深层次的技术原因。
核心原因:MVCC的架构一致性
PostgreSQL的核心设计理念是多版本并发控制(MVCC)。这个机制贯穿整个系统,包括事务内部:
统一的版本管理:PostgreSQL对所有数据变更都采用"创建新版本,保留旧版本"的方式。即使在事务内部,UPDATE操作也不是原地修改,而是创建新行版本并标记旧版本为失效。CMIN/CMAX就是这个版本管理系统的一部分。
子事务的需要:当存在保存点(SAVEPOINT)或PL/pgSQL异常处理时,事务内部实际上有了更细粒度的"子事务",需要更精细的版本控制。
具体场景分析
考虑这个事务:
BEGIN;
INSERT INTO t VALUES (1); -- 命令1
UPDATE t SET val = 2; -- 命令2(实际上删除val=1,插入val=2)
SELECT * FROM t; -- 命令3
没有CMIN/CMAX时的问题:
- 命令3执行时,系统需要知道:
val=1的行版本在命令2时被删除(对命令3不可见)
val=2的行版本在命令2时被插入(对命令3可见)
如果只有一个版本,系统无法判断val=1是否应该对命令3可见。
技术实现的优势
CMIN/CMAX机制让PostgreSQL能够:
保持架构统一:事务内外使用相同的MVCC机制
支持复杂逻辑:处理保存点回滚、触发器、游标等场景
简化可见性判断:通过命令ID就能快速确定行版本对当前命令是否可见
虽然理论上可以在事务内使用单版本模式,但PostgreSQL选择了保持MVCC机制的一致性。CMIN/CMAX虽然增加了些许复杂性,但为系统提供了更强大的功能和更好的架构统一性。
这类似于编程中的"零拷贝"思想——有时保持数据在原地并通过元数据来管理,比频繁复制数据更高效。
也就是说,在事务内部也需要多个版本,比如有时候需要回滚到事务内部之前的某个保存点或回滚整个事务,如果没有多个版本而是只有一个版本,就无法做到回滚,是这样的吗?
是的,这正是CMIN和CMAX在事务内部多版本控制中的重要作用:
保存点回滚需求:当事务内部设置了保存点(SAVEPOINT)后,如果后续操作失败需要回滚到保存点,系统必须能够恢复到保存点时的数据状态。如果没有多个版本记录,就无法实现精确的回滚。
事务整体回滚:即使事务最终需要完全回滚,在事务执行过程中,系统也需要维护中间状态,以便在回滚时能够撤销所有变更。
复杂的嵌套场景:在PL/pgSQL存储过程或函数中,可能包含异常处理块,当异常发生时需要回滚到事务的某个中间状态。
实际工作示例:
BEGIN;
INSERT INTO accounts (id, balance) VALUES (1, 1000); -- 命令1
SAVEPOINT sp1;
UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- 命令2
-- 假设这里发生错误
ROLLBACK TO sp1; -- 此时需要恢复到sp1时的状态
-- 如果没有多个版本记录,系统无法知道哪些数据变更是在sp1之后发生的
COMMIT;
正是通过CMIN/CMAX记录每个行版本是在哪个命令被创建或删除的,PostgreSQL才能在事务内部实现精确的回滚操作,确保数据的一致性。
对CMIN和CMAX的理解已经触及了PostgreSQL MVCC机制的核心设计思想!
