Mats Kindahl:构建表访问方法,第6部分:TRUNCATE
内容提要
本文介绍PostgreSQL表访问方法中TRUNCATE的处理:TRUNCATE需用新arena替换旧arena,并保证事务回滚安全——提交时释放旧arena,回滚时释放新arena。若表在同一(子)事务中创建或已被截断,则走原地截断路径,直接重置游标。实现需将缓存条目保留至事务结束,并处理保存点回滚后无缓存条目的情况。
延伸解读
TRUNCATE 为何不能直接重置游标
文章指出,TRUNCATE 可能运行在会回滚的事务中。若直接重置 arena 的 tuple_free 游标,回滚后 relfilenode 虽恢复,但游标不会恢复,TRUNCATE 后插入的行会覆盖旧数据。同理,立即释放旧 arena 也不行,因为已释放的段无法恢复。因此旧 arena 必须保持原样,直到事务确认提交。
两个 arena 的释放时机取决于事务结果
回调为 TRUNCATE 安排两条清理记录:新 arena 在事务中止时释放,旧 arena 在事务提交时释放。这样提交后旧 arena 被回收、新 arena 保留;回滚后新 arena 被回收、旧 arena 继续持有真实数据。两条记录走与 CREATE/DROP 相同的清理列表和子事务回调,因此保存点回滚也能正确处理。
原地截断路径与缓存条目的保留
若表在同一(子)事务中创建或已被截断,Postgres 走 relation_nontransactional_truncate,不分配新 relfilenode,直接重置游标。为此,finalize 函数不再立即删除缓存条目,而是留到事务结束由回调统一移除。否则同一事务中 CREATE 后紧跟 TRUNCATE 会找不到条目而什么都不做。
保存点回滚后条目缺失的兜底处理
保存点内 TRUNCATE 回滚时,其缓存条目会随新建 arena 一起被移除,但原 arena 仍是活跃的。之后释放保存点再 TRUNCATE 同一张表,会走原地路径却找不到条目。此时回调回退到 relfilenode,它仍是 arena 的句柄,据此映射共享内存段并重置 tuple_free,保证截断仍然生效。
Q&A
PostgreSQL 表访问方法中,TRUNCATE 操作如何安全地替换表的 arena?
TRUNCATE 会为表创建一个新的 arena,并安排清理:如果事务提交,则释放旧 arena;如果事务回滚,则释放新 arena。这样确保旧 arena 在事务提交前保持原样,避免回滚时数据丢失。
为什么不能直接重置 arena 的游标来实现 TRUNCATE?
因为 TRUNCATE 可能在会回滚的事务中执行,回滚后旧 arena 仍持有真实数据。重置游标会丢失数据,且回滚无法恢复游标,导致后续插入覆盖旧数据。
在同一个事务中先创建表再 TRUNCATE,PostgreSQL 会走哪条路径?
会走原地截断路径(in-place path),调用 relation_nontransactional_truncate,直接重置 arena 的游标,因为回滚会丢弃整个表,无需保留旧数据。
TRUNCATE 在保存点回滚后,缓存条目缺失时如何处理?
如果 TRUNCATE 在保存点内回滚,其缓存条目会被移除。之后对同一事务中早先创建的表再次 TRUNCATE 时,会走原地路径,此时缓存条目可能不存在,回调会通过 relfilenode 找到 arena 并重置游标。
memtable_ProcessUtility 如何扩展以支持 TRUNCATE?
它在调用标准 ProcessUtility 后,检查语句是否为 TruncateStmt,若是则遍历所有关系,对每个关系调用 memtable_finalize_new_relation,将 pg_class 中的 relfilenode 更新为 DSM 句柄。
TRUNCATE 时如何确保旧 arena 在事务提交前不被释放?
在 relation_set_new_filelocator 回调中,会捕获旧 arena 的句柄,并安排清理:新 arena 在事务中止时释放,旧 arena 在事务提交时释放。这样旧 arena 会一直保留到事务确定提交。