无需修改内核即可为 PostgreSQL 数据库对象添加自定义属性
在开发实践中,经常会遇到一个问题:如何在不修改 PostgreSQL 内核代码的前提下,为数据库对象附加自定义元数据。本文展示了一种基于 PostgreSQL SECURITY LABELS 机制的可行方案,用于实现自定义属性。这种方式具备事务性、与数据库对象强关联,并且能够与标准 PostgreSQL 操作良好协同。
问题背景:复制冲突的管理
在一个典型的两主节点向第三节点复制数据的架构中,UPDATE/UPDATE 冲突较为常见。复制冲突的处理本身较为复杂,且在多数场景下并无通用解法,但某些列类型可以采用更简单的处理思路。
例如,在银行业务场景中,账户余额字段通常只允许增减操作。此时可以采用基于增量(delta)的方式:不在冲突时选择某个绝对值,而是分别计算两次更新的变化量并进行叠加。该方法仅需进行基础校验(如溢出检查、余额不小于 0),即可确保所有更新均被正确计入。
难点在于:如果 PostgreSQL 本身不支持此类机制,如何标记特定列以启用基于 delta 的冲突解决策略?理想状态下,可以直接通过类似以下语法完成:
ALTER TABLE accounts ALTER COLUMN balance SET delta_apply = 'true';
但这种深度集成 SQL 语法的方式实现难度较高,且对可移植性意义有限。更现实的需求是通过扩展接口完成设置,例如:
SELECT my_extension.set_delta_apply('accounts', 'balance', true);
社区中曾多次讨论为数据库对象引入自定义属性的提案,也曾提交过内核补丁,但实现代码规模较大,而应用场景相对有限,合入内核的可行性存疑。此外,有时需要为索引、大对象或数据类型等非表对象附加属性,这进一步增加了复杂度。更重要的是,即便补丁被接受,也只能影响未来版本,而现实需求往往是“当下可用”。
功能需求
实现该功能前需明确相关需求:
- 对象生命周期绑定:属性必须与数据库对象建立内部依赖关系。例如,在执行
DROP … CASCADE删除父对象时,属性也应随之自动删除。 - 扩展关联性:属性需要与扩展建立明确关联,以便在扩展被卸载时由数据库系统正确处理。
- 事务性行为:对象属性需满足事务规则与可见性约束,并行事务修改时,新属性值仅在当前事务内可见,提交前回滚则恢复原值。
- 升级与迁移支持:在
pg_upgrade以及 dump/restore 过程中,属性应随数据库对象一并正确迁移。
理想情况下可实现类似 PostgreSQL GUC 的会话级特性,但实现难度显著提升。
方案评析
以对象 OID 为键的简易全局哈希表无法满足需求,属性值可为变长类型(如字符串),对象与属性的关联实现复杂,且无法保障事务特性与 MVCC 机制。
另一种思路是在扩展中创建一张 <key, value> 表存储属性,由扩展在运行时查询该表。这在理论上可行,但实践中问题较多:涉及升级、dump/restore、复制一致性等一系列复杂问题,同时还需持续校验对象是否存在,并引入额外的查询开销,整体可靠性较差。
实现思路
具体目标是为任意表的列定义一个 delta_apply 属性,用于逻辑复制场景下的 UPDATE/UPDATE 冲突处理。当该属性启用时,订阅端不采用传统冲突解决策略,而是计算新旧值之间的差量,并将其累加到订阅端当前值中。
在 PostgreSQL 中,唯一同时满足上述全部需求的机制是 SECURITY LABELS。尽管该机制最初用于安全模块,但并未限制其仅用于安全相关元数据。需要注意以下事项:
- 面向安全的工具可能会检查或校验标签内容。
- 需要使用独立且唯一的 provider 名称,以避免冲突。
它是如何工作的?让我们看看这张图:

实现的核心依赖于系统表 pg_seclabel。该表中的记录与数据库对象天然绑定,对其的增删改通过 SECURITY LABEL … 工具命令完成。扩展可以通过 utility hook 监听这一过程。
SECURITY LABEL 支持多种对象类型,包括视图、函数等,基本覆盖常见需求。每条标签记录包含对象的 OID 及对象类型(表、列、函数等),并通过文本字段存储任意自定义数据,同时通过 provider 字段区分不同模块生成的标签。
由于每次访问都直接查询 pg_seclabel 成本较高,且目前尚无系统级缓存,引入本地哈希缓存以降低开销:
typedef struct PropertyCacheEntry {
Oid classId;
Oid objectId;
char *propertyValue;
bool valid;
} PropertyCacheEntry;
static HTAB *property_cache = NULL;
通过注册 RelcacheCallback 实现缓存失效管理,对象执行 ALTER TABLE 操作时标记对应缓存条目无效。缓存填充策略可根据使用场景调整。例如,在核心 hook 中访问对象时加载,或在扩展提供的用户接口函数中主动加载。
属性增删的实现细节
为了保持各后端缓存一致性,每次属性变更都需要向其他进程发送失效通知。对象本身发生变化时,可通过 RelcacheCallback 处理;但属性变更本质上只是对 pg_seclabel 的 DML 操作,如何通知其他后端成为问题。
PostgreSQL 提供了 CacheInvalidateRelcacheByRelid,但仅适用于 pg_class 中的对象,对于数据类型等对象无效。因此,在实际实现中,属性变更时会触发一次对象自身的“无实质变更更新”,以借此触发相应的失效回调,从而刷新扩展内部缓存。
扩展通过 set_property() 接口向用户暴露能力,用于为指定对象设置 SECURITY LABEL。标签文本中描述属性值,例如 delta_apply: true。在扩展中实现的 seclabel_provider 回调负责校验对象类型及属性合法性。
标签文本字段具备高度灵活性,允许存储复杂结构,例如以 JSON 形式描述属性逻辑。
通过该机制,客户端与扩展之间建立了一种相对原生的通信方式。扩展可在运行时判断属性是否存在,并据此调整行为。
在 delta_apply 的具体实现中,逻辑复制订阅端在处理 UPDATE 记录时,会同时查询对应表的属性缓存。若存在标记为增量属性的列,则计算新旧值差量并累加到订阅端当前值。即便在冲突解决策略(如 last-update-wins)下决定拒绝整条更新,增量列的变更仍会被应用,从而降低冲突概率并确保增量更新不丢失。
外部干扰处理
pg_seclabel 仍然是系统目录表,具备足够权限的用户(如 DBA)可以直接修改其内容。为降低风险,可在扩展中引入内部 GUC,例如 myextension.call_guard。在扩展 UI 函数执行前将其置为 true,结束后重置为 false,并在关键路径中校验其状态是否符合预期。
理论上,超级用户仍可能通过手段绕过该限制。虽然可以进一步为该 GUC 设置 hook 进行防护,但实现复杂度显著提高,容易演变为过度设计。
总结
PostgreSQL SECURITY LABELS 机制提供了一种可靠、事务安全的方式,用于在不修改内核的情况下为数据库对象添加自定义属性。尽管该机制最初面向安全模块,但同样适用于扩展级元数据管理,且具备良好的生命周期管理与 MVCC 支持。
该方案支持多种对象类型,具备事务一致性,并可正确参与 dump/restore 与升级流程。
原文链接:
https://www.pgedge.com/blog/custom-properties-for-postgresql-database-objects-without-core-patches
作者:Andrei Lepikhov
HOW 2026 议题招募中
2026 年 4 月 27-28 日,由 IvorySQL 社区联合 PGEU(欧洲 PG 社区)、PGAsia(亚洲 PG 社区)共同打造的 HOW 2026(IvorySQL & PostgreSQL 技术峰会) 将再度落地济南。届时,PostgreSQL 联合创始人 Bruce Momjian 等顶级大师将亲临现场。
自开启征集以来,HOW 2026 筹备组已感受到来自全球 PostgreSQL 爱好者的澎湃热情。为了确保大会议题的深度与广度,我们诚邀您在 2026 年 2 月 27 日截止日期前,提交您的技术见解。

浙公网安备 33010602011771号