暂无图片
暂无图片
3
暂无图片
暂无图片
暂无图片

PostgreSQL 数据库学习 - 1.数据库体系结构之存储结构

原创 lmj 2022-01-21
788

一、逻辑存储结构

逻辑存储结构通常指表、索引、视图、函数等逻辑对象(也称数据库对象)

image-20220120153531236

参考之前写的文章数据库对象

二、物理存储结构

物理存储结构表示数据库在物理层面上是如何存储的。

在初始化initdb数据库集簇(database cluster)时会在指定目录下创建基础目录(即通常所说的数据目录,会被配置到环境变量PGDATA中)。所有相关组成数据库的物理文件的分布:

image-20220120154053628

image-20220121100752130

子目录/文件 描述
base 默认的数据库目录
global 数据库集簇中的所有表
pg_commmit_ts 事务提交的时间戳目录
pg_dynshmem 动态共享内存目录
pg_logical 逻辑解码的状态数据
pg_multixact 多个事务的状态目录
pg_notify 包含系统发出的 LISTEN/NOTIFY 的目录
pg_replslot 复制槽目录
pg_serial 已提交的可序列化事务的信息目录
pg_snapshots 快照目录(pg_export_snapshot函数导出的快照信息文件)
pg_stat 统计目录
pg_stat_tmp 临时统计目录
pg_subtrans 子事务的状态数据
pg_tblspc 各个用户自建表空间指向的软链接符号目录
pg_twophase 两阶段提交目录
pg_wal wal segment 目录
pg_xact 事务提交状态的目录
pg_hba.conf 基于主机的认证配置文件,用于配置允许哪些IP的主机通过哪种方式访问数据库
pg_ident.conf 基于主机的映射文件
PG_VERSION 包含PostgreSQL主版本号
postgresql.auto.conf 存储使用 ALTER SYSTEM修改的配置参数
postgresql.conf 默认的数据库参数文件
postmaster.opts 记录服务器上次启动时的相关密令信息
postmaster.pid 当前数据库运行的进程文件

下面介绍一些常用的的目录及文件

1、base 目录

1.1 概述

base目录存储用户创建的数据库文件,及隶属于用户数据库的所有关系,比如表、索引等

1.2 一级目录

以数据库oid命名的目录
image-20220121104303669

postgres=# SELECT oid,datname from pg_database;
  oid  |  datname
-------+------------
 13892 | postgres
 16385 | lmj_test     -- 自己建的数据库
     1 | template1
 13891 | template0
 16408 | db_new_tsp   -- 在自建的表空间里的数据库(下文有详细介绍)
(5 rows)
1.3 二级文件

存储某个数据库内的所有relation,包括表、索引、视图等。

二级子文件分为三大类:

  • oid命名的主数据文件;
  • _fsm结尾的空闲空间映射文件;
  • _vm结尾的可见性映射文件。

image-20220121105840757

-- 以 上图中的 1247 为例:1247是普通表 pg_type 的oid
postgres=# select oid,relname,relkind from pg_class where oid = 1247;
 oid  | relname | relkind
------+---------+---------
 1247 | pg_type | r
(1 row)
--- relkind 值为 r:表示普通表

pg_class 表参考

  • 主数据文件(具体介绍请看第三节堆表文件的内部布局

  • FSM(FREE SPACE MAP) - 空闲空间映射文件

    • 存储相应表或索引文件中每个页面上的空闲空间信息。hash索引除外

    • 详情可查看src/backend/storage/freespace/README

    • 可使用扩展 pg_freespace 查看特定表或索引上的空闲空间信息。

      lmj_test=> \c lmj_test postgres
      You are now connected to database "lmj_test" as user "postgres".
      lmj_test=# create extension pg_freespacemap ;
      CREATE EXTENSION
      lmj_test=# create table tab_fsm(id int,name varchar);
      CREATE TABLE
      lmj_test=# insert into tab_fsm select id,md5(id::varchar) from generate_series(1,10000) as id;
      INSERT 0 10000
      lmj_test=# select *,round(100*avail/8192 ,2) as "freespace ratio" from pg_freespace('tab_fsm') where avail !=0;
       blkno | avail | freespace ratio
      -------+-------+-----------------
          83 |  5440 |           66.00
      (1 row)
      

      详情可查看官网:pg_freespacemap扩展

  • VM(VISIBILITY MAP)可见性映射文件

    • 存储页面的可见性信息,包含所有活跃事务的元组,也包含需要冻结的一些元组

    • 可使用扩展 pg_freespace 查看

      lmj_test=# create extension pg_visibility ;
      CREATE EXTENSION
      lmj_test=# create table tab_vm as select * from tab_fsm ;
      SELECT 10000
      -- 删除前
      lmj_test=# select * from pg_visibility_map('tab_vm') where blkno = 0;
       blkno | all_visible | all_frozen
      -------+-------------+------------
           0 | t           | f
      -- 删除 id<100 的元组
      lmj_test=# delete from tab_vm where id<100;
      DELETE 99
      -- 删除后重新查看:all_visible t-->f,此时元组不可见
      lmj_test=# select * from pg_visibility_map('tab_vm') where blkno = 0;
       blkno | all_visible | all_frozen
      -------+-------------+------------
           0 | f           | f
      (1 row)
      

      详情可查看 pg_visibility扩展

    • 每一个堆元组都存储两位:如果第一位被设置,那么表示页面都是可见的,既然可见,那么该页面不包含任何清理的元组信息。索引扫描可以使用索引元组及逆行查询。如果第二位被设置,那么vacuum将会跳过所有的页面访问,提高vacuum的速度。

      /* src/backend/access/heap/visibilitymap.c */

      The visibility map is a bitmap with two bits (all-visible and all-frozen)per heap page.

      • A set all-visible bit means that all tuples on the page areknown visible to all transactions, and therefore the page doesn’t need tobe vacuumed.
      • A set all-frozen bit means that all tuples on the page arecompletely frozen, and therefore the page doesn’t need to be vacuumed evenif whole table scanning vacuum is required (e.g. anti-wraparound vacuum).The all-frozen bit must be set only when the page is already all-visible.
    • 索引没有可见性映射文件,只有空闲空间映射文件

2、global 目录

2.1 概述

存储pg_control及数据库集群维度的数据库及其关系,非客户维度的数据

image-20220121151538988

lmj_test=# select oid,relname,relkind from pg_class where oid = 6115 ;
 oid  |            relname            | relkind
------+-------------------------------+---------
 6115 | pg_subscription_subname_index | i
(1 row)
2.2 pg_control文件

pg_control文件记录数据库集群控制信息,包括initdb初始化、WAL和checkpoint的信息

3、pg_wal 目录

3.1 概述

pg_wal是WAL机制中的wal日志存储目录

PG 版本 目录名
<10 pg_xlog
>=10 pg_wal
3.2 WAL(write ahead log) 机制

数据库重做日志。从名字上看也可以简单的理解为,在数据写入磁盘前先记录日志。

3.3 文件结构

image-20220121152431759

image-20220121152304412

  • wal segment 文件(wal段文件)

    • 存储着数据库行记录明细,每一条记录明细都是服务于数据库恢复操作的,确保前后数据一致。首先针对数据的任意一次修改操作均被记录在wal段文件中,包括insert、update和delete,其次系统的一些管理行为也会被记录在wal段文件中,例如事务提交和vacuum等行为。

    • 文件名:26个十六进制数由三部分组成

      image-20220121154244804

      image-20220121154414959

      LSN(Log Sequnce Number)日志序列号:记录 wal 日志的绝对位置,是一个不断增长的8字节(64 bit )的长数字

    • 注意区分数据库log日志文件和wal文件。从内容上:数据库日志是记录数据库层面上的操作,比如记录启动数据库等操作(记录日志的内容可自行定义),而针对数据的任意一次修改操作均被记录在wal段文件中。从作用上:数据库日志-当系统发生错误时可以从 log 中查找 error,并解决问题;而wal日志用于数据库备份恢复及流复制等,与Oracle中的Redo Log功能相同。

    • 文件大小:默认16MB,可以在initdb时通过--wal-segments选项来配置wal文件大小

  • .history

    .history文件内容包括原.history文件,当前时间线切换记录和切换原因,作用于数据库的时间点恢复行为。当数据库引擎从多个时间线的备份中恢复时,数据库从.history文件中找到从pg_control的start_timeline到指定的recovery_target_timeline间的所有wal段文件进行恢复。

  • archive_status 目录

    archive_status是wal段文件的备份目录,包括.ready和.done文件。超出wal_keep_segments数目限制的wal日志会在archive_status目录内被标记,归档操作完成后被进一步移除。

    • .ready

      .ready是同名wal段文件在archive_status目录内的标记文件,代表该wal段文件可被归档。wal段文件在数据目录中的存储文件数量是有上限的,可以通过wal_keep_size 参数来约束,因此数据库引擎在wal段文件个数达到上限后会在archive_status目录内增加可移除的wal段文件的标记文件,文件名是原wal段文件名后增加.ready后缀,等待归档工具进行归档。

    • .done

      .done是同名wal段文件在archive_status目录内的标记文件,代表该wal段文件已被归档,可以被清理。数据库引擎默认通过archive_command命令对.ready文件进行归档,归档成功与否取决于archive_command命令返回true还是false,当archive_command返回true时,代表与.ready文件同名的wal段文件已被归档,引擎再将该文件的扩展名重命名为.done,等待数据库引擎在下一次的checkpoint时进一步清理原wal段文件

4、pg_xact 目录

4.1 概述

事务提交日志存储目录,默认256KB

4.2 commit log

image-20220121162120282

事务提交日志存储数据库的单个事务运行状态。Commit Log由共享内存中一组8KB的page组成,每个page包含一列数组,每个数组元素包含XID和该事物的实时状态。当page不足时,创建新的page来存储新的事务。

5、配置文件

  • postgresql.conf

    数据库初始化时默认的配置文件

  • postgresql.auto.conf

    存储使用 ALTER SYSTEM修改的配置参数,优先级别高于postgresql.conf文件

  • pg_hba.conf

    pg_hba.conf文件负责客户端的连接和认证,起到防火墙的作用。格式:TYPE DATABASE USER ADDRESS METHOD

三、表空间的布局

3.1 默认的表空间

两个默认的表空间:pg_defaultpg_global

postgres=# select oid,spcname from pg_tablespace;
 oid  |  spcname
------+------------
 1663 | pg_default
 1664 | pg_global
(2 rows)

postgres=# create table tab_default_tsp(id int,name varchar);
CREATE TABLE
-- reltablespace=0,表示使用的是默认表空间
postgres=# select oid,relname,relfilenode,reltablespace from pg_class where relname = 'tab_default_tsp';
  oid  |     relname     | relfilenode | reltablespace
-------+-----------------+-------------+---------------
 16391 | tab_default_tsp |       16391 |             0
(1 row)
  • 所有创建的数据库,如果没有明确指定表空间,那么使用默认的表空间 pg_default。用于管理整个数据库集群的表默认被存储在pg_global表空间中

    create database lmj_test owner lmj;
    
    postgres=# SELECT db.oid,db.datname,tsp.spcname from pg_database db inner join pg_tablespace tsp on db.dattablespace=tsp.oid where db.datname = 'lmj_test';
      oid  | datname  |  spcname
    -------+----------+------------
     16385 | lmj_test | pg_default
    (1 row)
    
  • 用于管理整个数据库集群的表默认被存储在pg_global表空间中;pg_global表空间的物理位置为$PGDATA\global目录。

  • pg_global不能被当作默认的表空间

    image-20220121165248545

3.2 用户创建的表空间

执行CREATE TABLESPACE语句会在指定的目录下创建表空间,在该目录下还会创建版本特定的子目录。版本特定的命名方式为PG_主版本号_目录版本号

image-20220121163425572

用户创建的表空间对应一个目录

--# 在 /soft/pg14/data6000/lmj_tsp_test 目录下创建了 lmj_tsp_test 表空间
postgres=# create tablespace lmj_tsp_test owner lmj location '/soft/pg14/data6000/lmj_tsp_test';
CREATE TABLESPACE

--# 新建的表空间对应的 oid = 16397
postgres=# select oid,spcname from pg_tablespace;
  oid  |   spcname
-------+--------------
  1663 | pg_default
  1664 | pg_global
 16397 | lmj_tsp_test
(3 rows)

--# 在目录下创建了版本特定的子目录-PG_14_202107181
postgres=# \! ls -l /soft/pg14/data6000/lmj_tsp_test
total 0
drwx------. 2 postgres postgres 6 Jan 20 17:20 PG_14_202107181 

--# $PGDATA/pg_tblspc 目录下有与新建表空间oid相同的符号链接指向实际的表空间目录
postgres=# \! ls -l $PGDATA/pg_tblspc
total 0
lrwxrwxrwx. 1 postgres postgres 32 Jan 20 17:20 16397 -> /soft/pg14/data6000/lmj_tsp_test

--#1. 在新建的表空间 lmj_tsp_test 下创建 tab_new_tsp 表
postgres=# create table tab_new_tsp(id int,name varchar) tablespace lmj_tsp_test;
CREATE TABLE

---# tab_new_tsp 表的所属表空间为 lmj_tsp_test
postgres=# select oid,relname,relfilenode,reltablespace from pg_class where relname = 'tab_new_tsp';
  oid  |   relname   | relfilenode | reltablespace
-------+-------------+-------------+---------------
 16403 | tab_new_tsp |       16403 |         16397
(1 row)

---# 使用 pg_relation_filepath 函数查找该表对应的目录
postgres=# select pg_relation_filepath('tab_new_tsp');
            pg_relation_filepath
---------------------------------------------
 pg_tblspc/16397/PG_14_202107181/13892/16403
(1 row)

--#2. 在新建的表空间 lmj_tsp_test 下创建 db_new_tsp 数据库
create database db_new_tsp owner lmj tablespace lmj_tsp_test;

---# 数据库对应的 oid=16408
postgres=# select oid,datname,datdba,dattablespace from pg_database where datname = 'db_new_tsp';
  oid  |  datname   | datdba | dattablespace
-------+------------+--------+---------------
 16408 | db_new_tsp |  16384 |         16397
(1 row)

---# 在版本特定的子目录下创建相应的数据库目录
postgres=# \! ls -l $PGDATA/lmj_tsp_test/PG_14_202107181 | grep 16408
drwx------. 2 postgres postgres 8192 Jan 20 17:47 16408

四、表和索引相关文件的布局

4.1 数据库相关知识

  • 任何的新初始化数据库都有一个统一的存放目录 base 用来存放默认的数据库:postgres、template1、template0。

    postgres=# SELECT oid,datname,datdba from pg_database ;
      oid  |  datname   | datdba
    -------+------------+--------
     13892 | postgres   |     10
         1 | template1  |     10
     13891 | template0  |     10
     16408 | db_new_tsp |  16384
    (4 rows)
    
    postgres=# \! ls -l $PGDATA/base
    total 48
    drwx------. 2 postgres postgres 8192 Jan 12 18:02 1
    drwx------. 2 postgres postgres 8192 Jan 12 18:02 13891
    drwx------. 2 postgres postgres 8192 Jan 20 17:26 13892
    
  • 当创建数据库时不指定TEMPLATE,将使用默认模板数据库template1

  • 在数据库中有一个特殊的数据类型叫做 oid(object identifier)对象标识符,每个数据库都有对应的唯一的 oid 值。整个数据库结构和数据库对象都是通过 oid来管理的。

4.2 表在数据库中的管理

创建一张表 tab_test,oid=16414

postgres=# \c lmj_test lmj
You are now connected to database "lmj_test" as user "lmj".
lmj_test=> create table tab_test (id int,name varchar);
CREATE TABLE

lmj_test=> select oid,relname,relfilenode from pg_class where relname = 'tab_test';
  oid  | relname  | relfilenode
-------+----------+-------------
 16414 | tab_test |       16414
(1 row)

lmj_test=> select pg_relation_filepath('tab_test');
 pg_relation_filepath
----------------------
 base/16385/16414
(1 row)
  • 默认不会给创建的空表分配空间,只分配一个文件名称

    lmj_test=> \! ls -l $PGDATA/base/16385/16414 -rw-------. 1 postgres postgres 0 Jan 20 18:24 /soft/pg14/data6000/base/16385/16414
  • 在第一次执行INSERT操作后,将会分配一个页面的大小(8K)

    lmj_test=> insert into tab_test values(1,'lmj'); INSERT 0 1 lmj_test=> \! ls -l $PGDATA/base/16385/16414 -rw-------. 1 postgres postgres 8192 Jan 20 18:32 /soft/pg14/data6000/base/16385/16414
  • 表在内部通过 oid 管理,外部通过 refilenode 管理

    注:表的 oid 不一定等于 refilenode,如下

    # 对表进行 truncate 操作 lmj_test=> truncate TABLE tab_test ; TRUNCATE TABLE # 表的 oid 不等于 filenode,表的 filenode 的值发生改变 lmj_test=> select oid,relname,relfilenode from pg_class where relname = 'tab_test'; oid | relname | relfilenode -------+----------+------------- 16414 | tab_test | 16419 (1 row) # 查询表的存储位置 - 以 filenode 为名的文件 lmj_test=> select pg_relation_filepath('tab_test'); pg_relation_filepath ---------------------- base/16385/16419 (1 row) # 此时表的 oid和 filenode 的目录下的文件都为空 lmj_test=> \! ls -l $PGDATA/base/16385/16414 -rw-------. 1 postgres postgres 0 Jan 20 18:34 /soft/pg14/data6000/base/16385/16414 lmj_test=> \! ls -l $PGDATA/base/16385/16419 -rw-------. 1 postgres postgres 0 Jan 20 18:34 /soft/pg14/data6000/base/16385/16419 # 插入数据 lmj_test=> insert into tab_test values(1,'lmj'); INSERT 0 1 # 以 oid 命名的文件大小仍为0,filenode命名为文件大小为 8K lmj_test=> \! ls -l $PGDATA/base/16385/16414 -rw-------. 1 postgres postgres 0 Jan 20 18:34 /soft/pg14/data6000/base/16385/16414 lmj_test=> \! ls -l $PGDATA/base/16385/16419 -rw-------. 1 postgres postgres 8192 Jan 20 18:35 /soft/pg14/data6000/base/16385/16419
  • 表默认单个文件大小为1G。如果超过1G,将会使用relfilenode.编号(编号从1开始)来命名新的文件,直到新文件填满1G,以此类推。也可以在编译时使用 --eith-segsize来指定

五、堆表文件的内部布局

5.1 堆表文件的页面布局

image-20220121163500580

表的页面包含三种类型的数据:

  • 堆元组(heap tuples) :堆元组就是数据记录本身。它们从页面底部开始依序堆叠

  • 行指针(line pointer):每个行指针占4个字节,保存着指向堆元组的指针。它们也被称为项目指针(item pointer)。行指针简单地组织为一个数组,扮演了元组索引的角色。每个索引项从1开始依次编号,称为偏移号(offset number)。当向页面中添加新元组时,一个相应的新行指针也会被放入数组中,并指向新添加的元组。

  • 首部数据(header data):页面的起始位置分配了由结构PageHeaderData定义的首部数据。它的大小为24个字节,包含关于页面的元数据。该结构的主要成员变量为:

    成员变量 描述
    pd_lsn 该页面最近一次变更所写入XLOG记录对应的LSN。它是一个8字节无符号整数
    pd_checksum 页面的校验和
    pd_flags 标记位(数据页面中的格式化的空间或未格式化的空间)
    pd_lower 空闲空间的起始位置
    pd_upper 空间空间的结束位置
    pd_special 特殊空间的偏移量
    pd_pagesize_version 页面的大小以及页面的版本编号
    pd_prune_xid 页面中最老元组的xid

/* @src/include/storage/bufpage.h */

在 bufpage.h 文件中查看标准的页面定义

5.2 使用 pg_filedump 工具分析

lmj_test=# select pg_relation_filepath('tab_test');
 pg_relation_filepath
----------------------
 base/16385/16451
(1 row)

image-20220121172112029

lmj_test=# select * from tab_test ;
 id |    name
----+------------
  1 | lmj
  1 | PostgreSQL
(2 rows)
-- 显示格式化的块内容转储
[postgres@node1 pg_filedump]$ ./pg_filedump -d $PGDATA/base/16385/16451 | tail -n 10
  1f90: 00000000 00000000 00000000 00000000  ................
  1fa0: 00000000 00000000 00000000 00000000  ................
  1fb0: 00000000 00000000 fd020000 00000000  ................
  1fc0: 00000000 00000000 02000200 02091800  ................
  1fd0: 01000000 17506f73 74677265 53514c00  .....PostgreSQL.
  1fe0: fc020000 00000000 00000000 00000000  ................
  1ff0: 01000200 02091800 01000000 096c6d6a  .............lmj
最后修改时间:2022-01-21 17:48:39
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论