PostgreSQL内幕探索—基础知识
PostgreSQL(以下简称PG) 起源于 1986 年加州大学伯克利分校的 POSTGRES 项目,最初以对象关系模型为核心,支持高级数据类型和复杂查询功能。
1996 年更名为 PostgreSQL 并开源,逐步发展为功能全面的企业级关系型数据库系统,支持 SQL 标准并持续扩展特性(如多版本并发控制、地理空间数据支持等)
PG基础知识
大致从下面几个方面来了解PG的基础知识
- 数据集簇
- 数据库
- 表和索引
- 表空间
- 表文件
- 元组
数据集簇
数据集簇是一组数据库的集合,由一个PG服务器进行管理,可以简单理解为一个数据库实例。我们通过一张图来了解一个数据集簇的逻辑结构,如下图所示
逻辑结构
从图中我们可以知道,一个数据集簇中可以有多个数据库,每个数据库中包含了不同的对象,例如表table、索引index、视图view等。每个对象都有唯一的一个标识,称之为对象标识符oid,PG通过oid来管理各个对象。数据库中对象与oid的映射关系存储在对应的系统目录中,因对象不同有所差异,例如数据库对象存在于pg_database下,而堆表对象存在于pg_class下。如果需要查找对象的oid,可以使用下面的语句
# 查询数据库对象的oid
select datname,oid from pg_database where datname = 'testdb';
# 查询表对象的oid
select relname,oid from pg_class where relname = 't1';
物理结构
上面了解了数据集簇的逻辑结构,我们现在来认识数据集簇的物理结构。数据集簇的物理结构本质上是一个文件目录,包含了很多子目录和文件,通过initdb可以在指定目录$PGDATA
下创建一个初始化的数据集簇。下面的重要目录包含了base目录,默认有几个模板数据库,可以直接在目录下直接看到他们的oid。新创建的数据库都会在这个目录下面
数据集簇布局
这里直接摘用文章中的内容来详细展示数据集簇的布局,列出主要的文件和子目录
- 文件
文件 | 描述 |
---|---|
PG_VERSION | 包含PG主版本号的文件 |
pg_hba.conf | 控制PG的客户端认证 |
pg_ident.conf | 控制PG的用户名映射 |
postgresql.conf | 参数配置文件 |
postgresql.auto.conf | 存储使用alter system命令修改的配置参数 |
postmaster.opts | 记录上次启动的命令行选项 |
- 子目录
子目录 | 描述 |
---|---|
base/ | 每个数据库的子目录皆存储在此 |
global/ | 数据集簇范围的表以及pg_control文件 |
pg_commit_ts/ | 事务提交时间戳(9.5+) |
pg_clog/ | 事务提交状态日志 |
pg_dynshmem/ | 动态共享内存子系统使用的文件 |
pg_logical/ | 逻辑解码的状态数据 |
pg_mulixact/ | 多事务状态数据 |
pg_notify/ | LISTEN/NOTIFY数据 |
pg_repslot/ | 复制槽数据(9.4+) |
pg_serial/ | 已提交的可串行化事务数据 |
pg_snapshots/ | 存储导出的快照信息 |
pg_stat/ | 统计子系统的永久文件 |
pg_stat_temp/ | 统计子系统的临时文件 |
pg_subtrans/ | 子事务状态数据 |
pg_tblspc/ | 指向表空间的符号连接 |
pg_twophase/ | 两阶段事务的状态文件 |
pg_xlog/ | wal日志文件 |
数据库布局
在base子目录下有对应的oid目录,例如testdb的oid为16384,那么目录为base/16384
表和索引文件布局
每个小于1GB的表或者索引都在相应的数据库目录下存在对应的文件。数据库内部,表和索引是用oid进行管理的,但是在文件上有所差异,文件中使用的是refilenode
,这个refilenode
一般和oid相同,但是如果表发生了truncate、reindex、cluster等命令被改变,那么这个refilenode
就会发生改变。
使用内置函数pg_relation_filepath
可以轻松获取到表或索引对象的文件路径,当表或索引对象的数据超过了那么1GB之后,那么PG会创建一个refilenode.1
的新文件,如果新文件再次被用满,生成refilenode.2
文件,依此类推
在初始化PG时,可以通过--with-segsize
来指定表和索引文件的最大文件大小。
_fsm文件和_vm文件
每个表都有相关联的_fsm(空闲空间映射)文件和_vm(可见性映射)文件,分别存储了表文件上每个页面的空闲空间信息和可见性信息。索引只有_fsm文件
内部相关概念
在数据库内部,这些文件被称为相应的分支。一般有四种分支
- 0 数据文件本体
- 1 fsm保存空闲空间信息
- 2 vm保存可见性信息
- 3 不常见的特殊分支,表示不被日志记录的表和索引
表空间布局
表空间是在基础目录下附加的目录,在建表时指定表空间,那么会将此表的文件生成到该表空间的目录下,该目录下还会创建一个PG_主版本号_目录版本号
的目录。通过pg_tblspc
子目录进行关联
堆表文件内部布局
数据文件(堆表,索引,也包括vm和fsm文件)内部被划分为固定长度的页,或者叫区块,大小默认为8KB,可以在PG初始化的时候进行调整。每个文件中的页从0开始编号,称之为区块号。如果当前页被填满,那么在末尾追加一个新页来扩展文件大小。
堆表文件的布局如下图
下面来详细解释下表文件一个页(区块)中的三种数据类型
- 堆元组tuple:这里的tuple可以理解为行,存储的就是数据本身,从页面底部开始堆叠
- 行指针:保存者指向堆元组的指针,每个行指针占4B,形成一个简单的数组,扮演了元组索引的角色。每个索引项从1开始编号,这个编号称为偏移量。每次有新元组插入时,同时也会插入行指针,并指向新元组。
- 头部数据header:header中包含了很多重要的信息,下面一一列出
- pd_lsn 记录本页面最近一次变更写入xlog记录的lsn号,和wal机制相关
- pd_checksum 记录本页面的校验和值
- pd_lower、pd_upper pd_lower指向行指针的末尾,pd_upper指向最新元组的起始位置,可以使用pd_upper-pd_lower得到当前页大致的剩余可用空间
- pd_special 在索引页中会用到,在堆表页指向页尾。在索引中指向特殊区域的起始位置,特殊区域是仅由索引使用的特殊数据区域,例如btree,空间索引等
- pd_prune_xid 本页面中可以修剪的最老元组中的xid
为了标识表中的元组,数据库内部会使用元组标识符tid来管理。tid由一对值组成,分别是元组所在区块号和行指针数组的偏移量。
此外,大小如果超过了2KB(1/4)的堆元组会使用一种TOAST的方法来存储和管理
读写元组的方式
下面详细介绍下读写元组的流程,假设目前有一张表,这张表只有一个数据页,且页中只有一个tuple。
写元组
根据列子的情况,我们画出写入元组前后的对比
我们大致描述一下整体的步骤
- 从页面底部插入一条tuple,放在tuple1之后,将pd_upper的指针指向tuple2的起始位置
- 在行指针1后面生成一个行指针,偏移量为2,行指针2指向tuple2,pd_lower指向行指针2的末尾
- pd_lsn,pg_checksum,pg_flag也被修改为合适的值,后面详细展开说。
读元组
我们这里介绍两种常见的读取方式
- 顺序扫描:通过扫描每一页的行指针,依次读取所有页面的所有元组。
- btree索引扫描:索引文件中包含了索引元组,索引元组由一对值组成,key为索引列的值,value为目标元组的tid。这样可以直接通过索引读取到目标元组,避免不必要的页面扫描。