PostgreSQL 空闲空间映射(FSM)深度解读
PostgreSQL 空闲空间映射(FSM)深度解读
PostgreSQL 数据库是以文件的方式储存数据,文件会存在DATABASE下,而这些文件一般包括表和索引(除了哈希索引)的数据以及相关的属性信息,存这些信息都有一个空闲空间映射(FSM)来保持对关系中可用空间的跟踪中,当创建表并且插入数据后,在autovacuum参数开启时并且触发vacuum后,或手动执行vacuum后,可以通过pg_relation_filepath系统表查看表的filepath保存的路径,并找到对应的该表对应的FSM文件,而FSM(Free Space map)是什么,有什么作用呢 ?
1.在PostgreSQL 中 FSM(Free Space Map,空闲空间映射)文件是用于跟踪表和索引中数据块空闲空间的关键结构。当插入新数据时PG需要找到表中或者索引中存在足够的空闲空间的数据块(默认的为8KB,wal_block_size参数查看)来保存数据,而FSM文件则记录了每个数据块的空闲空间量,数据库在插入新数据时,可以快的定位合适的快进行插入,可以减少磁盘IO的读取次数从而提高插入的效率。而当表的数据更新或者删除时便会产生数据块的空闲空间,FSM也会追踪这些空间用于后续插入的数据能够进行复用,从而避免频繁扩展新的数据块,减少硬件磁盘的碎片,提高查询的效率。使用PG的vacuum命令可以释放数据块内的空闲空间。
2.FSM 是使用1字节储存表的页面的内的空闲空间,储存的1字节是状态,范围是0-255共256种状态,计算的方式是page大小除以256,就能得到256个值,该值被称为CAT的值,CAT 值与FSM的关系映射如下:
[外链图片转存中…(img-c29O9r0D-1761566540654)]
3.当我们频繁的插入数据,频繁的删除和更新数据,FSM的状态的更新会比较频繁,同时伴随着vacuum的执行,需要高效的处理FSM的空闲空间映射,PG采用了FSM High Level(FSM 高层结构)的技术来高效的管理和查询数据块的空闲空间信息,从而被复用,FSM High Level内部采用的是二叉树结构,二叉树分为三层结构,结构的顺序为Level 2 、Level 1 、Level0,Level 2 是最顶层并且仅有一个页面,所以逻辑页号为0,可以表示为<2,0>,最多具有4000个Level1 的孩子节点,并且孩子节点的逻辑页号为<1,0>…<1,3999> 。因为每个页面都具有4000个孩子节点,所以Level 0 层保存的就会有 4000 * 4000 的页面节点,逻辑页号为<0,1>…<0,4000*4000>。
4.物理页号的由来为FSM的文件的大小除以页的大小(wal_block_size查看。默认为8K),得到的FSM的总页数,最大的物理页数显示未"总页数-1",例如12345_fsm 文件大小为 32768 字节(32KB),则总页数为 32768 / 8192 = 4,物理页号范围是 0~3。
5.PostgreSQL 在插入数据时,会先判断FSM中的空闲空间映射的关系,通过FSM 的High Level 树形索引加快查询的速度,因为每层都有相对应的索引,再加上High Level的体积较小,很容易倍内存缓存,在保存数据时也会最大的剪枝掉空闲空间不足的分支,仅保留候选节点来加快数据的储存速度。
代码案例:
fsm_vm_case=# create table fsm_table(id int,name varchar(20));
CREATE TABLE
fsm_vm_case=# select pg_relation_filepath('fsm_table');pg_relation_filepath
----------------------base/16415/16419
(1 row)-- 仅创建表后没有插入数据,没有FSM文件
fsm_vm_case=# \! du -sh /pgcc/pgdata16/base/16415/16419*
0 /pgcc/pgdata16/base/16415/16419-- 插入一条数据后在没有执行vacuum时没有FSM相关的文件
fsm_vm_case=# insert into fsm_table values(1,'fmstest');
INSERT 0 1
fsm_vm_case=# \! du -sh /pgcc/pgdata16/base/16415/16419*
8.0K /pgcc/pgdata16/base/16415/16419-- 手动执行vacuum后会出现FSM文件和vm文件
fsm_vm_case=# vacuum fsm_table;
VACUUM
fsm_vm_case=# \! du -sh /pgcc/pgdata16/base/16415/16419*
8.0K /pgcc/pgdata16/base/16415/16419
24K /pgcc/pgdata16/base/16415/16419_fsm
8.0K /pgcc/pgdata16/base/16415/16419_vm-- 查看FSM的信息需要使用到freespacemap插件
postgres=# create extension pg_freespacemap;
CREATE EXTENSION-- 在一行数据的情况下查看FSM的保存的信息
fsm_vm_case=# select * from fsm_table;id | name
----+---------1 | fmstest
(1 row)
-- 返回的blkno和avail
fsm_vm_case=# select * from pg_freespace('fsm_table');blkno | avail
-------+-------0 | 8096
(1 row)-- 当插入到2万行数据后,执行vacuum后显示,blkno表示使用了178个物理块,avail表示178个物理块还有1216的字节用于插入数据。
fsm_vm_case=# insert into fsm_table select random()*10,(random()*10)::int from generate_series(1,20000);
INSERT 0 20000
fsm_vm_case=# vacuum analyze fsm_table ;
VACUUM
fsm_vm_case=# select * from pg_freespace('fsm_table') where avail > 0;blkno | avail
-------+-------178 | 1216
(1 row)-- 再次插入了20条数据,执行vacuum 后,发现在178的物理块中仅剩480字节的可用空间了。
fsm_vm_case=# insert into fsm_table select random()*10,(random()*10)::int from generate_series(1,20);
INSERT 0 20
fsm_vm_case=# vacuum analyze fsm_table ;
VACUUM
fsm_vm_case=# select * from pg_freespace('fsm_table') where avail > 0;blkno | avail
-------+-------178 | 480
(1 row)-- 使用表的大小除以物理块的大小约等于8192(因为178物理块没有插入满所以有偏差)
fsm_vm_case=# \dt+ fsm_tableList of relationsSchema | Name | Type | Owner | Persistence | Access method | Size | Description
--------+-----------+-------+----------+-------------+---------------+---------+-------------public | fsm_table | table | postgres | permanent | heap | 1464 kB |
(1 row)fsm_vm_case=# select 1464 / 178.0;?column?
--------------------8.2247191011235955
(1 row)-- 使用pgstattuple插件可以详细的观察表或索引的空间使用细节
fsm_vm_case=# create extension pgstattuple;
CREATE EXTENSION
fsm_vm_case=# select * from pgstattuple('fsm_table');
-[ RECORD 1 ]------+--------
table_len | 1466368
tuple_count | 40441
tuple_len | 1215234
tuple_percent | 82.87
dead_tuple_count | 0
dead_tuple_len | 0
dead_tuple_percent | 0
free_space | 5472
free_percent | 0.37fsm_vm_case=# select count(*) from fsm_table;
-[ RECORD 1 ]
count | 40441