元数据与系统表
系统表是整个 PostgreSQL 数据库存储体系中最重要的一部分数据,它们用来组织管理PostgreSQL 的数据空间。它们本质也是一个个表对象,相比于普通表来说,系统表存储的是元数据。
元数据(Metadata)可以理解为描述数据的数据,使数据更容易理解,查找,管理和使用,比如,用户创建的表有 create table t1
(c1 int, c2 text)
两种列类型,这一些类型 int, text
会被单独存放在 pg_type
的系统表中,同时 c1, c2
列名字则会被存放在 pg_attribute
的系统表中,并和 pg_type 形成关联。表名t1会被存在pg_class系统表中。OID
Oid 在 PostgreSQL 中被用来描述一个个数据表的逻辑对象,比如
Relation
, type
, attr
, namespace
等等,每创建一个对象都会为其分配一个属于自己的标识(Oid)。PG 也会通过 Oid 来在不同的数据表之间建立关联,也就是说有一些对象是全局唯一的(pg_class 表的oid)。但是因为 Oid 是 unsigned int
,所以当对象的数量超过42亿之后可能会有回卷,所以PG 对Oid的划分有一些自己的定义,比如预留16383 个Oid 作为全局唯一的对象标识,其他的都是给用户表使用,允许发生回卷。接下来看看 PostgreSQL 内部非常重要的一些系统表,以及它们之间的关系模型,从而更好的帮助我们理解创建的一个用户表是如何被组织管理的。
主要系统表
pg_class(表及与表类似结构的数据库对象信息)
用于存储表及与表类似结构的数据库对象信息,包含索引、序列、视图、复合数据类型、TOAST表等。每一个对象都在pg_class中表示为一个元组。
代码路径:
src/include/catalog/pg_class.h
// 因为过多,简单挑几个关键信息如下,pg_class 的唯一标识
CATALOG(pg_class,1259,RelationRelationId)
{
Oid oid; // 当前表对象在 pg_class 的唯一标识,pg_class会以oid 为主键建索引,方便查找
NameData relname; // relation 名字
Oid relnamespace; // 所处的 pg_namespace oid,用来和 pg_namespace系统表建立关联
Oid reltype; // 对象类型,用于和pg_type系统表建立关联
...
Oid relam; // am 类型,比如是heap or 其他的,也是和 pg_amthod 建立管理
...
Oid relfilenode; // 当前对象的物理文件名,pg 内部文件名都是以数字存在。
...
char relpersistence; // 该对象的存储类型, 'p' 表示永久,即基本持久化类型;
//'u' 表示 unlogged,不写wal.
// 't' 表示临时表,session 级别的生命周期
char relkind; // 该对象的类型,'r'=普通表,'i'=索引,'v'=视图, 't'=toast 大value, 'c'=符合类型 等
int16 relnatts; // 该对象的属性列的个数
...
}
可以看到通过 create type map as (string varchar, int_1 int); create table map_test (id int, value map); 创建的表在 pg_class 中存储的属性信息 有两个,一个是 类型
map
的属性信息, 一个是表 map_test
的属性信息。postgres=# create type map as (string varchar, int_1 int);
CREATE TYPE
postgres=# create table map_test (id int, value map);
CREATE TABLE
//复合类型 map 的属性信息
postgres=# select oid,relname,relnamespace,reltype,relam,relfilenode,relpersistence,relkind,relnatts from pg_class where relname='map';
oid | relname | relnamespace | reltype | relam | relfilenode | relpersistence | relkind | relnatts
-------+---------+--------------+---------+-------+-------------+----------------+---------+----------
16396 | map | 2200 | 16398 | 0 | 16396 | p | c | 2
(1 row)
//表map_test的属性信息
postgres=# select oid,relname,relnamespace,reltype,relam,relfilenode,relpersistence,relkind,relnatts from pg_class where relname='map_test';
oid | relname | relnamespace | reltype | relam | relfilenode | relpersistence | relkind | relnatts
-------+----------+--------------+---------+-------+-------------+----------------+---------+----------
16399 | map_test | 2200 | 16401 | 0 | 16399 | p | r | 2
(1 row)
pg_type(数据类型信息)
用于存储数据类型信息。基本数据类型和枚举类型由CREATE TYPE创建,域类型由CREATE DOMAIN创建,复合数据类型在表创建时自动创建。pg_type中每一个元组对应一个数据类型。比如上面的
create table map_test (id int, value map);
建表过程中用到的类型 int
以及 复合类型 map
都会被存储到 pg_type
中,而列名字 id
以及 value
则会被存储的pg_attribute
系统表中,这个后面会说。接下来看看pg_type 的定义
pg_type.h
,挑选几个简略定义如下:// pg_type的 固有对象 标识是1247
CATALOG(pg_type,1247,TypeRelationId)
{
Oid oid; // 类型oid
NameData typname; // 类型名字
...
int16 typlen; // 该类型的长度,对于变长类型则一直是-1,如果是-2则是以null 终止的c字符串。
...
char typtype; // 该类型的基础类型。 'b'=基本类型,'c'=复合类型, 'd'=域类型, 'e'=枚举类型等
}
比如对于我们前面通过
create type map as (string varchar, int_1 int);
创建的类型,可以从 pg_type中看到其信息如下postgres=# select oid,typname,typlen,typtype from pg_type where typname='map';
oid | typname | typlen | typtype
-------+---------+--------+---------
16398 | map | -1 | c
(1 row)
pg_attribute(表的属性信息)
用于存储表的属性信息。对于数据库中表的每个属性都有一个元组。在 pg_class中我们看到的是这个表对象的列的个数,但是具体每一个列 都是什么类型,名字是什么,长度是多少,是第几列等这样的列的描述信息则是会存储在 pg_attribute 系统表中。 其基本类型定义如下
pg_attribute.h
:CATALOG(pg_attribute,1249) BKI_BOOTSTRAP BKI_WITHOUT_OIDS BKI_ROWTYPE_OID(75) BKI_SCHEMA_MACRO
{
Oid attrelid; // 该列属于哪一个关系对象,关系对象的oid (一个数据库只能有一个关系对象的名字)
NameData attname; // 该列的名称
Oid atttypid; // 该列的类型, 指向 pg_type的一条类型
...
int16 attlen; // 该列的长度,同 pg_type中的 typlen,加速读取attr信息。
int16 attnum; // 该列的index,是 attrelid 的第几列。
...
}
比如我们查看 前面创建的
test_map
(create table map_test (
id
int,
value
map);
) 表的列描述信息如下:可以看到 pg_attribute 还为 map_test 默认分配了一些默认不可见的属性列,用作 extension 时查看更细粒度的tuple信息。 用户自己创建的两列
id
和 value
则是有自己的typeid信息,可以从 pg_type
中看到其定义。pg_namespace(命名空间)
用于存储命名空间,PostgreSQL的名字空间层次是:数据库.模式.表.属性。pg_namespace中每一个元组都对应一个名字空间。
代码路径:(src/backend/catalog/pg_namespace.h)
CATALOG(pg_namespace,2615)
{
NameData nspname;//命名空间
Oid nspowner;//命名空间所有者的oid
#ifdef CATALOG_VARLEN /* variable-length fields start here */
aclitem nspacl[1];
#endif
} FormData_pg_namespace;
查询命名空间,返回"pg_namespace"表的所有行和列,其中包含命名空间的详细信息:
//如命名空间的名称、所属数据库的OID(对象标识符)以及其他相关属性。
postgres=# select * from pg_namespace;
nspname | nspowner | nspacl
--------------------+----------+----------------------------
pg_toast | 10 |
pg_oracle | 10 |
pg_temp_1 | 10 |
pg_toast_temp_1 | 10 |
pg_catalog | 10 | {tbase=UC/tbase,=U/tbase}
public | 10 | {tbase=UC/tbase,=UC/tbase}
information_schema | 10 | {tbase=UC/tbase,=U/tbase}
(7 rows)
pg_tablespace(表空间)
-
用于存储表空间信息,将表放置在不同的表空间有助于实施磁盘文件布局。数据库在逻辑上分成多个存储单元,称作表空间。表空间用作把逻辑上相关的结构放在一起。数据库逻辑上是由一个或多个表空间组成。初始化的时候,会自动创建pg_default和pg_global两个表空间。
-
pg_tablespace在整个数据集簇里只有一份,也就是说同一个数据集簇内的所有数据库共享一个pg_tablespace表,而不是每个数据库都有自己的pg_tablespace表。pg_tablespace中每一个元组都对应一个表空间。
定义在pg_tablespace.h中:
CATALOG(pg_tablespace,1213) BKI_SHARED_RELATION
{
NameData spcname; /*表空间名字 */
Oid spcowner; /* 表空间的所有者的oid */
#ifdef CATALOG_VARLEN /* variable-length fields start here */
aclitem spcacl[1]; /* access permissions */
text spcoptions[1]; /* per-tablespace options */
#endif
} FormData_pg_tablespace;
查询表空间:
pg_database(数据库信息)
用于存储当前数据集簇中数据库的信息,是一个在整个集簇范围内共享的系统表。pg_database中每一个元组都对应集簇中的一个数据库。
代码路径:src/include/catalog/pg_database.h
postgres=# select * from pg_database;
datname | datdba | encoding | datcollate | datctype | datistemplate | datallowconn | datconnlimit | datlastsysoid | datfrozenxi
d | datminmxid | dattablespace | datacl
-----------+--------+----------+------------+------------+---------------+--------------+--------------+---------------+------------
--+------------+---------------+----------------------------
postgres | 10 | 6 | en_US.utf8 | en_US.utf8 | f | t | -1 | 13368 |
3 | 1 | 1663 |
template1 | 10 | 6 | en_US.utf8 | en_US.utf8 | t | t | -1 | 13368 |
3 | 1 | 1663 | {=c/tbase,tbase=CTc/tbase}
template0 | 10 | 6 | en_US.utf8 | en_US.utf8 | t | f | -1 | 13368 |
3 | 1 | 1663 | {=c/tbase,tbase=CTc/tbase}
(3 rows)
//数据库信息
postgres=# \l
List of databases
Name | Owner | Encoding | Collate | Ctype | Access privileges
-----------+-------+----------+------------+------------+-------------------
postgres | tbase | UTF8 | en_US.utf8 | en_US.utf8 |
template0 | tbase | UTF8 | en_US.utf8 | en_US.utf8 | =c/tbase +
| | | | | tbase=CTc/tbase
template1 | tbase | UTF8 | en_US.utf8 | en_US.utf8 | =c/tbase +
| | | | | tbase=CTc/tbase
(3 rows)
pg_index(索引)
用于存储索引的具体信息。
postgres=# select * from pg_index limit 5;
indexrelid | indrelid | indnatts | indisunique | indisprimary | indisexclusion | indimmediate | indisclustered | indisvalid | indcheck
xmin | indisready | indislive | indisreplident | indkey | indcollation | indclass | indoption | indexprs | indpred
------------+----------+----------+-------------+--------------+----------------+--------------+----------------+------------+---------
-----+------------+-----------+----------------+--------+--------------+-----------+-----------+----------+---------
2831 | 2830 | 2 | t | t | f | t | f | t | f
| t | t | f | 1 2 | 0 0 | 1981 1978 | 0 0 | |
2833 | 2832 | 2 | t | t | f | t | f | t | f
| t | t | f | 1 2 | 0 0 | 1981 1978 | 0 0 | |
2835 | 2834 | 2 | t | t | f | t | f | t | f
| t | t | f | 1 2 | 0 0 | 1981 1978 | 0 0 | |
2837 | 2836 | 2 | t | t | f | t | f | t | f
| t | t | f | 1 2 | 0 0 | 1981 1978 | 0 0 | |
2839 | 2838 | 2 | t | t | f | t | f | t | f
| t | t | f | 1 2 | 0 0 | 1981 1978 | 0 0 | |
(5 rows)
postgres=# select * from pg_class where oid =2831;
relname | relnamespace | reltype | reloftype | relowner | relam | relfilenode | reltablespace | relpages | reltuples | rel
allvisible | reltoastrelid | relhasindex | relisshared | relpersistence | relkind | relnatts | relchecks | relhasoids | relhaspkey | re
lhasrules | relhastriggers | relhassubclass | relrowsecurity | relforcerowsecurity | relispopulated | relreplident | relispartition | r
elhasextent | relpartkind | relparent | relfrozenxid | relminmxid | relacl | reloptions | relpartbound
---------------------+--------------+---------+-----------+----------+-------+-------------+---------------+----------+-----------+----
-----------+---------------+-------------+-------------+----------------+---------+----------+-----------+------------+------------+---
----------+----------------+----------------+----------------+---------------------+----------------+--------------+----------------+--
------------+-------------+-----------+--------------+------------+--------+------------+--------------
pg_toast_2604_index | 99 | 0 | 0 | 10 | 403 | 2831 | 0 | 1 | 0 |
0 | 0 | f | f | p | i | 2 | 0 | f | f | f
| f | f | f | f | t | n | f | f
| | 0 | 0 | 0 | | |
(1 row)
主要系统表:
-
系统表 pg_namespace 用于存储命名空间
-
pg_tablespace 存储表空间信息,将表放置在不同的表空间有助于实施磁盘文件布局。
-
pg_database中存放 了当前数据集簇中数据库的信息,它也是一个在整个集簇范围内共亭的系统表.
-
pg_c1ass 存储表及与表类似结构的数据库对象信息,包含索引、序列、视图、复合数据类型、 TOAST 表等。
-
pg_type存储数据类型信息
-
pg_attribute 存储表的属性信息,对于数据库中表的每个属性都有一个元组。
-
pg_index 存储索引的具体信息
关键系统表之间的相互依赖关系
pg_namespace表中的命名空间OID 与pg_class表中的命名空间OID相关联,用于确定表所属的命名空间。pg_class表中的表OID与pg_attribute表中的表OID相关联,用于确定每个表的列信息。pg_class表中的表OID与pg_index表中的表OID相关联,用于确定每个表的索引信息。
系统表初始化
在PostgreSQL数据库安装完后,需要先进行初始化数据库操作(initdb),生成模板数据库和相应的目录、文件信息,系统表即在此阶段生成。而用户数据库及其系统表都是从模板数据库进行复制生成的。
系统表的访问
由于系统表保存了数据库的所有元数据,所以系统运行时对系统表的访问是非常频繁的。为了 提高系统性能,在内存中建立了共享的系统表CACHE,使用 Hash 函数和 Hash 表提高查询效率。
PG 的 cache 体系 主要有三种:
-
syscache,缓存系统表数据
-
relcache,缓存 一个表关系relation 的完整数据(包括用户表的)
-
plancache,缓存planstmt,加速一个query对同一个planstmt 的访问
代码路径:src/backend/utils/cache/
主要介绍的是前两种:
SysCache(CatCache
)
SysCache介绍
代码路径:src/include/utils/catcache.h
-
缓存系统表数据,也叫
CatCache
catalog cache,catcache中存放最近访问过的系统表缓存。 -
从实现上看SysCache就是一个数组,数组的长度为预定义的系统表的个数。在PG 8.4.1中实现了54个系统表,因此SysCache数组具有54个元素。每个元素的数据结构为CatCache,该结构体内使用Hash来存储被缓存的系统表元组,每个系统表唯一地对应一个SysCache数组中的CatCache结构。
-
在Postgres进程初始化时(在InitProgres中),将会对SysCache进行初始化。SysCache的初始化实际上是填充SysCache数组中每个元素的CatCache结构的过程,主要任务是将査找系统表元组的关键字信息写入SysCache数组元素中。这样通过指定的关键字可以快速定位到系统表元组的存储位置。
-
CatCache的数据结构如下:
-
在SysCache.c文件中已经将所有系统表的CatCache信息存储在一个名为cacheinfo的静态数组 中,每个系统表的CatCache信息用一个数组元素来描述,其数据类型为cachedesc:
struct cachedesc
{
Oid reloid; /* OID of the relation being cached */
Oid indoid; /* OID of index relation for this cache */
int nkeys; /* # of keys needed for cache lookup */
int key[4]; /* attribute numbers of key attrs */
int nbuckets; /* number of hash buckets for this cache */
};
为了便于查找, SysCache 中的 CatCache 通过其 cc_next 字段构成了一个单向链表,其头部用全
局变量CacheHdr 记录(数据结构为catcacheheader):
typedef struct catcacheheader
{
slist_head ch_caches; //指向 CatCache 链表的头部
int ch_ntup; //所有CatCache中缓存元组的总数
} CatCacheHeader;
SysCache初始化
-
第一阶段:PG 在初始化 backend进程时 会通过
InitPostgres
-->InitCatalogCache
完成对SysCache
的初始化,并建立由CacheHdr记录的CatCache链表, 这里SysCache
是一个CatCache
结构的数组。InitCatalogCache函数中对SysCache的初始化主要分为以下几个步骤:-
根据cacheinfo为SysCache数组分配空间,这里将SysCache的长度设置为和cacheinfo数组相同。
-
循环调用InitCatcache函数根据cacheinfo中的每一个元素生成CatCache结构并放入SysCache数组的对应位置中。InitCatcache每调用一次将处理一个cachedesc结构。该函数根据cachedesc中要求的Hash桶的数量 为即将建立的CatCache结构分配内存,并根据cachedesc结构中的信息填充CatCache的各个字段。 最后将生成的CatCache链接在CacheHdr所指向的链表的头部。
-
-
第二阶段:调用函数RelationCacheInitializePhase2(负责RelCache的初始化)进行第二阶段初始化,该函数调用InitCatcachePhase2。InitCatcachePhase2将依次完善SysCache数组中的CatCache结构,主要是根据对应的系统表填充CatCache结构中的元组描述符(cc_tupledesc)、系统表名(cc_relname)、查找关键字的相关字段(cc_hashfunc、cc_isname、cc_skey)等。
-
SysCache数组初始化后,CatCache内是没有任何元组的,但是随着系统运行,对于系统表元组的访问,其中的系统表元组也会逐渐增多。
RelCache
RelCache 介绍
-
RelCache 中包含所有最近访问过的表的模式信息(包含系统表的信息) 。RelCache 的管理比 SysCache 要简单许多,原因在于大多数时候 RelCache 中存储的 RelationData 的结构是不变的,因此 PG仅用一个 Hash 表管理RelCache 。对 RelCache 的查找、 插入、删除、修改等操作也非常简单。当需要打开一个表时,首先在 RelCache 中寻找该表的 RelationData 结构,如果没有找到,则创建该结构并加入到 RelCache 中。
-
RelCache 中存放的不是元组,而是 RelationData 数据结构:
RelCache初始化
与SysCache 的初始化类似, RelCache 的初始化同样也在 lnitPostgres 函数中进行,同样分为两个阶段: RelationCachelnitialize 和RelationCacheInitializePhase2。
-
第一阶段:InitPostgres会调用函数RelationCachelnitialize对ReiCache进行第一阶段初始化,该函数将为该进程创建一个Hash表,其Hash键为表的OID,并设置Hash函数为oid_hash。Hash表的创建在函数hash_create中实现,该函数将创建一个标准Hash表结构体HTAB。
-
第二阶段:在完成了 Hash表的创建后,InitPostgres将调用RelationCachelnitializePhase进人第二阶段的初始化。该函数将必要的系统表和系统表索引的模式信息加人到RelCache中,这个过程通过函数RelationCacheInitializePhase2 来实现。这个阶段会确保 pg_class、pg_attribute、pg_proc、pg_type 四个系统表及相关索引的模式信息被加入到RelCache。在PostgreSQL中,使用一个文件pgJntemaLinit来记录系统表RelationData结构体,若该文件存在且未损坏,则将其内容直接读人RelCache中。否则,分别建立 pg_class、pg__atlribute、pg_proc、pg_type 及其索引的 RelationData 结构,加入到 RelCache上的Hash表中,并重写pg_internal.init文件。
-
当RelCache初始化完成后,我们就可以使用它来査找表的模式信息。RelCache的主要操作包括:
RelCache的主要操作
插人新打开的表
当打开新的表时,要把它的RelationData加入到RelCache中。该操作通过宏RelationCachelnsert来实现:首先,根据关系表OID在Hash表中找到对应的位置,调用函数hash_search,指定査询模式为HASH_ENTER,该模式下若发现OID对应的Hash桶已存在,则返回其指针;否则创建一个新的空的hash桶,返回其指针。然后将返回的指针强制转换为RelIdCacheEnt(数据结构:relidcacheent),然后把打开表的RelationData陚值给reldesc字段。
typedef struct relidcacheent
{
Oid reloid;
Relation reldesc;
} RelIdCacheEnt;
査找Hash表
査找Hash表通过定义宏RelationldCacheLookup (ID,RELATION)来实现,调用函数hash_search,指定査询模式为HASH_FIND,若找到ID对应的RelldCacheEnt,则将其iddesc字段的值賦值给 RELATION;否则,设置RELATION为NULL。
从Hash表中删除元素
从Hash表中删除元素通过定义宏RelationCacheDelete(RELATION)来实现,调用函数hash_search,指定査询模式为HASH_REVOKE,在该模式下,若找到对应的Hash桶,则将其删除;否则返回NULL。
Hash表实际上扮演了 RelCache索引的角色,所有对于RelCache的査找都是通过Hash表辅助进行的。