1 前言
- PG可以用pageinspect方便的读取查看表文件。
- 本篇介绍一种用vim查看、编辑的方法,案例比较简单,主要分享原理。
修改表文件和controlfile是非常危险的行为,请不要在生产尝试。
2 用例
简化问题,用简单编码的数据类型。
sql
drop table t1;
create table t1 (a int, b int);
insert into t1 select t.i, t.i/10 from generate_series(1,1000) t(i);
analyze t1;
select relname,relfilenode,relpages from pg_class where relname = 't1';
relname | relfilenode | relpages
---------+-------------+----------
t1 | 49158 | 5
checkpoint;
select ctid, xmin, * from t1 order by 1 limit 10 offset 220;
ctid | xmin | a | b
---------+----------+-----+----
(0,221) | 50889007 | 221 | 22
(0,222) | 50889007 | 222 | 22
(0,223) | 50889007 | 223 | 22
(0,224) | 50889007 | 224 | 22
(0,225) | 50889007 | 225 | 22
(0,226) | 50889007 | 226 | 22
(1,1) | 50889007 | 227 | 22
(1,2) | 50889007 | 228 | 22
(1,3) | 50889007 | 229 | 22
(1,4) | 50889007 | 230 | 23
模拟页面损坏,编辑修改a=230 b=23
这一行的数据,把xmin编辑成错误的值,制造报错。
3 vim打开表文件找到页面位置
前面查询到表文件49158,使用vim编辑并转换为16进制显示:
转换后
每个页面8KB,一个页面的地址占用0x2000,所以一号页面的地址范围是0x2000 - 0x4000
(ctid得知我们需要修改的数据在1号页面)
4 根据地址定位元组位置
ctid = (1,4)
元组在1号页面的第4个位置,根据PG的页面布局,我们从页面尾部,向上找第四个元组
postgres=# select ctid, xmin, * from t1 where a = 230;
ctid | xmin | a | b
-------+----------+-----+----
(1,4) | 50889007 | 230 | 23
根据页面地址范围,1号页面的结尾在00004000处,向上找四个元组,因为这里元组都是定长的,可以按规律找四个即可。这里数据简单可以用这种方法。
如果数据类型比较复杂,长短不一,可根据每个元组头部的xmin确定位置。例如我们需要找的元组的xmin是50889007,看到下面第四行的元组头部为
2f81 0803
= 十六进制:0x308812f
= 十进制:50889007
等于xmin的值
所以可以根据xmin的值,从最后一个一个向上找。
5 如何解析表中的二进制数据?
拿到页面1的第四行数据:
00003f80: 2f81 0803 0000 0000 0000 0000 0000 0100
00003f90: 0400 0200 0009 1800 e600 0000 1700 0000
这段数据可以分成三段来看
第一部分:HeapTupleHeaderData
-----------------------------------
第二部分:nulls bitmap 不一定有
-----------------------------------
第三部分:具体数据值
头上一定是一个HeapTupleHeaderData,而HeapTupleHeaderData的第一个元素是一个uint32记录的xmin。
struct HeapTupleHeaderData
{
union
{
HeapTupleFields t_heap;
DatumTupleFields t_datum;
} t_choice;
ItemPointerData t_ctid; /* current TID of this or newer tuple (or a
uint16 t_infomask2; /* number of attributes + various flags */
uint16 t_infomask; /* various flag bits, see below */
uint8 t_hoff; /* sizeof header incl. bitmap, padding */
bits8 t_bits[FLEXIBLE_ARRAY_MEMBER]; /* bitmap of NULLs */
/* MORE DATA FOLLOWS AT END OF STRUCT */
};
typedef struct HeapTupleFields
{
TransactionId t_xmin; /* inserting xact ID */
TransactionId t_xmax; /* deleting or locking xact ID */
union
{
CommandId t_cid; /* inserting or deleting command ID, or both */
TransactionId t_xvac; /* old-style VACUUM FULL xact ID */
} t_field3;
} HeapTupleFields;
所以按顺序来解析,因为我这台机器是小端序的(lscpu可以看),所以低地址存低位的值,解析出来:
-
2f81 0803
=十六进制:0x308812f
=十进制:50889007
等于xmin的值。 -
0200
=十六进制:0x2
= t_infomask2 -
0009
=十六进制:0x900
= t_infomask -
e600 0000
=十六进制:0xe6
=十进制:230
= a列的值 -
1700 0000
=十六进制:0x17
=十进制:23
= b列的值00003f80: 2f81 0803 0000 0000 0000 0000 0000 0100
| | | |
|xmin(4B)| xmax(4B)| cid(4B) | t_ctid00003f90: 0400 0200 0009 1800 e600 0000 1700 0000
|mask2|mask|hoff| 这里没有null值数组,所以就是数据了。t_infomask = 0x900
t_infomask2 = 0x02
t_hoff = 0x18
结果和真实数据对得上:
postgres=# select ctid, xmin, * from t1 where a = 230;
ctid | xmin | a | b
-------+----------+-----+----
(1,4) | 50889007 | 230 | 23
6 vim修改表文件中的数据
先做个实验,把b列的值也改成230:
select ctid, xmin, * from t1 where a = 230;
ctid | xmin | a | b
-------+----------+-----+----
(1,4) | 50889007 | 230 | 23
使用vim编辑
转换回二进制:
wq保存退出。
重新查询b还是23,为什么呢?因为页面已经在shared_buffer中了,现在没有负载所以页面不会重新从磁盘读上来。
这里需要重启一下,注意不要kill -9,redo有可能会把数据盖掉,让数据库正常checkpoint后关闭,在启动: