PostgreSQL内核
## 数据库集群的逻辑结构
数据库集群是一组数据库集合。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](https://github.com/postgres/postgres/blob/master/src/include/storage/bufpage.h)
[PostgreSQL catalogs](Chapter 51. System Catalogs)
[Bitmap scan](Bitmap Indexes - PostgreSQL wiki)
相关文章