本文参考书籍《postgresql指南–内幕探索》的
Chapter 1:Database Cluster, Databases and Tables
该书的网络内容可参考:The Internals of PostgreSQL
本文内容转载自:数据库集簇、数据库和数据表
数据库集簇的逻辑结构
数据库集簇(database cluster)是一组数据库(database)的集合,由一个PostgreSQL服务器管理。数据库集簇与高可用数据库集群不同,并非意味着“一组数据库服务器”,一个PostgreSQL服务器只会在单机上运行并管理单个数据库集簇。
数据库集簇的物理结构
数据库集簇本质上就是一个文件目录,即基础目录。执行initdb会在指定目录下创建基础目录,从而初始化一个新的数据库集簇。
PostgreSQL中的表空间对应一个包含基础目录之外的数据的目录。
数据库集簇的布局
主要的文件与子目录如下:
文件 | 描述 |
---|---|
PG_VERSION | 包含 PostgreSQL 主版本号 |
pg_hba.conf | 控制 PostgreSQL 客户端认证 |
pg_ident.conf | 控制 PostgreSQL 用户名映射 |
postgresql.conf | 保存数据库相关的配置参数 |
postgresql.auto.conf | 存储使用 ALTER SYSTEM 修改的配置参数 (version 9.4 or later) |
postmaster.opts | 记录服务器上次启动的命令行选项,例如:/home/postgres/pgsql/bin/postgres “-D” “/pgdata/postgres” “-c” |
base/ | 每个数据库对应的子目录都存储在此 |
global/ | 数据库集簇范畴的表 (例如 pg_database) 及 pg_control |
pg_commit_ts/ | 事务提交的时间戳数据 (Version 9.5 or later) |
pg_clog/ | 事务提交状态数据 (Version 9.6 or earlier)。在版本10.0中被重命名为 pg_xact |
pg_dynshmem/ | 动态共享内存子系统中使用的文件 (Version 9.4 or later) |
pg_logical/ | 逻辑解码的状态数据 (Version 9.4 or later) |
pg_multixact/ | 多事务状态数据 |
pg_notify/ | LISTEN/NOTIFY 状态数据 |
pg_repslot/ | 复制槽数据 (Version 9.4 or later) |
pg_serial/ | 已提交的可串行化事务相关信息 (version 9.1 or later) |
pg_snapshots/ | 导出快照 (version 9.2 or later)。PostgreSQL 函数 pg_export_snapshot 在此子目录中创建的快照信息文件 |
pg_stat/ | 统计子系统的永久文件 |
pg_stat_tmp/ | 统计子系统的临时文件 |
pg_subtrans/ | 子事务状态数据 |
pg_tblspc/ | 指向表空间的符号链接 |
pg_twophase/ | 两阶段事务的状态文件 |
pg_xlog/ | WAL 段文件 (Version 9.6 or earlier),它在版本10.0中被重命名为 pg_wal |
数据库布局
一个数据库与base子目录下的一个子目录对应。
SELECT
datname,
oid
FROM
pg_database
ORDER BY
oid;
datname | oid
-----------+-------
template1 | 1
template0 | 13268
postgres | 13269
(3 rows)
[postgres@2ef1e2faf3d9:base]$ ls -l
total 12
drwx------ 2 postgres dba 4096 May 15 08:40 1
drwx------ 2 postgres dba 4096 May 15 08:40 13268
drwx------ 2 postgres dba 4096 May 19 08:23 13269
表和索引相关文件的布局
每个小于1GB的表或索引都在相应的数据库目录中存储为单个文件。这些数据文件由变量relfilenode管理。
SELECT
relname,
oid,
relfilenode
FROM
pg_class
WHERE
relname = 't_test';
relname | oid | relfilenode
---------+-------+-------------
t_test | 16447 | 16447
(1 row)
[postgres@2ef1e2faf3d9:13269]$ ls -l |grep 16447
-rw------- 1 postgres dba 0 May 19 10:10 16447
relfilenode通常和oid一致。不过,relfilenode会被一些命令(例如TRUNCATE、REINDEX、CLUSTER、VACUUM FULL)所改变。
TRUNCATE t_test;
SELECT
relname,
oid,
relfilenode
FROM
pg_class
WHERE
relname = 't_test';
relname | oid | relfilenode
---------+-------+-------------
t_test | 16447 | 16450
(1 row)
SELECT
pg_relation_filepath('16447');
pg_relation_filepath
----------------------
base/13269/16450
(1 row)
SELECT
pg_relation_filepath('t_test');
pg_relation_filepath
----------------------
base/13269/16450
(1 row)
当表和索引的文件大小超过1GB时,PostgreSQL会创建并使用一个名为relfilenode.1的新文件,以此类推,会有relfilenode.2。
INSERT INTO t_test (
id
)
SELECT
generate_series(1, 100000000);
SELECT
pg_size_pretty(pg_table_size('t_test'));
pg_size_pretty
----------------
3458 MB
(1 row)
[postgres@2ef1e2faf3d9:13269]$ ls -l |grep 16450
-rw------- 1 postgres dba 1073741824 May 19 10:27 16450
-rw------- 1 postgres dba 1073741824 May 19 10:27 16450.1
-rw------- 1 postgres dba 1073741824 May 19 10:27 16450.2
-rw------- 1 postgres dba 403554304 May 19 10:27 16450.3
-rw------- 1 postgres dba 909312 May 19 10:27 16450_fsm
在编译PostgreSQL时,可以使用配置选项–with-segsize更改表和索引的最大文件大小。
SELECT
name,
setting,
unit,
short_desc
FROM
pg_settings
WHERE
name like 'seg%size%';
name | setting | unit | short_desc
--------------+---------+------+------------------------------------------
segment_size | 131072 | 8kB | Shows the number of pages per disk file.
(1 row)
SHOW segment_size ;
segment_size
--------------
1GB
(1 row)
每个表都有两个与之关联的文件,_fsm和_vm分别存储了表文件每个页面上的空闲空间信息与可见性信息。索引没有可见性映射文件,只有空闲空间映射文件。
CREATE TABLE t_test2 (
id int
);
SELECT
pg_relation_filepath('t_test2');
pg_relation_filepath
----------------------
base/13269/16454
(1 row)
# 在另一个窗口查看
[postgres@2ef1e2faf3d9:13269]$ ls -l |grep 16454
-rw------- 1 postgres dba 0 May 19 10:41 16454
INSERT INTO t_test2 (
id
)
SELECT
generate_series(1, 100000);
# 在另一个窗口查看
[postgres@2ef1e2faf3d9:13269]$ ls -l |grep 16454
-rw------- 1 postgres dba 3629056 May 19 10:41 16454
-rw------- 1 postgres dba 24576 May 19 10:41 16454_fsm
UPDATE t_test2
SET
id = id + 1;
CHECKPOINT;
# 在另一个窗口查看
[postgres@2ef1e2faf3d9:13269]$ ls -l |grep 16454
-rw------- 1 postgres dba 7249920 May 19 10:42 16454
-rw------- 1 postgres dba 24576 May 19 10:42 16454_fsm
-rw------- 1 postgres dba 8192 May 19 10:42 16454_vm
表空间的布局
PostgreSQL中的表空间是基础目录之外的附加数据区域。
在/home/postgres/tblspc中创建一个表空间new_tblspc,其oid为16458,则会在表空间下创建一个名如“PG_主版本号_目录版本号”的子目录。
CREATE TABLESPACE new_tblspc LOCATION '/home/postgres/tblspc';
SELECT
*,
oid
FROM
pg_tablespace
WHERE
spcname = 'new_tblspc';
oid | spcname | spcowner | spcacl | spcoptions
-------+------------+----------+--------+------------
16458 | new_tblspc | 10 | |
(1 row)
[postgres@2ef1e2faf3d9:tblspc]$ ls -l /home/postgres/tblspc/
total 4
drwx------ 2 postgres dba 4096 May 19 14:34 PG_9.6_201608131
表空间目录通过pg_tblspc子目录中的软链接寻址,链接名称与表空间oid相同。
[postgres@2ef1e2faf3d9:pg_tblspc]$ ls -l
total 0
lrwxrwxrwx 1 postgres dba 21 May 19 14:34 16458 -> /home/postgres/tblspc
在该表空间下创建数据库:
CREATE DATABASE test TABLESPACE new_tblspc;
SELECT
datname,
oid
FROM
pg_database
WHERE
datname = 'test';
datname | oid
---------+-------
test | 16459
(1 row)
[postgres@2ef1e2faf3d9:PG_9.6_201608131]$ ls -l /home/postgres/tblspc/PG_9.6_201608131
total 4
drwx------ 2 postgres dba 4096 May 19 14:37 16459
如果在new_tblspc表空间内创建一个新表,但新表所属的数据库却创建在基础目录下(比如postgres数据库),那么PostgreSQL会首先在new_tblspc表空间内版本特定的子目录下,创建目录名称与现有数据库oid相同的子目录,然后将新表文件放置在这个目录下。
postgres=# CREATE TABLE t_test3 (id int) tablespace new_tblspc;
postgres=# SELECT relname, oid FROM pg_class where relname = 't_test3';
relname | oid
---------+-------
t_test3 | 16460
(1 row)
postgres=# SELECT pg_relation_filepath('16460');
pg_relation_filepath
----------------------------------------------
pg_tblspc/16458/PG_9.6_201608131/13269/16460
(1 row)
[postgres@2ef1e2faf3d9:tblspc]$ ls -l /home/postgres/tblspc/
total 4
drwx------ 4 postgres dba 4096 May 19 14:55 PG_9.6_201608131
[postgres@2ef1e2faf3d9:tblspc]$ ls -l /home/postgres/tblspc/PG_9.6_201608131/
total 8
drwx------ 2 postgres dba 4096 May 19 14:55 13269
drwx------ 2 postgres dba 4096 May 19 14:37 16459
[postgres@2ef1e2faf3d9:tblspc]$ ls -l /home/postgres/tblspc/PG_9.6_201608131/13269/
total 0
-rw------- 1 postgres dba 0 May 19 14:55 16460
堆表文件的内部布局
数据文件(堆表、索引,也包括空闲空间映射和可见性映射)内部被划分为固定长度的页,或者叫区块,大小默认为8192B(8KB)。
每个文件中的页从0开始按顺序编号,这些数字称为区块号。如果文件已填满,PostgreSQL就通过在文件末尾追加一个新的空页来增加文件长度。页面内部的布局取决于数据文件的类型。
表的页面包含了三种类型的数据:
-
堆元组——即数据记录本身。它们从页面底部开始依序堆叠。
-
行指针——每个行指针占4B,保存着指向堆元组的指针。它们也被称为项目指针(item pointer)。行指针形成一个简单的数组,扮演了元组索引的角色。每个索引项从1开始依次编号,称为偏移号。当向页面中添加新元组时,一个相应的新行指针也会被放入数组中,并指向新添加的元组。
-
首部数据——页面的起始位置分配了由结构PageHeaderData定义的首部数据。它的大小为24B,包含关于页面的元数据。
结构体PageHeaderData定义于src/include/storage/bufpage.h。该结构的主要成员变量如下:
-
pd_lsn——本页面最近一次变更所写入的xlog记录对应的lsn。它是一个8B无符号整数,与WAL机制有关。
-
pd_checksum——本页面的校验和值。
-
pd_lower、pd_upper——pd_lower指向行指针的末尾,pd_upper指向最新堆元组的起始位置。
-
pd_special——在索引页中会用到该字段,在堆表页中它指向页尾。(在索引页中它指向特殊空间的起始位置,特殊空间是仅由索引使用的特殊数据区域,包含特定的数据,具体内容依索引的类型而定)
行指针的末尾与最新元组起始位置之间的空余空间称为空闲空间或空洞。
为了识别表中的元组,数据库内部会使用元组标识符(tuple identifier,TID)。TID由一对值组成,分别是元组所属页面的区块号和指向元组的行指针的偏移号。
此外,大小超过约2KB(8KB的四分之一)的堆元组会使用一种称为TOAST(The Oversized-Attribute Storage Technique,超大属性存储技术)的方法来存储与管理。
读写元组的方式
写入堆元组
假设有一个表仅由一个页面组成,且该页面只包含一个堆元组。此页面的pd_lower指向第一个行指针,而该行指针和pd_upper都指向第一个堆元组。
当写入第二个元组时,它会被放在第一个元组之后。第二个行指针写入到第一个行指针的后面,并指向第二个元组。pd_lower更改为指向第二个行指针,pd_upper更改为指向第二个堆元组。
页面内的首部数据也会被改写为适当的值。
读取堆元组
这里简述两种典型的访问方式:顺序扫描与B树索引扫描。
-
顺序扫描——通过扫描每一页中的行指针,依序读取所有页面中的所有元组。
-
B树索引扫描——索引文件包含索引元组,索引元组由一个键值对组成,键为被索引的列值,值为目标堆元组的TID。进行索引查询时,首先使用键进行查找,如果找到了对应的索引元组,PostgreSQL就会根据相应值中的TID来读取对应的堆元组。
PostgreSQL还支持TID扫描。TID扫描是一种通过使用所需元组的TID直接访问元组的方法。
CREATE TABLE t_test4 (
id int
);
INSERT INTO t_test4 (
id
)
VALUES (1);
SELECT
ctid,
id
FROM
t_test4
WHERE
ctid = '(0, 1)';
ctid | id
-------+----
(0,1) | 1
(1 row)
EXPLAIN
SELECT
ctid,
id
FROM
t_test4
WHERE
ctid = '(0, 1)';
QUERY PLAN
--------------------------------------------------------
Tid Scan on t_test4 (cost=0.00..4.01 rows=1 width=10)
TID Cond: (ctid = '(0,1)'::tid)
(2 rows)