PostgreSQL内核

2020-06-17 00:00:00 数据 数据库 文件 目录 存放

## 数据库集群的逻辑结构

数据库集群是一组数据库集合。host:port是一个数据库集群,但集群下有许多子库。

数据库集群的逻辑结构如下所示。数据库是数据库集群下的子集。在关系型数据库说法中,数据库对象是一个用来存储或引用数据的数据结构。数据表是一个例子,它有像索引、序列、试图、函数等。在Pg中,数据库他们还有自己的数据库对象,它们之间用逻辑结构做隔离。所有的数据库对象,像数据表、索引等都只属于各自的数据库。


数据库集群,包括多个数据库对象。每个数据库对象分别拥有:数据表、索引、视图、函数、序列、存储过程等。


在Pg中,所有数据库对象内部有各自的Object identifiers(OIDS)进行管理,类型是4字节无符号整型(unsigned int32)。数据库对象和其OIDs根据不同的类型对象存放至合适的system catalogs中。举个例子,数据库的OIDs是存放至pg_database,数据表的OIDs存放至pg_class。


```sql

postgres=# SELECT datname, oid FROM pg_database WHERE datname = 'postgres'; datname | oid

----------+-------

postgres | 12630

postgres=# SELECT relname, oid FROM pg_class WHERE relname='abc';

relname | oid

---------+-------

abc | 16553

```


## 数据库集群的物理结构

数据库集群本质上就是一个目录,这目录包含了一些子目录和文件。执行initdb命令初始化新的数据库集群,将会在指定路径创建目录。可以设置环境变量$PGDATA为刚创建的目录路径(不是必须的)。


### $PGDATA下子目录

> [ ](68.1. Database File Layout)


| 目录名 | 用途 |

| ------------- | ------------------------------------ |

| base/ | 存放每个数据子库目录 |

| global/ | 存放的文件用于存储全局的系统表信息和全局控制信息 |

| pg_commit_ts/ | 存放事物提交的时间戳数据 |

| pg_dynshmem/ | 动态共享内存 |

| pg_logical/ | 逻辑解码的状态值 |

| pg_multixact/ | 多事物的状态值(行锁) |

| pg_notify/ | 存放LISTEN/NOTIFY的数据 |

| pg_replslot/ | 存放replication slot数据 |

| pg_serial/ | 存放序列化事务提交的信息 |

| pg_snapshots/ | 存放快照文件,pg_export_snapshot函数创建快照信息文件 |

| pg_stat/ | 静态系统的持久化文件 |

| pg_stat_tmp/ | 静态系统的临时文件 |

| pg_subtrans/ | 存放子事务的数据 |

| pg_tblspc/ | 存放表空间的软连接 |

| pg_twophase/ | 存放2阶段提交中准备事务阶段的数据 |

| pg_wal/ | 存放wal文件 |

| pg_xact/ | 存放事物提交的数据;在pg10之前,目录名叫pg_clog |


### $PGDATA下的文件


| 文件名 | 用途 |

| --------------- | ------------ |

| PG_VERSION | pg版本号文件 |

| pg_hba.conf | pg的客户端权限控制文件 |

| pg_ident.conf | pg用户名映射文件 |

| postgresql.conf | pg主配置文件 |

| postmaster.opts | 记录后启动pg的命令 |


### 数据库base目录结构

所有的数据库目录都在$PGDATA/base/目录下,以它们的OIDs来命名目录。

```shell

ls base/

1 12629 12630 16384 16399 16460 16654 24577 24578 24693 24694

```


### 数据表和索引

数据表文件、索引文件,它们在其数据库目录下,多只能存放1GB的内容。表和索引作为数据库内部有各自的OIDs,并且它们的数据文件由relfilenode这个字段存储标记着。relfilenode这个字段是在pg_class表,是表或索引的值,relfilenode与OIDs并不是一直相同的。以数据表举例,在创建了新数据表后,这2个值是相等的。

```sql

postgres=# create table newtb(a int);

CREATE TABLE

postgres=# SELECT relname, oid, relfilenode FROM pg_class WHERE relname = 'newtb';

relname | oid | relfilenode

---------+-------+-------------

newtb | 24767 | 24767

```


在物理路径上,在$PGDATA/base/12630/24767。如下所示

```shell

➜ ls base/12630/24767

base/12630/24767

```


relfilenode在某些情况下会发生改变,比如进行TRUNCATE, REINDEX, CLUSTER操作后。如下所示,对newtb进行TRUNCATE操作后,其relfilenode值已发生改变,但oid没变化。

```sql

postgres=# SELECT relname, oid, relfilenode FROM pg_class WHERE relname = 'newtb';

relname | oid | relfilenode

---------+-------+-------------

newtb | 24767 | 24767

(1 row)


postgres=# truncate table newtb;

TRUNCATE TABLE

postgres=# SELECT relname, oid, relfilenode FROM pg_class WHERE relname = 'newtb';

relname | oid | relfilenode

---------+-------+-------------

newtb | 24767 | 24770

(1 row)

```


在文件目录中,可以查到新的relfilenode已经存在了($PGDATA/base/12630/24770),旧的目录会暂时先保留,过一会儿自动删除。

```shell

➜ ls base/12630/24767

base/12630/24767

➜ ls base/12630/24770

base/12630/24770

➜ ls base/12630/24767

ls: base/12630/24767: No such file or directory

```


通过pg内置函数:pg_relation_filepath,可以查询指定relation的文件路径

```sql

postgres=# SELECT pg_relation_filepath('newtb');

pg_relation_filepath

----------------------

base/12630/24770

(1 row)

```


当文件大小超过1G,Pg会自动创建一个新的文件,文件名是{relfilenode}.1格式,{relfilenode}.1这文件也超过1G,则会创建{relfilenode}.2文件。如下所示

```shell

bash-4.2$ ls -alh base/16384/104249*

-rw-------. 1 postgres postgres 1.0G 3月 18 00:03 base/16384/104249

-rw-------. 1 postgres postgres 1.0G 2月 20 19:14 base/16384/104249.1

-rw-------. 1 postgres postgres 843M 3月 21 01:12 base/16384/104249.2

-rw-------. 1 postgres postgres 744K 3月 21 01:08 base/16384/104249_fsm

-rw-------. 1 postgres postgres 88K 3月 17 19:46 base/16384/104249_vm

```


```

1G这个大小是在configure时设置的,使用参数--with-segsize 来调整

```



#### 空闲空间映射表(_fsm)

文件名以_fsm结尾,对应的是Free space map文件。用map来标识哪些block是空闲的,用一个Byte来标识block。对于有N个字节的Block,它在_fsm文件中第blknum个字节中记录的值是(31+N)/32。通过这种方式标识一个block空闲字节数。FSM是一个三层树形结构,在需要用到它的时候才自动产生。

#### 可见性映射表文件(_vm)

文件名以_vm结尾,对应的是visibility map。在Pg做多版本并发控制时通过元组上标识”已“来实现删除或更新,后通过VACUUM功能来清理数据,回收空间。在做VACUUM时使用VM快速查找包含元组Block。VM是简单的Bitmap,一个bit对应一个Block。索引有单独的FSM,但没有VM。


## 表空间(TableSpace)

表空间是独立于$PGDATA目录下的数据存放区,但通过软连接的方式指向到 $PGDATA/pg_tblspc子目录下。

在PSQL下,通过命令创建表空间,创建时,需保证目录是已存在的。

```sql

CREATE TABLESPACE mytbs LOCATION '/Users/yongsean/mytbs';

```


表空间创建好后,会在$PGDATA/pg_tblspc下产生个软连接

```shell

➜ pg11 ll pg_tblspc

total 0

lrwx------ 1 yongsean staff 21B Mar 22 00:00 24772 -> /Users/yongsean/mytbs

```

在刚创建的表空间目录下,会有一个新目录,其命名方式是以如下结构组成

```

PG _ 'Major version' _ 'Catalogue version number'

```

例子如下

```shell

➜ pg11 ll pg_tblspc/24772/

total 0

drwx------ 2 yongsean staff 64B Mar 22 00:00 PG_11_201809051

```


在表空间mytbs下,可以创建新的数据库


```sql

postgres=# CREATE DATABASE mytbsdb TABLESPACE mytbs;

CREATE DATABASE

```

```shell

➜ mytbs ll PG_11_201809051

total 0

drwx------ 296 yongsean staff 9.3K Mar 22 00:08 24773

```


也可以在默认表空间的DB下创建其他表空间的数据表

```sql

postgres=# CREATE TABLE mytbstb (a int) TABLESPACE mytbs;

CREATE TABLE

postgres=# \d+ mytbstb

Table "public.mytbstb"

Column | Type | Collation | Nullable | Default | Storage | Stats target | Description

--------+---------+-----------+----------+---------+---------+--------------+-------------

a | integer | | | | plain | |

Tablespace: "mytbs"

postgres=# SELECT datname, oid FROM pg_database WHERE datname = 'postgres';

datname | oid

----------+-------

postgres | 12630

(1 row)

postgres=# SELECT pg_relation_filepath('mytbstb');

pg_relation_filepath

---------------------------------------------

pg_tblspc/24772/PG_11_201809051/12630/24774

(1 row)

```

```shell

➜ mytbs ll PG_11_201809051/

total 0

drwx------ 3 yongsean staff 96B Mar 22 00:09 12630

drwx------ 296 yongsean staff 9.3K Mar 22 00:08 24773

➜ mytbs ll PG_11_201809051/12630

total 0

-rw------- 1 yongsean staff 0B Mar 22 00:09 24774

```


可以发现,目录12630/是数据库postgres的oid,而24774/是mytbstb的relfilenode。12630/在表空间目录PG_11_201809051/下成为了子目录。


## 堆表的内部结构

在数据文件内部,以固定长度的“页(或称为块)”的单位分开,页的默认大小是8K。每个文件内的页的序号都从0开始,或称为“块数(block number)”。如果文件被填满,pg会新增空“页”到文件末尾,并且文件体积会变大。


在数据文件内部,以固定长度的“页(块)”分割,“页”默认大小是8K。每个文件内页的序号从0开始(或称块数)。文件被填满,pg会在文件末尾增加新的空“页”,这时文件的体积也会相应增大。

页由3个部分组成

- 元组堆

- 行指针(成员指针)

- 头部数据


### 读取元组堆(Heap Tuples)

有两种常见的扫描方式

1. 序列扫描:没有任何索引,一行行扫,Sequential scan

2. BTree索引扫描:BTree index scan


##### 序列扫描(Sequential scan)

所有页中的所有数据都会被扫到,直到找到想要的值


##### BTree扫描

索引里包含了TID,TID由Block和Offset组成。根据TID定位到指定Page,根据Offset找到Tuple。


tid在PG查询里,是一个隐含字段:ctid。

```sql

postgres=# select * from tt200 where ctid = '(0,1)';

a

---

5

(1 row)

```



### 相关链接

[PageHeaderData](github.com/postgres/pos)


[PostgreSQL catalogs](Chapter 51. System Catalogs)


[Bitmap scan](Bitmap Indexes - PostgreSQL wiki)

相关文章