一、PostgreSQL内核

497 阅读6分钟

数据库集群的逻辑结构

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

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

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

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下子目录

传送门

目录名 用途
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来命名目录。

ls base/
1     12629 12630 16384 16399 16460 16654 24577 24578 24693 24694

数据表和索引

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

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。如下所示

➜  ls base/12630/24767
base/12630/24767

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

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),旧的目录会暂时先保留,过一会儿自动删除。

➜  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的文件路径

postgres=# SELECT pg_relation_filepath('newtb');
 pg_relation_filepath 
----------------------
 base/12630/24770
(1 row)

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

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下,通过命令创建表空间,创建时,需保证目录是已存在的。

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

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

➜  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'

例子如下

➜  pg11 ll pg_tblspc/24772/               
total 0
drwx------  2 yongsean  staff    64B Mar 22 00:00 PG_11_201809051

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

postgres=# CREATE DATABASE mytbsdb TABLESPACE mytbs;
CREATE DATABASE
➜  mytbs ll PG_11_201809051 
total 0
drwx------  296 yongsean  staff   9.3K Mar 22 00:08 24773

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

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)
➜  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。

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

相关链接

PageHeaderData

PostgreSQL catalogs

Bitmap scan

#标签 #PostgreSQL