一: 基本概念:
1: 数据库集簇(database cluster) :
一组数据库(databse)的集合。
2: 数据库对象:
存储或引用数据的数据结构。常见: table, datatabse, index, view, sequence, fuction, tablespace
3: 数据库对象 <==> oid :
oid是数据库对象的唯一标识
4: 查询数据库的oid:
select oid from pg_database;
5: 查询表的oid:
select oid from pg_class;
6: 数据库集簇:
物理上就是一个目录(基础目录base)。 initdb时,会在指定的目录下(一般是环境变量PGDATA),创建基础目录base.
7: base目录中的每一个子目录, 物理上都对应一个数据库. 那么该数据库的表、索引等对象都在这个子目录下存储为一个文件。
8: 一个数据库<==> base子目录下的一个目录对应。该目录名称==数据库oid.
select oid from pg_database <==> $PGDATA/base/*
9: 表和索引作为数据库对象也是通过oid管理的。
select relfilename, oid, relfilenode from pg_class;
这里relfilenode字段表示的是数据库文件名。 relfilename字段表示的是表名。
当其小于1GB,表在数据库目录下会存储为单个文件。当超过1GB,会扩展出relfilenode.1(表oid.1)这个文件。
(表名relfilenode 和 表oid一致)
10: 数据目录
base/数据库oid/表oid
11: cd $PGDATA
ls -la base/16384/18751*
>> base/16384/18751
>> base/16384/18751_fsm
>> base/16384/18751_vm
fsm: 空闲空间映射, vm可见性空间映射。
12: 创建表空间:
create tablespace tablespace_name realative location '表空间的目录';
eg: create tablespace examples relative location 'tablespace3';
在指定的目录:PGDATA的realative location下创建一个相对路径tablespace3, $PGDTA/realative location/tablespace3;
该语句会在pg的表空间目录下$PGDTA/pg_tblspc/ 下创建一个链接指向指定的目录。链接的名称和表空间的oid一致。
eg: ls -la $PGDTA/pg_tblspc
>> 16386 -> $PGDTA/realative location/tablespace3
ls -la $PGDTA/realative location/tablespace3
>> PG_主版本号_目录版本号 (创建的表空间下会有一个特定子目录)
13: 数据文件(base目录下的文件):
包括表,索引, 空闲空间映射、可见性空间映射。在磁盘或内存中以固定长度页,大小为8KB, 来存储。页的个数用区块号表示, 从0开始。
14: 页:
(1)堆元祖:数据记录本身, 位于页面底部(一步步堆叠)
(2)行指针:每个行指针4KB, 指向堆元组。行指针形成一个简单的数组,看做是元祖的索引。
(3)首部数据:PageHeaderData. 大小为24KB.
包括:pd_lsn: 本页面最近变更对应的xlog记录的标识
pd_checksum: 本页面的校验和
pd_lower,pd_upper: 指向行指针的末尾和最新堆元组的起始位置
15:页的空闲空间:
行指针的末尾和最新元祖的起始位置之间的空余空间。
16:TID(tuple id):
数据库内部元组的唯一标识符。
TID由一对值组成,分别为页的区块号,和行指针的偏移号。TID=(block=1, offset=2)表述表中第1页第2个元组。
17: 读写堆元组:
写入: 页面tuple从下往上堆叠, 行指针增加 ,修改pd_lower和pd_upper
读取: 顺序扫描和B树索引扫描;
顺序扫描:扫描行指针,依序读取页面tuple
B树索引扫描:
18: 索引扫描:
(避免不必要的页面扫描)
索引文件中包含着索引元组.
索引元组 index tuple: 由一个键值对组成。键:被索引的列值;值:目标堆元祖的TID.
Key: 'NAME' Value: TID。之后根据TID来读取对应的元组。
19:PG的进程架构:
PG是多进程架构,协同工作。
(1):pg服务器进程(PM): 所有子进程的父进程
(2):后端进程(backend process): pg进程,负责处理客户端发过来的查询和语句。通过单条tcp连接与客户端通信。一条连接只允许操作一个服务器上指定数据库。多个客户端同时连接的话,指定参数max_connections。
(3):后台进程(backgroud process): 负责执行数据库的各项任务,例如checkpoint(检查点处理)、autovacume(清理)、wal writer(将缓冲区wal数据持久化)、backgroud writer(将共享缓冲池的脏页逐渐刷入持久化存储中)
20: pg的内存架构(pg使用内存上下文来管理内存,避免内存泄漏等问题):
本地内存区域: 由后端进程分配,供自己使用
共享内存区域: PG主进程分配。例如 shared_buffer_pool: 将表和索引中的页面从持久化存储中加载到此中,进行读操作。
21: PG的并发控制
并发控制用来维护数据库一致性和隔离性的技术。
数据库事务正确执行的4个基本属性:ACID
【数据库】数据库四大特性ACID_IT猫咪酱的博客-CSDN博客
三种并发控制技术:
(1) 多版本并发控制MVCC
(2) 严格两阶段锁定S2PL
(3) 乐观并发控制OCC
二: 进阶概念:
1: MVCC:
多版本并发控制技术。有点:读写互不阻塞,事务会选择其中的一个版本进行事务的读取,确保各个事务的隔离性。MVCC: 每个写操作都会创建一个新版本的数据项,并保留其旧版本。
2: 快照隔离(Snapshot Isolation: SI):
MVCC的一种变体。
3: SQL标准里定义的三种异常:
(1): 脏读: 读取到了未提交的数据
(2): 不可重复读: 同一事务中,多次读到的数据不一致
(3):幻读: 读某一范围数据,前后读取的数据不一致
4: PG的事务隔离等级:
(1): 读已提交
(2):可重复读
(3):可串行化
5: 事务标识:
当事务开始时(begin), 事务管理器就会为事务分配一个唯一事务标识(txid), 之后真正启动事务。
#BEGIN;
#select txid txid_current(); // 查询当前事务的事务id
6: 0:无效事务id
1: initdb时候初始化事务id
2: 冻结的事务id
7: <txid:过去, 事务可见
>=txid: 当前 未来,事务不可见
8: 事务逻辑上无限,实际空间有限, 环形空间结构
9: 元组(tiple):
普通数据元组(Heap Tuple);
toast元组。
10: 普通元组数据结构:
11: 元组的增、删、改
12:空闲空间映射
插入堆表或者索引的元组时, PG使用表或者索引相应的FSM来选择可供插入的页面。
FSM都是以fsm后缀fsm存储。
13:CLOG:
提交日志,保存事务的状态。分配在共享内存中。
14:事务状态:
4种;
in_progress(事务运行中);commited(事务提交);aborted(事务异常终止);sub_commited(子事务相关).
15: 事务快照 snapshot
记录哪些事务是活跃的(acative: 正在进行中的,或者还未开始的事务。 不可见)哪些事务是不活跃的(inactive:已经提交的或者终止的事务。 可见)
#select txid_current_snapshot; // 查询当前事务的快照
16: 可见性规则
《postgresql指南--内幕探索》第五章并发控制(二)_postgresql 序列化异常_hmxz1024的博客-CSDN博客
需要用到:
(1): tuple中的t_xmin, t_xmax;
(2):clog:记录当前事务的状态
(3): 当前的snapshot
t_xmin的状态为ABORTED ⇒ Tuple is Invisible
t_xmin的状态为IN_PROGRESS ⇒ Tuple is Invisible (most) :
反之eg: If Status(t_xmin) = IN_PROGRESS && t_xmin = current_txid && t_xmax = INVAILD ⇒ Tuple is Visible 如果这个tuple是当前事务提交的,并tuple的t_xmax值是0,说明这个tuple是由当前事务插入并且并没有被修改过,所以,它是可见的。
t_xmin的状态为COMMITTED ⇒ Tuple is visible (most)
反之eg:
如果tuple被当前事务UPDATE或者DELETE了,自然这个tuple对于我们来说是旧版本了,不可见;
对于一个tuple,插入它的事务已经提交(COMMITED),并且该事务在当前的snapshot下是active的,说明该事务对当前事务中的命令来说是 in progress 或者 not yet started,故该事务插入的tuple对在当前为不可见;
17:详细的pg并发制技术,请见
(2条消息) 《postgresql指南--内幕探索》第五章并发控制(一)_hmxz2nn的博客-CSDN博客
《postgresql指南--内幕探索》第五章并发控制(二)_postgresql 序列化异常_hmxz1024的博客-CSDN博客
18:vacuum PG的维护清理过程
(0) :死元组:对于任何事务都不可见的元组,即delete/update操作过的元组(t_xmax被有效设置)。
(1): 对于DELETE和UPDATE比较多的的表,死元组可能占据很多磁盘空间.同时,死元组也将从索引中引用,进一步增加了浪费的磁盘空间量.这就是我们在PostgreSQL中称之为“膨胀”的东西,同时因为查询也会变慢。
(2): vacuum两个任务: 删除死元组;冻结事务标识
(3):vacuum清理过程的两种模式:
并发清理:删除表文件每个页面的死元组。其他事务在运行时可以读取该表。
完整清理:不会删除所有死元组,并且对所有活的元组记性碎片整理。其他事务执行时,无法读取该表。
(4):vacuum 清理过程涉及全表扫描,代价高昂。中引入了可见性空间映射( Visibility Map,VM)来提高移除死元组的效率。
19: vacuum并发清理流程
详细流程见:《postgresql指南--内幕探索》第六章 清理过程_hmxz1024的博客-CSDN博客
20: 自动清理守护进程 auto_vacuum
自动清理守护进程已经将清理过程自动化,因此 PostgreSQL 运维起来非常简单。自动清理守护程序周期性地唤起几个 autovacuum_worker 进程,默认情况下每分钟唤醒一次(由参数 autovacuum_naptime 定义),每次唤起三个工作进程(由 autovacuum_max_works 定义)。
自动清理守护进程唤起的 autovacuum 工作进程会依次对各个表执行并发清理,从而将对数据库活动的影响降至最低。
21: 完整清理(FULL VACUUM)
虽然并发清理对于运维至关重要,但光有它还不够。比如,即使删除了很多死元组,也无法压缩表大小的情况。死元组虽然都被移除了,但表的尺寸没有减小。这种情况既浪费了磁盘空间,又会对数据库性能产生负面影响。使用完整清理可以将压缩原表的大小。
(1).创建新表文件:
当对表执行VACUUM FULL命令时,PostgreSQL首先获取该表的AccessExclusiveLock锁,然后创建一个大小为8 KB的新表文件。AccessExclusiveLock锁不允许访问。
(2).将活动元组复制到新表中:
PostgreSQL仅将旧表文件中的活动元组复制到新表中。
(3).删除旧文件,重建索引,并更新统计信息,FSM和VM:
复制所有活动元组后,PostgreSQL删除旧文件,重建所有关联的表索引,更新该表的FSM和VM,并更新关联的统计信息和系统目录。
使用VACUUM FULL命令时应考虑两点:
- 在处理Full VACUUM时,没有人可以访问(读/写)该表。
- 最多临时使用表的磁盘空间的两倍;因此,在处理一个巨大的表时,有必要检查剩余的磁盘容量。
22: pg的缓冲区管理器:
(1)缓冲区管理器管理共享内存和持久存储之间的数据。对系统性能有重要影响。
(2)Pg的缓冲区管理器包括:缓冲表;缓冲区描述符;缓冲池;
23: buffer_tag(缓冲区标签): 数据页面的唯一标识。有三个值组成:
[1]:关系文件节点: 表空间oid;数据库oid;表oid
[2]:关系分支编号: 0:main文件 1:fsm 2:vm
[3]:页面号:3 : 3号页面
24: 缓冲区的结构
缓冲表:散列表(哈希表):散列表的每个数据项包括:buffer_tag和buffer_id。
缓冲区描述符:保存数据页面(槽)的元数据。和缓冲池的槽一一对应。是一个数组。
缓冲池:槽的大小为8KB(等于页面大小), 是一个数组。每一个槽存着一个数据文件(表或者索引)页。数组槽的索引称为buffer_id。
25: 后段进程如何读取数据页:
(1)向缓冲区管理器发送请求,请求中带有目标页面的buffer_tag
(2) 根据buffer_tag返回buffer_id, 即目标页面在缓冲池中的槽位。
如果该页面不在缓冲池中,那么管理器就会将页面从持久存储位置加载一个到缓冲池槽中,然后返回该槽的buffer_id
(3) 后端进程访问buffer_id对应的槽,读取页面
26:脏页:
后端进程修改缓冲池中的页面,例如向该页面插入元组,但是没有刷新到持久存储。这种页面被称为脏页。
27:脏页刷盘:
缓冲区管理器不会做这个事情,需要额外进程如:检查点进程checkpoint; 后台写入进程bg writer
28: 页面置换算法:
页面置换是?当所有缓冲区槽位都被占,但是还不包括申请访问的页面,此时需要将缓冲区的一个页面逐出去,放置所请求的页面。
常见算法:LRU, 时钟扫描算法。
29:脏页刷盘:
(1)检查点进程---->脏页刷盘。
将检查点记录写入WAL段文件,在检查点开始时进行脏页刷盘。
(2)后台写入进程---->脏页刷盘。
目的是通过少量多次的脏页刷盘,减少检查点带来的密集写入的影响。会一点点将脏页落盘,尽可能减少对数据库活动的影响。参数bg_writer_delay被唤醒去刷页。