1 逻辑组织结构
如果阅读过手册一定听过postgresql cluster的概念,第一次听到这个概念可能都会有一些困惑。cluster在安装数据库时,由initdb工具生成,initdb后产生的pgdata文件夹可以理解为cluster的物理存储结构。数据库启动、停止时pg_ctl -D参数指定的文件夹即cluster文件夹,所以一个PG Server可以运行在一个PG Cluster上。
数据库对象这个概念在DBMS中的定义:
any defined object in a database that is used to store or reference data
在PG中数据库对象包括例如:堆表、索引、序列、函数等等
下图可以看到Cluster中可以创建多个数据库,每一个数据库中包含了表等其他数据库对象。在GP中schema是一个逻辑上的隔离概念,在实际存储中只是使用schema name对table name等做了区分。
注意:所有的数据库对象都会唯一的对应某一个数据库,这种隔离是逻辑上的,所以一个数据库的负载一定会影响到其他数据库。具体可以看后面的进程结构介绍。
2 物理组织结构
2.1 文件结构
现在来初始化一个cluster,使用initdb的指定,指定生成路径。
代码语言:javascript复制# initdb -D /home/mingjie.gmj/databases/data/pgdata8412
The files belonging to this database system will be owned by user "mingjie.gmj".
This user must also own the server process.
The database cluster will be initialized with locale "en_US.utf8".
The default database encoding has accordingly been set to "UTF8".
The default text search configuration will be set to "english".
Data page checksums are disabled.
creating directory /home/mingjie.gmj/databases/data/pgdata8412 ... ok
creating subdirectories ... ok
selecting default max_connections ... 100
selecting default shared_buffers ... 128MB
selecting dynamic shared memory implementation ... posix
creating configuration files ... ok
running bootstrap script ... ok
performing post-bootstrap initialization ... ok
syncing data to disk ... ok
WARNING: enabling "trust" authentication for local connections
You can change this by editing pg_hba.conf or using the option -A, or
--auth-local and --auth-host, the next time you run initdb.
Success. You can now start the database server using:
pg_ctl -D /home/mingjie.gmj/databases/data/pgdata8412 -l logfile start
指定后tree观察PGDATA文件目录结构
代码语言:javascript复制.
|-- base
| |-- 1
| | |-- 112
| | |-- 113
| | |-- 1247
| | |-- 1247_fsm
| | |-- 1247_vm
.........
| | |-- 828
| | |-- pg_filenode.map
| | `-- PG_VERSION
| |-- 13157
| | |-- 112
| | |-- 113
| | |-- 1247
| | |-- 1247_fsm
| | |-- 1247_vm
.........
| | |-- 827
| | |-- 828
| | |-- pg_filenode.map
| | `-- PG_VERSION
| `-- 13158
| |-- 112
| |-- 113
| |-- 1247
| |-- 1247_fsm
| |-- 1247_vm
.........
| |-- 827
| |-- 828
| |-- pg_filenode.map
| `-- PG_VERSION
|-- global
| |-- 1136
| |-- 1136_fsm
| |-- 1136_vm
.........
| |-- 6114
| |-- 6115
| |-- pg_control
| |-- pg_filenode.map
| `-- pg_internal.init
|-- pg_commit_ts
|-- pg_dynshmem
|-- pg_hba.conf
|-- pg_ident.conf
|-- pg_logical
| |-- mappings
| |-- replorigin_checkpoint
| `-- snapshots
|-- pg_multixact
| |-- members
| | `-- 0000
| `-- offsets
| `-- 0000
|-- pg_notify
| `-- 0000
|-- pg_replslot
|-- pg_serial
|-- pg_snapshots
|-- pg_stat
|-- pg_stat_tmp
|-- pg_subtrans
| `-- 0000
|-- pg_tblspc
|-- pg_twophase
|-- PG_VERSION
|-- pg_wal
| |-- 000000010000000000000001
| `-- archive_status
|-- pg_xact
| `-- 0000
|-- postgresql.auto.conf
`-- postgresql.conf
上述文件结构的根目录即initdb生成的PGDATA文件夹,对应一个cluster的物理存储结构(BASE文件夹内部见下一节)
项 | 描述 |
---|---|
PG_VERSION | 一个包含PostgreSQL主版本号的文件 |
base | 包含每个数据库对应的子目录 |
current_logfiles | 记录日志记录收集器当前写入的日志文件 |
global | 包含集簇范围的表的子目录,比如pg_database |
pg_commit_ts | 包含事务提交时间戳数据的子目录 |
pg_dynshmem | 包含被动态共享内存子系统所使用的文件的子目录 |
pg_logical | 包含用于逻辑复制的状态数据的子目录 |
pg_multixact | 包含多事务(multi-transaction)状态数据的子目录(用于共享的行锁) |
pg_notify | 包含LISTEN/NOTIFY状态数据的子目录 |
pg_replslot | 包含复制槽数据的子目录 |
pg_serial | 包含已提交的可序列化事务信息的子目录 |
pg_snapshots | 包含导出的快照的子目录 |
pg_stat | 包含用于统计子系统的永久文件的子目录 |
pg_stat_tmp | 包含用于统计信息子系统的临时文件的子目录 |
pg_subtrans | 包含子事务状态数据的子目录 |
pg_tblspc | 包含指向表空间的符号链接的子目录 |
pg_twophase | 包含用于预备事务状态文件的子目录 |
pg_wal | 包含 WAL (预写日志)文件的子目录 |
pg_xact | 包含事务提交状态数据的子目录 |
postgresql.auto.conf | 一个用于存储由ALTER SYSTEM 设置的配置参数的文件 |
postmaster.opts | 一个记录服务器最后一次启动时使用的命令行参数的文件 |
postmaster.pid | 一个锁文件,记录着当前的 postmaster 进程ID(PID)、集簇数据目录路径、postmaster启动时间戳、端口号、Unix域套接字目录路径(Windows上为空)、第一个可用的listen_address(IP地址或者*,或者为空表示不在TCP上监听)以及共享内存段ID(服务器关闭后该文件不存在) |
2.2 普通表文件组织结构
对于每个数据库,在PGDATA/base里都有一个子目录对应, 子目录的名字为该数据库在pg_database里的 OID。
代码语言:javascript复制postgres=# select oid, datname, dattablespace from pg_database ;
oid | datname | dattablespace
------- ----------- ---------------
13158 | postgres | 1663
1 | template1 | 1663
13157 | template0 | 1663``
~/databases/data/pgdata8412/base][12]$ ll
total 20
drwx------ 2 mingjie.gmj users 4096 Apr 22 20:17 1
drwx------ 2 mingjie.gmj users 4096 Apr 22 20:17 13157
drwx------ 2 mingjie.gmj users 12288 Apr 22 20:17 13158
下面我们来创建一张表,我们可以通过两种方式找到表文件:
代码语言:javascript复制postgres=# create table tbl1 (id int, info text);
CREATE TABLE
postgres=# select relname, relfilenode from pg_class where relname='tbl1';
relname | relfilenode
--------- -------------
tbl1 | 16384
postgres=# SELECT pg_relation_filepath('tbl1');
pg_relation_filepath
----------------------
base/13158/16384
(1 row)
查询pg_class.relfilenode得到tbl1表的relfilenode=16384,对于普通表文件以表或索引的filenode号命名。
表在postgres库中创建,我们进入base/13158目录可以看到表文件:
代码语言:javascript复制ls -al $PGDATA/base/13158/16384
-rw------- 1 mingjie.gmj users 8192 Apr 22 20:47 16384
某些数据库操作会重新分配表文件,例如truncate
(这也是truncate比delete快的原因)。
postgres=# select relname, relfilenode from pg_class where relname='tbl1';
relname | relfilenode
--------- -------------
tbl1 | 16384
(1 row)
postgres=# truncate tbl1;
TRUNCATE TABLE
postgres=# select relname, relfilenode from pg_class where relname='tbl1';
relname | relfilenode
--------- -------------
tbl1 | 16390
(1 row)
2.3 系统表文件组织结构
系统初始化后,没有创建任何表但是base/13158/下已经生成了很多表文件,这些文件就是当前数据库系统表,例如pg_class,注意系统表的relfilenode为0,可以使用隐藏列oid 或者 pg_relation_filepath函数找到表文件。
代码语言:javascript复制postgres=# select oid,relname, relfilenode from pg_class where relname='pg_class';
oid | relname | relfilenode
------ ---------- -------------
1259 | pg_class | 0
postgres=# SELECT pg_relation_filepath('pg_class');
pg_relation_filepath
----------------------
base/13158/1259
(1 row)
系统表分两类,一类是每个数据库内独有的系统表,一类是所有数据库共享的系统表。为什么有共享表的?例如pg_database记录cluster所有数据库的信息,不需要每个数据库单独存储一份。共享系统表存储在$PGDATA/global/
目录下。
postgres=# select oid,relname, relfilenode from pg_class where relname='pg_database';
oid | relname | relfilenode
------ ------------- -------------
1262 | pg_database | 0
postgres=# SELECT pg_relation_filepath('pg_database');
pg_relation_filepath
----------------------
global/1262
在数据目录中可以看到*_fsm
和*_vm
两种文件,两种文件和表文件是对应的,后面章节会有介绍。
ll | grep 1247
-rw------- 1 mingjie.gmj users 73728 Apr 22 20:47 1247
-rw------- 1 mingjie.gmj users 24576 Apr 22 20:17 1247_fsm
-rw------- 1 mingjie.gmj users 8192 Apr 22 20:47 1247_vm
# fsm文件:空闲空间映射表
# vm文件:可见性映射表
2.4 表空间
表空间可以理解为一个存放表文件的目录。表空间提供了表存储的灵活控制方式: 例如在当前磁盘快满时,可以在任意新挂载的文件系统上创建表空间,把表存储在新的目录中;一个频繁使用的表可以放在IO性能更好的磁盘上,比如SSD。很少使用的数据表可以存储在一个便宜、较慢的磁盘系统上。
使用表空间有两种方式:
- 创建表时指定表空间
- 创建数据库时指定表空间
创建表空间
代码语言:javascript复制CREATE TABLESPACE tablespace_name
[ OWNER { new_owner | CURRENT_USER | SESSION_USER } ]
LOCATION 'directory'
[ WITH ( tablespace_option = value [, ... ] ) ]
创建表空间后,可以在$PGDATA/pg_tblspc
找到表空间的符号链接,注意备份恢复后需要保证表空间的路径一致。
postgres=# create tablespace tmpspace location '/tmp/tmptsp';
CREATE TABLESPACE
postgres=# CREATE TABLE foo(i int) TABLESPACE tmpspace;
CREATE TABLE
postgres=# select oid,datname from pg_database where datname='postgres';
oid | datname
------- ----------
13158 | postgres
postgres=# select oid,relfilenode from pg_class where relname='foo';
oid | relfilenode
------- -------------
46807 | 46807
(1 row)
postgres=# select pg_relation_filepath('foo');
pg_relation_filepath
---------------------------------------------
pg_tblspc/46803/PG_10_201707211/13158/46807
(1 row)
pg_relation_filepath返回的地址pg_tblspc/46803/PG_10_201707211/13158/46807
,PG使用pg_tblspc目录下的链接来找到对应的表空间,表文件的真正目录是:
ll /tmp/tmptsp/PG_10_201707211/13158/46807
-rw------- 1 mingjie.gmj users 0 Apr 23 10:55 /tmp/tmptsp/PG_10_201707211/13158/46807
其中PG_10_201707211是按照版本号定义的:
代码语言:javascript复制PG _ 'Major version' _ 'Catalogue version number'
注意:PostgreSQL使用符号连接来简化表空间的实现,意味着表空间只能在支持符号连接的系统上使用。
3 表文件组织结构
表文件内部默认由8KB的block组成,8K是数据库读写的基本单位。《PostgreSQL数据库内核分析》中是这样描述的:
每一个页面包括五个部分。
项 | 描述 |
---|---|
PageHeaderData | 24字节长。包含关于页面的一般信息,包括空闲空间指针。 |
ItemIdData | 一个记录(偏移量,长度)对的数组,指向实际项。每个项 4 字节。 |
Free space | 未分配的空间(空闲空间)。新项指针从这个区域的开头开始分配,新项从其结尾开始分配。 |
Items | 实际的项本身。 |
Special space | 索引访问模式相关的数据。不同的索引访问方式存放不同的数据。在普通表中为空。 |
页面的前24个字节组成PageHeaderData,在页头后面是4字节的ItemIdData即途中的LinpX,可以理解为指针指向实际的tuple存储位置(图中的TupleX),注意linp和tuple的存储顺序是相反的,linp从上到下生长,tuple从页面下到上生长。
PageHeaderData中定义了当前页面的信息:
代码语言:javascript复制typedef struct PageHeaderData
{
/* XXX LSN is member of *any* block, not only page-organized ones */
PageXLogRecPtr pd_lsn; /* LSN: next byte after last byte of xlog
* record for last change to this page */
uint16 pd_checksum; /* checksum */
uint16 pd_flags; /* flag bits, see below */
LocationIndex pd_lower; /* offset to start of free space */
LocationIndex pd_upper; /* offset to end of free space */
LocationIndex pd_special; /* offset to start of special space */
uint16 pd_pagesize_version;
TransactionId pd_prune_xid; /* oldest prunable XID, or zero if none */
ItemIdData pd_linp[FLEXIBLE_ARRAY_MEMBER]; /* line pointer array */
} PageHeaderData;
x
域 | 类型 | 长度 | 描述 |
---|---|---|---|
pd_lsn | PageXLogRecPtr | 8 bytes | LSN: 最后修改这个页面的 WAL 记录最后一个字节后面的第一个字节 |
pd_checksum | uint16 | 2 bytes | 页面校验码 |
pd_flags | uint16 | 2 bytes | 标志位 |
pd_lower | LocationIndex | 2 bytes | 到空闲空间开头的偏移量 |
pd_upper | LocationIndex | 2 bytes | 到空闲空间结尾的偏移量 |
pd_special | LocationIndex | 2 bytes | 到特殊空间开头的偏移量 |
pd_pagesize_version | uint16 | 2 bytes | 页面大小和布局版本号信息 |
pd_prune_xid | TransactionId | 4 bytes | 页面上最老未删除XMAX,如果没有则为0 |
页头后面是ItemIdData,从代码上可以看到这是一个4字节按位拆开使用的结构体。它记录了偏移量、属性位和tuple的长度。
代码语言:javascript复制typedef struct ItemIdData
{
unsigned lp_off:15, /* offset to tuple (from start of page) */
lp_flags:2, /* state of item pointer, see below */
lp_len:15; /* byte length of tuple */
} ItemIdData;
Tuple的一般由几部分组成:
The overall structure of a heap tuple looks like: fixed fields (HeapTupleHeaderData struct) nulls bitmap (if HEAP_HASNULL is set in t_infomask) alignment padding (as needed to make user data MAXALIGN'd) object ID (if HEAP_HASOID is set in t_infomask) user data fields
Tuple存储在从未分配空间末尾开始从后向前的空间里。实际结构取决于表的内容。所有表行都用同样方法构造,都会有一个定长的头部HeapTupleHeaderData:
代码语言:javascript复制struct HeapTupleHeaderData
{
union
{
HeapTupleFields t_heap;
DatumTupleFields t_datum;
} t_choice;
ItemPointerData t_ctid; /* current TID of this or newer tuple (or a
* speculative insertion token) */
/* Fields below here must match MinimalTupleData! */
uint16 t_infomask2; /* number of attributes various flags */
uint16 t_infomask; /* various flag bits, see below */
uint8 t_hoff; /* sizeof header incl. bitmap, padding */
/* ^ - 23 bytes - ^ */
bits8 t_bits[FLEXIBLE_ARRAY_MEMBER]; /* bitmap of NULLs */
/* MORE DATA FOLLOWS AT END OF STRUCT */
};
typedef struct HeapTupleFields
{
TransactionId t_xmin; /* inserting xact ID */
TransactionId t_xmax; /* deleting or locking xact ID */
union
{
CommandId t_cid; /* inserting or deleting command ID, or both */
TransactionId t_xvac; /* old-style VACUUM FULL xact ID */
} t_field3;
} HeapTupleFields;
HeapTupleHeaderData存储的信息是实现MVCC机制的基础。
域 | 类型 | 长度 | 描述 |
---|---|---|---|
t_xmin | TransactionId | 4 bytes | 插入XID标志 |
t_xmax | TransactionId | 4 bytes | 删除XID标志 |
t_cid | CommandId | 4 bytes | 插入和/或删除CID标志(覆盖t_xvac) |
t_xvac | TransactionId | 4 bytes | VACUUM操作移动一个行版本的XID |
t_ctid | ItemPointerData | 6 bytes | 当前版本的TID或者指向更新的行版本 |
t_infomask2 | uint16 | 2 bytes | 一些属性,加上多个标志位 |
t_infomask | uint16 | 2 bytes | 多个标志位 |
t_hoff | uint8 | 1 byte | 到用户数据的偏移量 |
当通过表扫描或者索引拿到了tuple后,看起来只是拿到了一些乱码,必须使用表结构信息对数据进行切分才会有意义,表结构信息保存在pg_attribute系统表中。标识域位置的关键值是attlen和attalign。
attlen 对于一个固定尺寸的类型,typlen是该类型内部表示的字节数。对于一个变长类型,typlen为负值。-1表示一个“varlena”类型(具有长度字),-2表示一个以空值结尾的C字符串。 attalign typalign是当存储此类型值时要求的对齐性质 https://www.postgresql.org/docs/10/catalog-pg-type.html
4 表数据读取
顺序扫描 seqscan页面会顺序加载进入缓冲区,在缓冲区内扫描进程按照linp指针顺序扫描tuple。 PG顺序扫描的优化叫做同步扫描,即多进程并发扫描时,对同一张表后面的进程优先从其他进程正在扫描的位置开始扫描,避免缓冲区已经置换出去,增加大量IO(具体见《PostgreSQL数据库内核分析3.4.1》)
索引扫描 btree索引块会优先加载进入内存,在索引中定位到具体的数据页面和偏移量,然后加载指定的页面进入内存,按偏移量读取数据。可以看到IO操作大大减少了,也不需要进行数页面扫描(代价是索引块、页面的扫描)。