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

相关推荐
lpfasd1233 小时前
第九章-Tomcat源码阅读与扩展
1024程序员节
野生技术架构师3 小时前
为什么 idea 建议去掉 StringBuilder,使用“+”拼接字符串
1024程序员节
爱学习的大牛1233 小时前
使用C++开发Android .so库的优势与实践指南
android·.so·1024程序员节
bug攻城狮4 小时前
SaaS多租户架构实践:字段隔离方案(共享数据库+共享Schema)
mysql·架构·mybatis·springboot·1024程序员节
brave_zhao4 小时前
达梦数据库,子查询作为删除条件的sql案例,使用了mybatis批量删除
1024程序员节
爬山算法4 小时前
Redis(80)如何解决Redis的缓存穿透问题?
1024程序员节
007php0074 小时前
京东面试题解析:同步方法、线程池、Spring、Dubbo、消息队列、Redis等
开发语言·后端·百度·面试·职场和发展·架构·1024程序员节
小丁爱养花4 小时前
Redis 内部编码/单线程模型/string
数据库·redis·缓存·1024程序员节
小二_沏杯二锅头4 小时前
CentOS7.9部署Mysql8(二进制方式)
1024程序员节