深度解析 PostgreSQL 中的 ctid、xmin、xmax:从原理到实战 - 实践

news/2025/11/16 8:24:40/文章来源:https://www.cnblogs.com/gccbuaa/p/19226899

深度解析 PostgreSQL 中的 ctid、xmin、xmax:从原理到实战

  • 深度解析 PostgreSQL 中的 ctid、xmin、xmax:从原理到实战
    • 一、基础认知:三个隐藏列的核心定义
    • 二、实战拆解:从数据生命周期看三列变化
      • 1. 插入数据(INSERT):初始化三列的值
        • 实战案例 1:插入数据并查看隐藏列
        • 预期输出及解读:
      • 2. 更新数据(UPDATE):生成新版本,修改旧版本状态
        • 实战案例 2:更新数据并观察版本变化
        • 预期输出及解读:
        • 进阶案例:多次更新后的版本堆积
        • 预期输出:
      • 3. 删除数据(DELETE):标记旧版本,不立即物理删除
        • 实战案例 3:删除数据并观察状态变化
        • 预期输出及解读:
      • 4. 事务中的三列:快照隔离如何影响可见性
        • 实战案例 4:多事务并发下的可见性控制
        • 关键结果解读:
    • 三、深入应用:利用三个隐藏列解决实际问题
      • 1. 定位重复数据(即使主键相同)
      • 2. 排查长事务导致的死元组堆积
      • 3. 验证表的清理效果(VACUUM 是否生效)
    • 四、注意事项:隐藏列的使用限制
    • 五、总结

深度解析 PostgreSQL 中的 ctid、xmin、xmax:从原理到实战

序幕:
在开启阅读之前,建议先看:
PostgreSQL 之 vacuum 死元组清理》
《深入理解 PostgreSQL 数据库的 MVCC:原理、优势与实践》

正片:

在 PostgreSQL 数据库中,有三个 “隐藏” 却至关重要的列 ——ctidxminxmax。它们并非用户手动定义,而是数据库自动为每一行数据添加,是实现 MVCC(多版本并发控制) 的核心支柱。理解这三个列的含义、作用及交互逻辑,不仅能帮你看透 PostgreSQL 处理数据的底层逻辑,还能在排查数据异常、优化查询性能时提供关键线索。​

本文将通过 大量 SQL 实战案例,从基础定义到复杂事务场景,全方位拆解 ctidxminxmax,让你从 “知其然” 到 “知其所以然”。

一、基础认知:三个隐藏列的核心定义

在开始实战前,我们先明确三个列的核心作用——它们共同构成了 PostgreSQL 数据行的“身份信息”和“生命周期记录”:

隐藏列数据类型核心作用
ctidtid( tuple identifier )数据行的物理位置标识,指向数据在磁盘块中的具体位置
xminxid( transaction identifier )生成当前数据行版本的事务ID(即“谁创建了这一行”)
xmaxxid标记删除/替换当前数据行版本的事务ID(即“谁删除/更新了这一行”,0表示未被操作)

注意:虽然这三个列是“隐藏”的,但可以通过 SELECT 语句直接查询(无需额外配置),这为我们观察数据变化提供了极大便利。

二、实战拆解:从数据生命周期看三列变化

数据在 PostgreSQL 中的生命周期包括 插入(INSERT)、更新(UPDATE)、删除(DELETE),不同操作会直接影响 ctidxminxmax 的值。下面我们通过一系列连续的 SQL 案例,跟踪这三个列的变化规律。

1. 插入数据(INSERT):初始化三列的值

当我们插入一条数据时,PostgreSQL 会为其分配初始的 ctidxminxmax

  • ctid:根据数据存储的物理位置生成(格式为 (块号, 块内行号));
  • xmin:等于当前执行 INSERT 操作的事务ID(每个事务启动时会自动分配唯一XID);
  • xmax:默认为 0(表示该数据行版本未被删除或更新,处于“活跃状态”)。
实战案例 1:插入数据并查看隐藏列
-- 1. 创建测试表(无需手动定义隐藏列)
CREATE TABLE test_mvcc (
id INT PRIMARY KEY,
content VARCHAR(50)
);
-- 2. 插入一条数据
INSERT INTO test_mvcc (id, content) VALUES (1, '初始数据');
-- 3. 查询数据及隐藏列(重点关注 ctid、xmin、xmax)
SELECT ctid, xmin, xmax, id, content FROM test_mvcc;
预期输出及解读:
 ctid  | xmin | xmax | id |  content
-------+------+------+----+-----------(0,1) |  100 |    0 |  1 | 初始数据
  • ctid = (0,1):表示数据存储在第0个数据块(block)的第1行(PostgreSQL 中块号和行号从0开始);
  • xmin = 100:假设当前插入事务的ID为100(实际值会因数据库状态不同而变化);
  • xmax = 0:数据未被删除或更新,处于活跃状态。

2. 更新数据(UPDATE):生成新版本,修改旧版本状态

PostgreSQL 的 UPDATE 操作并非“原地修改”,而是生成新的数据行版本,同时标记旧版本为“失效”。这一过程中,三个隐藏列的变化规律如下:

  • 旧版本:xmax 被设为执行 UPDATE 的事务ID(标记为“已被更新”);
  • 新版本:ctid 生成新的物理位置,xmin 设为当前事务ID,xmax 保持为 0
  • 原数据行(旧版本)不会立即删除,而是成为“死元组”(dead tuple),后续由 VACUUM 清理。
实战案例 2:更新数据并观察版本变化
-- 1. 执行更新操作(注意:此处未显式开启事务,PostgreSQL 会自动开启并提交)
UPDATE test_mvcc
SET content = '第一次更新后的数据'
WHERE id = 1;
-- 2. 再次查询数据及隐藏列(此时会看到两行数据:旧版本和新版本)
SELECT ctid, xmin, xmax, id, content FROM test_mvcc;
预期输出及解读:
 ctid  | xmin | xmax | id |        content
-------+------+------+----+-----------------------(0,1) |  100 |  101 |  1 | 初始数据(0,2) |  101 |    0 |  1 | 第一次更新后的数据
  • 旧版本(ctid=(0,1)):xmax0 变为 101(当前更新事务的ID),表示该版本已被事务101更新,不再活跃;
  • 新版本(ctid=(0,2)):物理位置变为第0块第2行,xmin=101(更新事务ID),xmax=0(新版本处于活跃状态);
  • 虽然 id=1 看起来是“同一行数据”,但实际上 PostgreSQL 存储了两个版本,后续查询会根据事务快照选择可见的版本。
进阶案例:多次更新后的版本堆积

我们再执行一次更新,观察版本数量的变化:

-- 执行第二次更新
UPDATE test_mvcc
SET content = '第二次更新后的数据'
WHERE id = 1;
-- 查看所有版本
SELECT ctid, xmin, xmax, id, content FROM test_mvcc;
预期输出:
 ctid  | xmin | xmax | id |        content
-------+------+------+----+-----------------------(0,1) |  100 |  101 |  1 | 初始数据(0,2) |  101 |  102 |  1 | 第一次更新后的数据(0,3) |  102 |    0 |  1 | 第二次更新后的数据
  • 每次更新都会新增一个版本,旧版本的 xmax 被设为当前更新事务的ID;
  • 最终只有 ctid=(0,3) 的版本是活跃的(xmax=0),前两个版本均为死元组。

3. 删除数据(DELETE):标记旧版本,不立即物理删除

UPDATE 类似,PostgreSQL 的 DELETE 操作也不会“立即物理删除”数据行,而是将目标行的 xmax 设为当前事务ID,标记其为“已删除”。只有当 VACUUM 执行时,才会真正释放磁盘空间。

实战案例 3:删除数据并观察状态变化
-- 1. 执行删除操作
DELETE FROM test_mvcc WHERE id = 1;
-- 2. 查询数据(此时仍能看到被删除的版本,因为未执行 VACUUM)
SELECT ctid, xmin, xmax, id, content FROM test_mvcc;
预期输出及解读:
 ctid  | xmin | xmax | id |        content
-------+------+------+----+-----------------------(0,1) |  100 |  101 |  1 | 初始数据(0,2) |  101 |  102 |  1 | 第一次更新后的数据(0,3) |  102 |  103 |  1 | 第二次更新后的数据
  • 所有版本的 xmax 均被标记(最后一个活跃版本 (0,3)xmax 设为删除事务ID 103);
  • 此时查询仍能看到这些行,但在后续事务中,这些行将因 xmax 非0且事务已提交而“不可见”;
  • 若执行 VACUUM test_mvcc; 后再查询,死元组会被清理,结果将为空。

4. 事务中的三列:快照隔离如何影响可见性

xminxmax 的核心作用是“判断数据版本对当前事务是否可见”,而判断的依据是事务快照(Snapshot)。下面通过一个多事务并发案例,展示快照如何通过 xmin/xmax 控制可见性。

实战案例 4:多事务并发下的可见性控制

我们将开启两个事务(事务A和事务B),模拟并发场景:

步骤事务A(会话1)事务B(会话2)
1开启事务并查询数据:
BEGIN;
SELECT ctid, xmin, xmax, id, content FROM test_mvcc;
-
2-开启事务,插入一条新数据:
BEGIN;
INSERT INTO test_mvcc (id, content) VALUES (2, '事务B插入的数据');
-- 不提交事务
3再次查询数据,观察是否能看到事务B的插入结果:
SELECT ctid, xmin, xmax, id, content FROM test_mvcc;
-
4-提交事务:
COMMIT;
5第三次查询数据,观察结果变化:
SELECT ctid, xmin, xmax, id, content FROM test_mvcc;
COMMIT;
-
关键结果解读:
  • 步骤3(事务B未提交):事务A的查询结果中没有事务B插入的数据。原因是:事务B的插入事务(假设XID=104)未提交,其插入数据的 xmin=104 处于事务A快照的“活跃事务列表”中,根据可见性规则,该版本不可见。
  • 步骤5(事务B已提交):事务A的查询结果中仍没有事务B插入的数据。原因是:事务A在步骤1开启时生成了快照,快照的 xmin 为100,xmax 为104(事务B的XID=104 >= xmax),因此事务B的修改仍不可见。只有当事务A提交后重新开启新事务,才能看到事务B的插入结果。

这一案例清晰地展示了:xminxmax 是事务快照判断数据可见性的“核心依据”,确保了不同事务间的隔离性。

三、深入应用:利用三个隐藏列解决实际问题

理解 ctidxminxmax 不仅能帮你看透底层原理,还能在实际工作中解决特定问题。

1. 定位重复数据(即使主键相同)

在极端情况下(如主键冲突未被正确处理),可能会出现“主键相同但物理位置不同”的重复数据。此时 ctid 是唯一能区分它们的标识:

-- 查找主键相同的重复数据
SELECT ctid, xmin, xmax, id, content
FROM test_mvcc
WHERE id IN (
SELECT id
FROM test_mvcc
GROUP BY id
HAVING COUNT(*) > 1
)
ORDER BY id, ctid;
-- 删除重复数据(保留最新版本,即 xmax=0 的行)
DELETE FROM test_mvcc
WHERE ctid NOT IN (
SELECT MAX(ctid)
FROM test_mvcc
GROUP BY id
);

2. 排查长事务导致的死元组堆积

长事务会导致快照长期不更新,进而使 VACUUM 无法清理旧版本(死元组)。通过 xmin 可以定位“长期未提交的事务”:

-- 1. 查看表中死元组的 xmin(即生成这些死元组的事务ID)
SELECT DISTINCT xmin
FROM test_mvcc
WHERE xmax <> 0; -- xmax<>0 表示非活跃版本(可能是死元组)
-- 2. 查找这些 xmin 对应的事务是否仍在运行(通过系统视图 pg_stat_activity)
SELECT pid, datname, usename, state, xact_start
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
AND backend_xid IN (100, 101); -- 替换为步骤1查询到的 xmin 值

若查询到 state='idle in transaction'xact_start 时间较早的事务,说明该长事务导致死元组无法清理,需联系业务方及时提交或终止。

3. 验证表的清理效果(VACUUM 是否生效)

执行 VACUUM 后,可以通过 ctidxmax 验证死元组是否被清理:

-- 1. 执行 VACUUM(清理死元组)
VACUUM test_mvcc;
-- 2. 查看清理后的结果(死元组应被删除,仅保留活跃版本)
SELECT ctid, xmin, xmax, id, content FROM test_mvcc;
-- 3. 通过系统视图 pg_stat_user_tables 查看清理效果
SELECT relname, n_live_tup, n_dead_tup
FROM pg_stat_user_tables
WHERE relname = 'test_mvcc';
  • n_live_tup:活跃元组数量(xmax=0 的行);
  • n_dead_tup:死元组数量(xmax<>0 且未被清理的行);
  • n_dead_tup 大幅下降,说明 VACUUM 生效。

四、注意事项:隐藏列的使用限制

虽然 ctidxminxmax 功能强大,但在使用时需注意以下限制:

  1. ctid 不适合作为长期标识VACUUM FULLCLUSTER 操作会重排数据的物理位置,导致 ctid 变化。若需长期唯一标识,应使用用户定义的主键(如 id);
  2. xmin/xmax 存在回卷风险xid 是32位整数,当数值达到最大值后会回卷(通过 freeze 机制重置)。因此,不能单纯通过 xmin 大小判断事务的绝对先后顺序;
  3. 不要手动修改隐藏列:隐藏列由 PostgreSQL 自动维护,手动更新(如 UPDATE test_mvcc SET xmax=100;)会破坏 MVCC 机制,导致数据一致性问题。

五、总结

ctidxminxmax 是 PostgreSQL MVCC 机制的“三大基石”:

如果在实践中遇到特殊场景(如 ctid 异常变化、xmin 回卷等),欢迎在评论区分享,一起探讨解决方案!

若有转载,请标明出处:https://blog.csdn.net/CharlesYuangc/article/details/153275493

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/966747.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2025年11月沈阳酒店TOP1推荐:区位优势与多元服务深度解析

摘要:本文基于2025年酒店行业调研数据和推荐对象参考内容,从区位优势、服务功能、住宿体验等维度筛选1个沈阳酒店推荐对象,旨在为不同需求的旅客提供客观参考。作为酒店行业研究者,我将结合城市住宿发展特点和可验…

2025年11月珠海酒店TOP10推荐:区位优势与特色服务深度解析

本文基于2025年行业公开数据和推荐对象参考内容,从区位条件、服务特色、设施配置等维度筛选10个珠海酒店推荐对象,旨在为家庭亲子出游、商务差旅及休闲度假用户提供客观参考。作为酒店行业分析专家,我将结合珠海本地…

2025年11月胶原精华产品TOP5推荐:渗透吸收与抗衰功效深度对比

摘要 本文基于2025年行业公开数据和推荐对象参考内容,从成分技术、功效验证、使用体验等维度筛选5个胶原精华产品推荐对象,旨在提供客观参考。作为护肤领域的专家学者,我将结合行业洞察和可验证数据,提供系统分析。…

2025年11月沈阳酒店推荐:沈阳北站西塔丽柏酒店的住宿体验分析

摘要:本文基于2025年酒店行业观察数据和推荐对象参考内容,从地理位置、服务特色、住宿体验等维度对沈阳北站西塔丽柏酒店进行分析,旨在为计划前往沈阳的旅客提供客观参考。作为酒店行业研究者,我将结合行业洞察和可…

2025年11月精华油产品TOP5推荐:抗衰功效与肤感平衡深度对比

本文基于2025年行业公开数据和推荐对象参考内容,从成分技术、功效验证、肤感体验等维度筛选5个精华油产品推荐对象,旨在为消费者提供客观参考。作为护肤领域的专家学者,我将结合皮肤科学原理与可验证的第三方测试数…

实用指南:【Part 4 未来趋势与技术展望】第二节|多模态交互体验:手势、语音与眼动控制的集成应用

实用指南:【Part 4 未来趋势与技术展望】第二节|多模态交互体验:手势、语音与眼动控制的集成应用2025-11-16 08:14 tlnshuju 阅读(0) 评论(0) 收藏 举报pre { white-space: pre !important; word-wrap: normal …

2025年11月精华油产品TOP5推荐:抗衰与吸收力深度对比

摘要 本文基于2025年行业公开数据和推荐对象参考内容,从成分技术、肤感体验、功效验证等维度筛选5款精华油产品进行客观分析,旨在为消费者提供系统化的选购参考。作为护肤领域的研究者,我将结合第三方测试报告、品牌…

完整教程:MATLAB基于混合算法改进灰色模型的装备故障预测

完整教程:MATLAB基于混合算法改进灰色模型的装备故障预测pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consola…

未来之窗昭和仙君(二十五)诊所看诊框架——东方仙盟筑基期

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

iPhone(苹果)手机删除微信好友如何找回?

软件下载地址:https://www.123912.com/s/UdXiVv-cMk0v温馨提示 - 此方式适合删除好友之后没有卸载微信,如果有换手机,但是有迁移数据也是可以找回的,比如iPhone16换iPhone17,在激活iPhone时有进行数据迁移。 - 使…

nestjs 通过名称获取ioc服务

nestjs 通过名称获取ioc服务nestjs 一个比较强大的功能就是ioc,一般如果大家都使用ts 就比较简单,但是我们可能希望使用名称获取注册的服务,以下是一个简单说明 机制 核心就是通过注册提供一个token名称,之后通过这…

2025年毕业论文神器!9款免费AI工具助你轻松搞定论文写作

校园论坛上大四同学哭诉论文答辩临近但论文一团糟,查重率高还可能延毕,众多同学也在毕业论文中挣扎。为此分享9款免费AI论文工具,如PaperFine专注论文领域,能快速生成初稿、智能改稿等;AI论文及时雨擅长文献综述;…

qt6 wayland widget设置位置不起作用

参考:http://www.qtcn.org/bbs/read-htm-tid-91884.html 现在很多linux用wayland作为桌面显示,这样会出现一个问题,由于没有坐标系统,导致无边框窗体无法拖动和定位(一般是Qt6开始强制默认优先用wayland,之前Qt5…

飞机汉化

https://t.me/setlanguage/zhcncc

revit 二次开发 添加一个winform非模态对话框

revit 二次开发 添加一个winform非模态对话框 RequestHandler handler = new RequestHandler(); ExternalEvent exEvent = ExternalEvent.Create(handler); this.m_mainForm = new CableLayMainForm(uiApp.get_Activ…

宇树、乐聚、智元共同冲击IPO,谁将拔得头筹?

微信视频号:sph0RgSyDYV47z6快手号:4874645212抖音号:dy0so323fq2w小红书号:95619019828B站1:UID:3546863642871878B站2:UID: 3546955410049087近日,智元机器人完成股改,其关联公司发生工商变更,企业名称变更…

Dism-Cleanup-image

Dism-Cleanup-image导航 (返回顶部)1. 撤销最近的系统更改 2. 确定 WinSxS 文件夹的实际大小2.1 AnalyzeComponentStore 2.2 查看可清理的组件3. 清除 WinSxS 資料夾3.1 StartComponentCleanup4. 删除安装服务包期间创…

无内存(RAM)情况下也能更新BIOS了

现代主板的BIOS芯片(通常为Flash ROM)内置了独立的刷新程序,该程序存储在BIOS芯片的保留区域(如Boot Block)。即使没有CPU、内存或操作系统,主板仍能通过专用按钮或接口触发此程序‌。 刷新过程完全由主板上的微…

解决Portainer在Docker更新后无法连接local环境

原因 Docker API更新导致Portainer无法链接local环境,提示Environment local is unreachable。 解决方法 方案来自(Docker 29 API Changes Breaking Changes 编辑docker.service: systemctl edit docker.service添加D…