PostgreSQL 操作自定义系统表

本文介绍 PostgreSQL 数据库内核系统表(System Catalog)完整实践,首先,介绍系统表作为元数据与配置载体的核心概念;随后通过简单实例,说明创建自定义系统表的初始化、头文件定义、数据文件编写及编译注册的四步流程;接着,用通过分析源码介绍了内核源码中系统表 Insert、Update、Delete 等操作实现方式,旨在说明 PG 创建并操作自定义系统表基本方式

01 系统表概念


PostgreSQL 中的系统目录或系统表(System Catalogs)是关系型数据库存放模式(Schema)元数据的地方,部分常见元数据如下:

  • 数据库对象信息: pg_database 数据库信息,pg_class 关系表等对象,pg_proc 函数定义,pg_type 数据类型,pg_operator 定操作符,pg_trigger 触发器,pg_constraint 约束等

  • 对象间关系管理: pg_attribute 描述表的列及其数据类型 pg_typepg_depend 配合 pg_rewrite 追踪视图对基表的依赖,pg_inherits 管理表继承关系等

  • 访问权限控制 :角色信息pg_authid,默认权限规则pg_default_acl

  • 统计信息存储: pg_statisticpg_statistic_ext保存列级统计

这些系统表都属于pg_catalog模式,但是他们的作用范围可分为实例级别和数据库级别,实例级别是指存储在 $PGDATA/global/ 目录下,所有数据库共享同一份数据的全局系统表 ;数据库级别则是每个数据库都拥有独立副本,存储在$PGDATA/base/目录下指定数据库数据目录中的数据库级系统表

全局表并不多,如下表所示主要是全局共享对象数据库、表空间、复制源、角色、参数权限的记录和管理,这些系统表的表空间是pg_global,其他数据库级别的系统表表空间均为pg_default

类别 表名 核心功能
数据库对象 pg_database 记录集群中所有数据库的定义
pg_tablespace 定义所有表空间的路径、所有者等元数据
pg_subscription 管理逻辑复制订阅
pg_replication_origin 记录逻辑复制源节点信息
权限控制 pg_authid 存储所有角色的认证信息和权限属性
pg_db_role_setting 存储角色在特定数据库中的配置覆盖
pg_auth_members 维护角色之间的成员关系
pg_parameter_acl 管理配置参数的访问控制
共享对象管理 pg_shdepend 跟踪数据库对象对共享对象的依赖
pg_shdescription 存储共享对象的描述信息
pg_shseclabel 存储共享对象的安全标签

02 新建系统表


2.1 系统表初始化

用户在创建表等数据库对象时都是通过 PG 的 SQL 引擎来处理创建的,然而对于系统表就无法通过 SQL 引擎来创建,因为这存在一个"鸡生蛋蛋生鸡"问题:如何在没有系统表的情况下创建系统表?例如,pg_class系统表记录了所有关系表,而pg_class本身也是 table,所以在创建pg_class系统表时需要在pg_class系统表中插入一条记录自身的数据,这显然是十分矛盾的

为此 PG 中使用 BKI(Backend Interface)文件定义的脚本实现了一种引导程序在初始创建数据库系统时使用;在 Bootstrap 模式中,允许使用 BKI 脚本在无表环境下创建系统表并填充数据,而普通的 SQL 命令的执行要求系统表必须已经存在

而引导脚本postgres.bki是在编译阶段通过 src/backend/catalog/genbki.plsrc/backend/catalog/catalog.pm 根据src/include/catalog路径下的系统表头文件.h和数据文件.dat生成的;该脚本生成之后被放到 PG 安装目录的 share/postgres.bki 路径下,在使用 initdb初始化数据库时执行该脚本创建并初始化系统表

pg_class 系统表的初始化过程为例详细说明上述过程

src/include/catalog/pg_class.h部分定义如下,首先使用 CATALOG 根据系统表名称pg_class、固定 OID1259和系统表 OID 宏,定义一个表示表结构的 FormData_pg_class 的结构体

c 复制代码
CATALOG(pg_class,1259,RelationRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(83,RelationRelation_Rowtype_Id) BKI_SCHEMA_MACRO
{
    Oid         oid;
    NameData    relname;
    Oid         relnamespace BKI_DEFAULT(pg_catalog) BKI_LOOKUP(pg_namespace);
    Oid         reltype BKI_LOOKUP_OPT(pg_type);
    Oid         reloftype BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_type);
    Oid         relowner BKI_DEFAULT(POSTGRES) BKI_LOOKUP(pg_authid);
    Oid         relam BKI_DEFAULT(heap) BKI_LOOKUP_OPT(pg_am);
    Oid         relfilenode BKI_DEFAULT(0);
    Oid         reltablespace BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_tablespace);
    int32       relpages BKI_DEFAULT(0);
    float4      reltuples BKI_DEFAULT(-1);
    int32       relallvisible BKI_DEFAULT(0);
    int32       relallfrozen BKI_DEFAULT(0);
    Oid         reltoastrelid BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_class);
    bool        relhasindex BKI_DEFAULT(f);
    bool        relisshared BKI_DEFAULT(f);
    char        relpersistence BKI_DEFAULT(p);
    char        relkind BKI_DEFAULT(r);
    int16       relnatts BKI_DEFAULT(0);  
    int16       relchecks BKI_DEFAULT(0);
    bool        relhasrules BKI_DEFAULT(f);
    bool        relhastriggers BKI_DEFAULT(f);
    bool        relhassubclass BKI_DEFAULT(f);
    bool        relrowsecurity BKI_DEFAULT(f);
    bool        relforcerowsecurity BKI_DEFAULT(f);
    bool        relispopulated BKI_DEFAULT(t);
    char        relreplident BKI_DEFAULT(n);
    bool        relispartition BKI_DEFAULT(f);
    Oid         relrewrite BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_class);
    TransactionId relfrozenxid BKI_DEFAULT(3);  
    TransactionId relminmxid BKI_DEFAULT(1);
} FormData_pg_class;

/* Size of fixed part of pg_class tuples, not counting var-length fields */
#define CLASS_TUPLE_SIZE \
     (offsetof(FormData_pg_class,relminmxid) + sizeof(TransactionId))

/* ----------------
 *      Form_pg_class corresponds to a pointer to a tuple with
 *      the format of pg_class relation.
 * ----------------
 */
typedef FormData_pg_class *Form_pg_class;

DECLARE_UNIQUE_INDEX_PKEY(pg_class_oid_index, 2662, ClassOidIndexId, pg_class, btree(oid oid_ops));
DECLARE_UNIQUE_INDEX(pg_class_relname_nsp_index, 2663, ClassNameNspIndexId, pg_class, btree(relname name_ops, relnamespace oid_ops));
DECLARE_INDEX(pg_class_tblspc_relfilenode_index, 3455, ClassTblspcRelfilenodeIndexId, pg_class, btree(reltablespace oid_ops, relfilenode oid_ops));

MAKE_SYSCACHE(RELOID, pg_class_oid_index, 128);
MAKE_SYSCACHE(RELNAMENSP, pg_class_relname_nsp_index, 128);

表结构结构体FormData_XXX定义中使用 BKI 选项详细说明如下,其中表级别选项定义在 CATALOG 宏同一行描述表,例如BKI_BOOTSTRAP参数就指定了该表为数据库核心表,在 bootstrap 模式下会使其进入 heap_create_with_catalog 函数根据postgre.bki创建系统表;列级别选项则位于属性字段名称之后,用于指定属性列默认值等

类别 选项 作用 使用场景
表级别 BKI_BOOTSTRAP 标记为引导表,需在 initdb 第一阶段创建 核心系统表
BKI_SHARED_RELATION 声明为全局共享表 实例级系统表
BKI_ROWTYPE_OID 注册行类型 OID 所有包含数据的系统表
BKI_SCHEMA_MACRO 结束标记 定义换行
列级别 BKI_FORCE_NULL 强制初始值为 NULL 所有可为 NULL 的列
BKI_FORCE_NOT_NULL 强制初始值为非 NULL 所有非 NULL 列
BKI_DEFAULT 指定默认值 需要默认值的列
BKI_ARRAY_DEFAULT 数组类型默认值 数组类型列
BKI_LOOKUP 声明外键引用(非空) Oid/regproc 类型列
BKI_LOOKUP_OPT 声明外键引用(可空) 可为 NULL 的 Oid 列

在定义FormData_XXX表结构的基础上,头文件genbki.h中声明了系统表的 TOAST 表、索引、外键约束等的宏定义,为 genbki.pl 脚本提供元数据生成指导

宏定义 作用
DECLARE_TOAST 声明主表的 TOAST 表
DECLARE_TOAST_WITH_MACRO 声明 TOAST 表并生成 C 常量
DECLARE_INDEX 声明普通索引
DECLARE_UNIQUE_INDEX 声明唯一索引
DECLARE_UNIQUE_INDEX_PKEY 声明主键索引
DECLARE_OID_DEFINING_MACRO 定义 OID 常量宏
DECLARE_FOREIGN_KEY 声明非空外键
DECLARE_FOREIGN_KEY_OPT 声明可空外键
DECLARE_ARRAY_FOREIGN_KEY 声明数组元素外键
DECLARE_ARRAY_FOREIGN_KEY_OPT 声明可空数组外键
MAKE_SYSCACHE 声明系统缓存 syscache

pg_class 系统表是使用BKI_BOOTSTRAP参数指定的核心系统表,在初始化时还会根据src/include/catalog/pg_class.dat数据文件中定义的数据行将条目插入系统表中;pg_class 作为记录所有关系的系统表,boostrap 阶段创建的系统表也需要写入其中,这些表的 OID 等元数据信息要与定义一致

json 复制代码
[
{ oid => '1247',
  relname => 'pg_type', reltype => 'pg_type' },
{ oid => '1249',
  relname => 'pg_attribute', reltype => 'pg_attribute' },
{ oid => '1255',
  relname => 'pg_proc', reltype => 'pg_proc' },
{ oid => '1259',
  relname => 'pg_class', reltype => 'pg_class' },
]

根据.h头文件和.dat数据文件的定义,BKI 生成框架中的 Catalog.pmgenbki.pl脚本生成 pg_class 的 drive 头文件 pg_class_d.h和引导脚本postgres.bki中的 SQL 语句如下

作为 pg_class 的元数据定义头文件,pg_class_d.h 主要包含该系统表相关索引的 OID 、表结构属性列等相关内容的宏定义

c 复制代码
#ifndef PG_CLASS_D_H
#define PG_CLASS_D_H

/* Macros related to the structure of pg_class */

#define RelationRelationId 1259
#define RelationRelation_Rowtype_Id 83
#define ClassOidIndexId 2662
#define ClassNameNspIndexId 2663
#define ClassTblspcRelfilenodeIndexId 3455

#define Anum_pg_class_oid 1
#define Anum_pg_class_relname 2
#define Anum_pg_class_relnamespace 3
#define Anum_pg_class_reltype 4
#define Anum_pg_class_reloftype 5
#define Anum_pg_class_relowner 6
#define Anum_pg_class_relam 7
#define Anum_pg_class_relfilenode 8
#define Anum_pg_class_reltablespace 9
#define Anum_pg_class_relpages 10
#define Anum_pg_class_reltuples 11
#define Anum_pg_class_relallvisible 12
#define Anum_pg_class_relallfrozen 13
#define Anum_pg_class_reltoastrelid 14
#define Anum_pg_class_relhasindex 15
#define Anum_pg_class_relisshared 16
#define Anum_pg_class_relpersistence 17
#define Anum_pg_class_relkind 18
#define Anum_pg_class_relnatts 19
#define Anum_pg_class_relchecks 20
#define Anum_pg_class_relhasrules 21
#define Anum_pg_class_relhastriggers 22
#define Anum_pg_class_relhassubclass 23
#define Anum_pg_class_relrowsecurity 24
#define Anum_pg_class_relforcerowsecurity 25
#define Anum_pg_class_relispopulated 26
#define Anum_pg_class_relreplident 27
#define Anum_pg_class_relispartition 28
#define Anum_pg_class_relrewrite 29
#define Anum_pg_class_relfrozenxid 30
#define Anum_pg_class_relminmxid 31
#define Anum_pg_class_relacl 32
#define Anum_pg_class_reloptions 33
#define Anum_pg_class_relpartbound 34

#define Natts_pg_class 34

// ...
#endif                          /* PG_CLASS_D_H */

生成的引导脚本postgres.bki中对应的 pg_class 系统表创建和初始化数据语句如下

sql 复制代码
create pg_class 1259 bootstrap rowtype_oid 83
 (
 oid = oid ,
 relname = name ,
 relnamespace = oid ,
 reltype = oid ,
 reloftype = oid ,
 relowner = oid ,
 relam = oid ,
 relfilenode = oid ,
 reltablespace = oid ,
 relpages = int4 ,
 reltuples = float4 ,
 relallvisible = int4 ,
 relallfrozen = int4 ,
 reltoastrelid = oid ,
 relhasindex = bool ,
 relisshared = bool ,
 relpersistence = char ,
 relkind = char ,
 relnatts = int2 ,
 relchecks = int2 ,
 relhasrules = bool ,
 relhastriggers = bool ,
 relhassubclass = bool ,
 relrowsecurity = bool ,
 relforcerowsecurity = bool ,
 relispopulated = bool ,
 relreplident = char ,
 relispartition = bool ,
 relrewrite = oid ,
 relfrozenxid = xid ,
 relminmxid = xid ,
 relacl = _aclitem ,
 reloptions = _text ,
 relpartbound = pg_node_tree
 )
insert ( 1247 pg_type 11 71 0 10 2 0 0 0 -1 0 0 0 f f p r 32 0 f f f f f t n f 0 3 1 _null_ _null_ _null_ )
insert ( 1249 pg_attribute 11 75 0 10 2 0 0 0 -1 0 0 0 f f p r 25 0 f f f f f t n f 0 3 1 _null_ _null_ _null_ )
insert ( 1255 pg_proc 11 81 0 10 2 0 0 0 -1 0 0 0 f f p r 30 0 f f f f f t n f 0 3 1 _null_ _null_ _null_ )
insert ( 1259 pg_class 11 83 0 10 2 0 0 0 -1 0 0 0 f f p r 34 0 f f f f f t n f 0 3 1 _null_ _null_ _null_ )
close pg_class

2.2 新增系统表

理解了系统表的初始化流程后,新增一张系统表只需要三步:

  • src/include/catalog目录下定义系统表.h头文件

  • 可选 )在src/include/catalog目录下定义系统表.dat数据文件

  • src/backend/catalog/Makefile 文件中的 CATALOG_HEADERS.h加入编译

2.2.1 定义头文件

系统表的头文件定义包含一般两部分:

  • 定义表结构 FormData_pg_XXX 结构体

  • 使用 DECLARE_XXX为系统表声明TOSAT表、索引、外键约束等

首先,新建一个 pg_my_catalog.h 的头文件,定义一个包含 3 个属性列的系统表,其中is_valid属性默认值为t;然后,分别在nameoid列声明唯一键和主键

其中 catalog/genbki.h 头文件定义了引导脚本genbki.pl相关的宏定义;catalog/pg_my_catalog_d.h头文件为编译生成的 drive 头文件,包含该系统表相关的 OID 和属性列宏定义

c 复制代码
#ifndef pg_my_catalog_H
#define pg_my_catalog_H

#include "catalog/genbki.h"
#include "catalog/pg_my_catalog_d.h"    /* IWYU pragma: export */

/* ----------------
 *      pg_my_catalog definition.  cpp turns this into
 *      typedef struct FormData_pg_my_catalog
 * ----------------
 */
CATALOG(pg_my_catalog,9164,MyCatalogRelationId)
{
    Oid         oid;
    NameData    name;
    bool        is_valid BKI_DEFAULT(t);
} FormData_pg_my_catalog;

/* ----------------
 *      Form_pg_my_catalog corresponds to a pointer to a tuple with
 *      the format of pg_my_catalog relation.
 * ----------------
 */
typedef FormData_pg_my_catalog *FormData_pg_my_catalog;

DECLARE_UNIQUE_INDEX(pg_my_catalog_name_index, 9165, MyCatalogNameIndexId, pg_my_catalog, btree(name name_ops));
DECLARE_UNIQUE_INDEX_PKEY(pg_my_catalog_oid_index, 9166, MyCatalogOidIndexId, pg_my_catalog, btree(oid oid_ops));

#endif                          /* pg_my_catalog_H */

Reference: 67.2. System Catalog Initial Data

在定义系统表和声明索引等数据库对象时,需要为他们分配 OID,当前 PG 预留了 1-16383 范围内的 OID 给数据库初始化阶段需要的系统表等数据库对象,在正常数据库操作期间分配的 OID 被限制为 >= 16384

  • 1--9999 用于手动分配 ,可以使用 PG 提供的脚本src/include/catalog/unused_oids获取手动分配未使用 OID 范围,使用 duplicate_oids 脚本来检查是否存在 OID 冲突错误

  • 10000-11999用于在initdb期间引导脚本genbki.pl执行时进行自动分配

  • 12000-16383范围内的 OID 则用于genbki.pl执行完成之后的自动分配,核心系统目录初始化完成之后,OID 计数器被设置为FirstUnpinnedObjectId(12000),即 OID 小于 12000 是不可删除的固定对象(Pinned)

2.2.2 定义数据文件

新建一个 pg_my_catalog.dat 的数据文件,使用 <属性列名> => '<value>'的格式为自定义的 pg_my_catalog系统表的 oid, name 字段赋值指定初始化 tuple 数据

json 复制代码
[
{ oid => '9202', name => 'test_my_catalog'},
]

2.2.3 编译头文件

最后,将头文件和数据文件添加到编译流程中,在 src/backend/catalog/Makefile 文件中找到 CATALOG_HEADERSPOSTGRES_BKI_DATA选项,将 pg_my_catalog.h添加到CATALOG_HEADERS选项,将pg_my_catalog.dat添加到POSTGRES_BKI_DATA选项

bash 复制代码
CATALOG_HEADERS := \
    pg_proc.h \
    pg_type.h \
    ... \
    pg_my_catalog.h

POSTGRES_BKI_DATA = \
    pg_aggregate.dat \
    pg_am.dat \
    ... \
    pg_my_catalog.dat

编译过程中自动生成的pg_my_catalog_d.h元数据头文件如下,包含系统表的 Table、Index 等数据库对象的 OID 和属性列序号的宏定义

c 复制代码
#ifndef PG_MY_CATALOG_D_H
#define PG_MY_CATALOG_D_H

/* Macros related to the structure of pg_my_catalog */

#define MyCatalogRelationId 9164
#define MyCatalogNameIndexId 9165
#define MyCatalogOidIndexId 9166

#define Anum_pg_my_catalog_oid 1
#define Anum_pg_my_catalog_name 2
#define Anum_pg_my_catalog_is_valid 3

#define Natts_pg_my_catalog 3

/* Definitions copied from pg_my_catalog.h */

/* OID symbols for objects defined in pg_my_catalog.dat */

#endif                          /* PG_MY_CATALOG_D_H */

完成编译安装,并重新 initdb 初始化数据目录之后,启动数据库就可以查询到自定义系统表了,查询 pg_my_catalog表也可以看到.dat数据文件定义的初始数据

sql 复制代码
postgres=# \d+ pg_my_catalog
                                     Table "pg_catalog.pg_my_catalog"
  Column  |  Type   | Collation | Nullable | Default | Storage | Compression | Stats target | Description 
----------+---------+-----------+----------+---------+---------+-------------+--------------+-------------
 oid      | oid     |           | not null |         | plain   |             |              | 
 name     | name    |           | not null |         | plain   |             |              | 
 is_valid | boolean |           | not null |         | plain   |             |              | 
Indexes:
    "pg_my_catalog_oid_index" PRIMARY KEY, btree (oid)
    "pg_my_catalog_name_index" UNIQUE CONSTRAINT, btree (name)
Not-null constraints:
    "pg_my_catalog_oid_not_null" NOT NULL "oid"
Access method: heap

postgres=# select * from pg_my_catalog ;
 oid  |      name       | is_valid 
------+-----------------+----------
 9202 | test_my_catalog | f
(1 row)

postgres=# 

03 操作系统表


3.1 写操作

在内核中操作系统表即对系统表进行增删改查,PG 中系统表是 heap 表,所以内核提供了一套基于 heap 表操作函数进行封装的操作函数CatalogTupleXXXX;这些函数定义在src/backend/catalog/indexing.c中,用于操作的主要是Insert, Update, Delete相关的函数,在进行 Insert, Update 操作时需要带上索引状态信息,可以自定义索引状态的xxxWithInfo函数需要配合CatalogOpen/CloseIndex使用

函数名称 函数作用 封装的 heap 操作函数
CatalogOpenIndexes 为系统表准备好所有索引的插入状态 index_open
CatalogCloseIndexes 释放 CatalogIndexState 资源 index_close
CatalogTupleInsert 插入一个元组:开索引、插入元组、关索引 simple_heap_insert
CatalogTupleInsertWithInfo 插入元组的同时,手动传入索引状态 simple_heap_insert
CatalogTuplesMultiInsertWithInfo 一次插入多个元组,用 slots 数组 heap_multi_insert
CatalogTupleUpdate 更新指定元组:开索引、更新元组、关索引 simple_heap_update
CatalogTupleUpdateWithInfo 更新元组的同时,插入新的索引状态 simple_heap_update
CatalogTupleDelete 删除指定系统表元组 simple_heap_delete

3.1.1 插入 Insert

PG 中系统表都是 heap 表,所以插入过程和 heap 表的 tuple 插入一致,需要手动构建 tuple 的 values, nulls 数组,然后使用heap_from_tuple构建好 tuple 插入到表中;其中,values 是一个 Datum 数组,所以存放值的时候需要使用XXXXGetDatum数据类型转换函数将其他 C 语言数据类型转换为 Datum 之后再放入 ;相对的,从 Datum 取出 数据时需要使用 DatumGetXXXX 将 Datum 转换为其他数据类型再使用

c 复制代码
void
StoreSingleMyCatalog(const char *name)
{
    Datum       values[Natts_pg_my_catalog];
    bool        nulls[Natts_pg_my_catalog];
    HeapTuple   tuple;
    Relation    rel;
    Oid         myoid;
    NameData    myname;

    // 1. 根据 MyCatalogRelationId 打开系统表 
    rel = table_open(MyCatalogRelationId, RowExclusiveLock);

    /*
     * 2. 给插入 tuple 的 values 和 nulls 赋值,values 中的值要将 c 类型转换为 Datum
     *        Oid 通过 GetNewOidWithIndex 自动生成
     */
    myoid = GetNewOidWithIndex(rel, MyCatalogOidIndexId,
                                Anum_pg_my_catalog_oid);
    values[Anum_pg_my_catalog_oid- 1] = ObjectIdGetDatum(myoid);
    namestrcpy(&myname, name);
    values[Anum_pg_my_catalog_name - 1] = NameGetDatum(&myname);
    values[Anum_pg_my_catalog_is_valid - 1] = BoolGetDatum(false);

    memset(nulls, 0, sizeof(nulls));

    // 3. 使用 heap_form_tuple 将 values 构建成 tuple,然后调用 CatalogTupleInsert 插入到表中
    tuple = heap_form_tuple(RelationGetDescr(rel), values, nulls);
    CatalogTupleInsert(rel, tuple);

    // 清理操作
    heap_freetuple(tuple);
    table_close(rel, RowExclusiveLock);
}

3.1.2 更新 Update

系统表的更新操作分为三步:

  1. 找到 tuple:根据 Oid 打开系统表,并使用合适的索引,这里传入的是 oid 所以使用 MyCatalogOidIndexId 索引,根据 ScanKeyInit 函数初始化的扫描条件对系统表进行扫描

  2. 构建新值:如果找到了指定 tuple,则按更新需求构建新的 values, nulls,并通过和 nulls 相似的 replaces bool 类型数组指定要更新的属性列

  3. 执行更新:最后使用 heap_modify_tuple 函数使用新值更新 tuple,并执行更新

c 复制代码
void
ChangeMyCatalogInValid(Oid tupid)
{
    Relation    rel;
    bool        nulls[Natts_pg_subscription];
    bool        replaces[Natts_pg_subscription];
    Datum       values[Natts_pg_subscription];
    ScanKeyData skey[1];
    SysScanDesc sscan;
    HeapTuple   tuple;

    /* 1. 通过索引查询到指定 tuple */
    rel = table_open(MyCatalogRelationId, RowExclusiveLock);
    
    ScanKeyInit(&skey[0],
                Anum_pg_my_catalog_oid,
                BTEqualStrategyNumber, F_OIDEQ,
                ObjectIdGetDatum(table_id));
                
    sscan = systable_beginscan(rel, MyCatalogOidIndexId , true,
                               NULL, 1, skey);
                
    tuple = systable_getnext(sscan);

    if (!HeapTupleIsValid(tuple))
        elog(ERROR, "cache lookup failed for tuple %u", tupid);

    /* 2. 构建 tuple 需要修改的值 values */
    memset(values, 0, sizeof(values));
    memset(nulls, false, sizeof(nulls));
    memset(replaces, false, sizeof(replaces)); // 对应属性列是否需要更新

    values[Anum_pg_my_catalog_is_valid - 1] = BoolGetDatum(true);
    replaces[Anum_pg_my_catalog_is_valid - 1] = true;

    /* 3. 根据新值更新 tuple */
    tuple = heap_modify_tuple(tuple, RelationGetDescr(rel), values, nulls,
                              replaces);
    CatalogTupleUpdate(rel, &tuple->t_self, tup);
    heap_freetuple(tuple);

    systable_endscan(sscan);
    table_close(rel, RowExclusiveLock);
}

其中,ScanKeyInit 是用于初始化过滤元组的条件ScanKeyData 结构体的函数,这个结构体用于对表进行扫描时,指定过滤和定位元组的条件;通俗地讲,就是将一个查询条件 WHERE id = 123转换成一个可以被底层存储系统理解的内部格式

c 复制代码
void
ScanKeyInit(ScanKey entry,
            AttrNumber attributeNumber,
            StrategyNumber strategy,
            RegProcedure procedure,
            Datum argument)
{
    entry->sk_flags = 0;
    entry->sk_attno = attributeNumber;
    entry->sk_strategy = strategy;
    entry->sk_subtype = InvalidOid;
    entry->sk_collation = C_COLLATION_OID;
    entry->sk_argument = argument;
    fmgr_info(procedure, &entry->sk_func);
}

这个函数通过一系列参数精确控制着过滤条件行为,具体如下:

  1. ScanKey entry :输出参数,指向一个需要被初始化的 ScanKeyData 结构体的指针

  2. AttrNumber attributeNumber:一个 int16 类型的值,用于指定该扫描条件针对地具体属性列,列序号和 pg_attribute 表中一致

  3. StrategyNumber strategy:指定索引访问方法使用的策略号,定义在src/include/access/stratnum.h 头文件中,例如,常用的 B-Tree 索引中策略号有:

    • BTEqualStrategyNumber 值为 1 表示 =
    • BTLessStrategyNumber 值为 2 表示 <
    • BTLessEqualStrategyNumber 值为 3 表示 <=
    • BTGreaterEqualStrategyNumber 值为 4 表示 >=
    • BTGreaterStrategyNumber 值为 5 表示 >
  4. RegProcedure procedure:指定用于比较的函数操作符的 OID,决定了比较的语义,和要比较的值的类型对应;例如,对于整数相等比较,需要传递 F_INT4EQ(即 integer_eq 函数的 OID);对于 Oid 相等比较,则是 F_OIDEQ

  5. Datum argument:一个 Datum 类型的值,指定要比较的值,使用时需要使用XXXXGetDatum()系列函数将 C 变量转换为 Datum 类型

3.1.3 删除 Delete

删除操作是更新操作的简化版,少了构建新值和进行 tuple 更新的操作,只需要根据 ScanKeyInit 函数初始化的扫描条件对系统表进行扫描找到对应 tuple 之后,调用CatalogTupleDelete 删除该 tuple 即可

c 复制代码
void
RemoveMyCatalog(Oid tupid)
{
    Relation    rel;
    ScanKeyData skey[1];
    SysScanDesc sscan;
    HeapTuple   tuple;

    /* 1. 通过索引查询到指定 tuple */
    rel = table_open(MyCatalogRelationId, RowExclusiveLock);
    
    ScanKeyInit(&skey[0],
                Anum_pg_my_catalog_oid,
                BTEqualStrategyNumber, F_OIDEQ,
                ObjectIdGetDatum(table_id));
                
    sscan = systable_beginscan(rel, MyCatalogOidIndexId , true,
                               NULL, 1, skey);
                
    tuple = systable_getnext(sscan);

    if (!HeapTupleIsValid(tuple))
        elog(ERROR, "cache lookup failed for tuple %u", tupid);
        
    /* 2. 删除找到的 tuple */
    CatalogTupleDelete(rel, &tuple->t_self);
    heap_freetuple(tuple);

    systable_endscan(sscan);
    table_close(rel, RowExclusiveLock);
}

3.2 读操作

系统表的读操作,即对系统表进行扫描,在更新和删除操作中已经提到了相关使用方法,PG 提供了一组 systable_xxxx函数来对系统表进行扫描,最常用的组合systable_beginscan, systable_getnext, systable_endscan开启一个扫描、获取下一个匹配的元组、结束扫描;这种扫描方式可以指定扫描键来使用索引进行扫描

c 复制代码
bool
CheckMyCatalogValid(Oid tupid)
{
    Relation    rel;
    ScanKeyData skey[1];
    SysScanDesc sscan;
    HeapTuple   tuple;
    FormData_pg_my_catalog datForm;
    bool        result = false;

    /* 1. 通过索引查询到指定 tuple */
    rel = table_open(MyCatalogRelationId, RowExclusiveLock);
    
    ScanKeyInit(&skey[0],
                Anum_pg_my_catalog_oid,
                BTEqualStrategyNumber, F_OIDEQ,
                ObjectIdGetDatum(table_id));
                
    sscan = systable_beginscan(rel, MyCatalogOidIndexId , true,
                               NULL, 1, skey);
                
    tuple = systable_getnext(sscan);

    if (!HeapTupleIsValid(tuple))
        elog(ERROR, "cache lookup failed for tuple %u", tupid);
        
    /* 2. 获取 tuple 的指定列 */
    datForm = (FormData_pg_my_catalog) GETSTRUCT(tuple);
    result = datForm->is_valid;
    
    /* // 使用 heap_getattr 获取指定列值
    bool isnull;
    Datum is_valid = heap_getattr(tuple, Anum_pg_my_catalog_is_valid,
                                  RelationGetDescr(rel), &isnull);
    if (!isnull)
        result = DatumGetBool(is_valid);
    */
    
    heap_freetuple(tuple);

    systable_endscan(sscan);
    table_close(rel, RowExclusiveLock);
    
    return result;
}

GETSTRUCT是一种用于获取系统表属性列的常用方法,该方法根据 tuple 指针返回其数据偏移量;将该地址转换为对应的FormData_pg_XXXX即可获取到结构体指定位置对应的属性列数据;当然也可以直接使用 heap 表属性列获取函数heap_getattr函数获取指定列的数据

c 复制代码
typedef struct HeapTupleData
{
    uint32      t_len;          /* length of *t_data */
    ItemPointerData t_self;     /* SelfItemPointer */
    Oid         t_tableOid;     /* table the tuple came from */
    HeapTupleHeader t_data;     /* -> tuple header and data */
} HeapTupleData;

static inline void *
GETSTRUCT(const HeapTupleData *tuple)
{
    return ((char *) (tuple->t_data) + tuple->t_data->t_hoff);
}

PG 12 引入了 TableAM 后,提供了另一种系统表扫描方法table_beginscan_catalog, table_endscan,这种方式忽略任何索引,总是进行顺序扫描;通常用于读取整个表或大部分数据的场景,此时顺序扫描比随机 IO 的索引扫描更高效

c 复制代码
populate_typ_list(void)
{
    Relation    rel;
    TableScanDesc scan;
    HeapTuple   tup;
    int         count;

    rel = table_open(MyCatalogRelationId, RowExclusiveLock);
    scan = table_beginscan_catalog(rel, 0, NULL);
    
    while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
    {
        /* 获取 tuple 的指定列 */
        datForm = (FormData_pg_my_catalog) GETSTRUCT(tuple);
        result = datForm->is_valid;
        if (result)
            count++;
    }

    table_endscan(scan);
    table_close(rel, NoLock);
}

如果文章对你有帮助,欢迎 点赞收藏 👍 ⭐️ 💬,如果还能够 点击关注,那真的是对我最大的鼓励 🔥 🔥 🔥


参考资料

https://www.postgresql.org/docs/current/catalogs.html

Chapter 67. System Catalog Declarations and Initial Contents

https://blog.japinli.top/2019/08/postgresql-new-catalog/

https://zhuanlan.zhihu.com/p/623283855

相关推荐
学传打活5 天前
【边打字.边学昆仑正义文化】_进阶篇5 宇宙星系的运转(续3)
微信公众平台·1024程序员节·汉字·昆仑正义文化
学传打活10 天前
【边打字.边学昆仑正义文化】_进阶篇5 宇宙星系的运转(续1)
微信公众平台·1024程序员节·汉字·昆仑正义文化
fangzhanpeng16818 天前
快速掌握C#语言基础知识点(1.基础数据类型)
开发语言·c#·1024程序员节
无己心20 天前
面向多平台 AI 搜索引擎的适配层架构设计
前端·人工智能·搜索引擎·ai·1024程序员节
u0103055271 个月前
AI驱动的安卓UI自动生成功能实现
人工智能·1024程序员节
u0103055271 个月前
长株潭智能体赋能腾讯元器链接共享
1024程序员节
云贝贝贝1 个月前
OceanBase认证体系介绍与备考指南
运维·服务器·oceanbase·1024程序员节
u0103055271 个月前
Java图像处理实战指南
人工智能·1024程序员节
u0103055271 个月前
Java AWT鼠标事件全解析
人工智能·1024程序员节
u0103055271 个月前
Java实用类应用精讲
人工智能·1024程序员节