表访问方法:PostgreSQL 中数据更新的处理方式
作者:Cary
前言
本文将详细探讨 PostgreSQL 如何处理更新操作。在 PostgreSQL 中,成功的更新可以被视为“插入一条新记录”,同时“标记旧记录为不可见”,这是因为 PostgreSQL 使用了 MVCC 技术。这个过程听起来简单,但实际上有许多因素需要考虑,以确保更新的成功。
涉及的 API
在执行更新操作时,主要涉及两种表访问方法的 API:
tuplefetchrow_version()
:此函数用于查找给定 TID 的元组的最新版本。我们需要使用给定的 TID 查找特定的元组进行更新。此外,如果适用,可以提供一个快照结构来执行可见性检查。该函数会提取元组并将其转换为 Tuple Table Slot,如果成功提取元组,则返回 true,否则返回 false。tuple_update()
:这是处理元组更新请求的主要处理函数。此函数会接收多个参数来执行更新操作:- 旧元组的 TID:待更新的旧元组的位置
- 新元组(以 Tuple Table Slot 表示):PostgreSQL 将其转换为 HeapTuple 进行更新
- 命令 ID 和快照:用于对待更新的旧元组执行可见性检查
更新流程
在执行更新操作之前,PostgreSQL 会进行一系列的检查和考量。此过程如下图所示:
1. 确定更新的列
主更新流程执行的第一个检查是确定需要更新的列,特别是要确认是否更新了标识键列(例如主键、索引键或分区键)。因为如果标识键列未被更新,可能无需为了执行更新操作而获取独占锁。此外,对于逻辑复制中的副本标识列,还需要进行额外的处理,以确保在 WAL 文件中记录足够的信息,以便逻辑订阅者能够识别出哪一行已被更新。
2. 确定元组是否可更新
这是关于并发控制的关键步骤。PostgreSQL 是一个多进程系统,一个元组可能会同时被多个客户端连接更新。当然,PostgreSQL 不允许一个元组同时被多个客户端更新,因此在继续操作之前,我们需要确保没有其他客户端正在更新同一个元组。这可以通过查看当前旧元组头部的 xmax
值(修改该元组的事务 ID)来检查,并查找 CLOG
或其提示位(hintbit)标志来确定 xmax
值是否已提交。在调用 HeapTupleSatisfiesUpdate()
后,有以下几种情况需要考虑:
- 元组不可见:这意味着另一个客户端后端已经更新了该元组(将其标记为不可见)并且已提交了事务。如果是这种情况,我们无法进行更新,因为根据快照,已经没有可更新的内容了。因此,系统将在此处报错。
- 元组正在被更新:这意味着另一个客户端后端已经更新了该元组(将其标记为不可见),但尚未提交或回滚事务。如果是这种情况,我们必须在此处等待,直到其他客户端的事务提交或中止。系统将进入等待循环,同时执行死锁检测,以确保我们正在等待的事务 ID 没有在等待我们完成(交叉等待,即死锁)。
- 元组可更新:这意味着该元组未被其他客户端后端更新,我们可以继续对其进行更新。
3. 准备新的元组头部
一旦我们确认可以更新旧元组,PostgreSQL 将开始准备新元组,将其从元组表槽(Tuple Table Slot)格式转换为堆元组(HeapTuple)格式。然后填写必要的头部信息,并准备插入操作。
4. TOAST 检查
一旦我们准备好了新的堆元组(HeapTuple)结构,就需要检查其大小是否适合更新期间给定的缓冲页剩余空间。我们需要确认新元组是否可以放入给定缓冲页的剩余空间中。如果可以,则无需执行额外操作,直接进入下一阶段处理。如果给定的缓冲页空间不足以容纳这个新的堆元组,那么我们需要执行 TOAST 操作。TOAST 是 PostgreSQL 中的一种技术,用于将大型数据分解为较小的块并以分布式方式存储。系统将调用 heap_toast_insert_or_update()
来完成 TOAST 操作,并生成一条 WAL 记录,表明在此处执行了 TOAST 操作,且在使用该值时需要进行 DE-TOAST 操作。
5. 从旧元组中提取副本标识
此操作仅仅是从旧元组中提取副本标识(如主键、索引键等),以便将这些附加信息包含在 WAL 段中,从而准确告知逻辑复制订阅者具体是哪一行被更新了。如果没有这个副本标识,逻辑复制订阅者只会收到某一行被更新为新值的通知,但无法确定具体是哪一行被更新了。
6. 页面设置为可修剪
此操作主要是对缓冲页的头部进行修改,以表明该页面将包含一个死元组,因为我们正在对其执行更新操作。这一标记主要是供 VACUUM 进程查看,用于判断一个页面是否完全可见(即页面内的所有元组都可见)或可修剪(即页面内存在需要清理的死元组)。
7. HOT(Heap Only Tuple) 更新
这是 PostgreSQL 采用的一种优化手段,用于在条件合适的情况下“节省索引元组”。之所以称为仅元组堆更新,是因为它字面上意味着只有堆元组,而没有直接关联的索引元组,即使在该表上创建了索引。
触发 HOT 更新以“节省索引元组”的条件如下:
- 没有索引列被更新。
- 新的元组可以插入到与旧元组相同的页面中。
如果满足上述条件,PostgreSQL 会将新的元组插入页面,并在页面内将旧元组与新元组“链接”在一起。与旧元组关联的索引元组仍然指向旧元组,但由于我们已将旧元组和新元组链接在同一页面内,我们仍然可以通过先定位到旧元组,然后沿着链接找到新元组。这样,PostgreSQL 就不需要创建一个新的索引元组来直接指向新元组,从而节省了一些潜在的元组空间。
8. 关系放入堆元组
在完成上述所有检查后,我们现在可以将新的元组放入指定的页面,并正确设置元组头部信息和 HOT 标志。
9. 将旧元组标记为不可见
新元组插入后,我们现在可以通过将旧元组的 xmax 值设置为当前事务 ID,设置其提示位(hintbit)和适当的 HOT 标志,将旧元组标记为不可见。
10. 标记缓冲区为脏页
这是一个缓冲区管理器例程,用于通知管理器当前页面内容已被修改。因此,在需要置换缓冲区页面前,必须先将该页面的修改内容刷新到磁盘。周期性的检查点进程会主动尝试优先将此脏页刷新到磁盘。
11. 缓存使堆元组无效
该机制用于通知所有活跃的后端进程,如果它们在其本地缓存中存储了刚刚被标记为不可见(已删除)的堆元组,则使这些元组失效。
12. 更新索引标志
在更新结束时,我们需要设置一个标志,告知执行器是否需要为新元组创建索引元组。在 HOT 的情况下,或者当表上没有创建索引时,我们通常会将其设置为 false。
总结
与插入和顺序扫描不同,更新操作在完成时需要更多的考虑。它不仅需要考虑并发控制,还必须考虑一系列优化措施、超大元组以及用于逻辑复制的副本标识。这些因素使得更新操作通常成为执行起来更为昂贵的操作。
关于 IvorySQL
lvorySQL 是由瀚高股份主导研发的一款开源的兼容 Oracle 的 PostgreSQL。IvorySQL 与 PostgreSQL 国际社区紧密合作,保持与最新 PG 版本内核同步,为用户提供便捷的升级体验。基于双 Parser 架构设计,100% 与原生 PostgreSQL 兼容,支持丰富的 PostgreSQL 周边工具和扩展,并根据用户需求提供定制化工具。同时,IvorySQL 4.0 提供更全面灵活的 Oracle 兼容功能,具备高度的 SQL 和 PL/SQL 兼容性能够为企业构建更加高效、稳定和灵活的数据库解决方案。
- 官网:https://www.ivorysql.org
- GitHub(欢迎点击 star 收藏哦):https://github.com/IvorySQL/IvorySQL
- 社群:VX 搜索“ivorysql_official” 添加小助理进群