
本文介绍 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_type,pg_depend配合pg_rewrite追踪视图对基表的依赖,pg_inherits管理表继承关系等 -
访问权限控制 :角色信息
pg_authid,默认权限规则pg_default_acl等 -
统计信息存储:
pg_statistic和pg_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.pl 和 src/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.pm和genbki.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;然后,分别在name和oid列声明唯一键和主键
其中 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_HEADERS和POSTGRES_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
系统表的更新操作分为三步:
-
找到 tuple:根据 Oid 打开系统表,并使用合适的索引,这里传入的是 oid 所以使用
MyCatalogOidIndexId索引,根据ScanKeyInit函数初始化的扫描条件对系统表进行扫描 -
构建新值:如果找到了指定 tuple,则按更新需求构建新的
values, nulls,并通过和nulls相似的replacesbool 类型数组指定要更新的属性列 -
执行更新:最后使用
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);
}
这个函数通过一系列参数精确控制着过滤条件行为,具体如下:
-
ScanKey entry:输出参数,指向一个需要被初始化的ScanKeyData结构体的指针 -
AttrNumber attributeNumber:一个int16类型的值,用于指定该扫描条件针对地具体属性列,列序号和pg_attribute表中一致 -
StrategyNumber strategy:指定索引访问方法使用的策略号,定义在src/include/access/stratnum.h头文件中,例如,常用的 B-Tree 索引中策略号有:BTEqualStrategyNumber值为1表示=BTLessStrategyNumber值为2表示<BTLessEqualStrategyNumber值为3表示<=BTGreaterEqualStrategyNumber值为4表示>=BTGreaterStrategyNumber值为5表示>
-
RegProcedure procedure:指定用于比较的函数操作符的 OID,决定了比较的语义,和要比较的值的类型对应;例如,对于整数相等比较,需要传递F_INT4EQ(即integer_eq函数的 OID);对于 Oid 相等比较,则是F_OIDEQ -
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