【PGCCC】从 PostgreSQL 表恢复已删除的数据 | 翻译

发布于:2024-10-11 ⋅ 阅读:(57) ⋅ 点赞:(0)

PostgreSQL 非常擅长保护您的数据安全,因此它不会自行消失。不幸的是,反之亦然——如果数据已被删除,它将无法恢复。
在本文中,我们将探讨从 PostgreSQL 表中恢复已删除数据的选项。

01 谨慎的人使用事务

如果在处理数据时从不犯错误,就不会需要紧急程序。但由于我们都是人,像这样的事情偶尔会发生:

-- remove an entry from our contact list
db=> DELETE FROM addressbook WHERE name = 'Heinz Schmidt';
DELETE 3
-- PANIC:  WHY WERE THERE 3 ROWS AND NOT JUST ONE

养成的一个好习惯是始终使用事务,在确认一切正常后才提交它们。

-- remove an entry from our contact list
db=> BEGIN;
BEGIN
db=*> DELETE FROM addressbook WHERE name = 'Heinz Schmidt';
DELETE 3
-- NOTICE:  huh?
db=*> ROLLBACK;
ROLLBACK

使用事务后,未预期的行数变化让人感到轻松了许多。

02 谨慎的人有备份

如果您有可用的备份,您可以通过从备份中获取一些所需内容用于恢复数据处理的错误。

-- remove an entry from our contact list
db=> DELETE FROM addressbook WHERE name = 'Heinz Schmidt';
DELETE 3
-- WARNING:  oh no, not again
db=> SELECT now();
             now
-----------------------------
 2024-03-11 16:22:25.1679+01

现在,您可以指定一个Delete命令之前的时间戳来执行“时间点还原”。

03 迅速的人使用pg_dirtyread

假设选项1和2失败了,我们确实需要从正在运行的PostgreSQL实例中恢复数据。好消息是,DELETE 实际上并不删除数据,它只是将其标记为对后续事务不可见。这样做是为了允许并发事务仍然读取数据。只有当VACUUM(或autovacuum)清理表时,才会实际删除行。(对于对此机制感兴趣的人,可以参考PostgreSQL文档中的MVCC章节。)
在PostgreSQL中没有内置的方法来获取已删除但仍然存在的行,但我维护着一个名为pg_dirtyread 的PostgreSQL扩展允许这样做。

-- remove an entry from our contact list
db=> DELETE FROM addressbook WHERE name = 'Heinz Schmidt';
DELETE 3
-- NOTICE:  sigh
db=> SELECT * FROM addressbook;
        name         |     city
---------------------+-------------
 Christoph Berg      | Krefeld
 Hans-Jürgen Schönig | Wöllersdorf
(2 rows)
-- WARNING:  put on safety goggles now, we'll need superuser privileges

我们需要从一个包中安装 pg_dirtyread(或从源代码编译),然后在发生事故的数据库中创建这个扩展:

$ sudo apt install postgresql-16-dirtyread

db=# CREATE EXTENSION pg_dirtyread;
CREATE EXTENSION

该扩展提供了一个函数pg_dirtyread(‘tablename’) ,它像 PostgreSQL本身一样读取表,但忽略了任何行删除的标记。SQL要求我们在调用它时提供一个带有数据类型注释的列表,因此我们首先检查表的定义:

db=# \d addressbook 
           Table "public.addressbook"
 Column | Type | Collation | Nullable | Default 
--------+------+-----------+----------+---------
 name   | text |           |          | 
 city   | text |           |          | 

由此我们可以编译 pg_dirtyread 调用:

db=# SELECT * from pg_dirtyread('addressbook') addressbook(name text, city text);
        name         |     city
---------------------+-------------
 Christoph Berg      | Krefeld
 Heinz Schmidt       | Berlin
 Heinz Schmidt       | Wien
 Heinz Schmidt       | Basel
 Hans-Jürgen Schönig | Wöllersdorf
(5 rows)
-- NOTICE:  phew

我们的数据就在那里!我们可以将缺失的行复制到新表中,然后将其注入原始表中:


db=# CREATE TABLE addressbook_recover AS
     SELECT * from pg_dirtyread('addressbook') addressbook(name text, city text)
     WHERE name = 'Heinz Schmidt';
SELECT 3
db=# SELECT * from addressbook_recover ;
     name      |  city
---------------+--------
 Heinz Schmidt | Berlin
 Heinz Schmidt | Wien
 Heinz Schmidt | Basel
(3 rows)
db=# INSERT INTO addressbook SELECT * FROM addressbook_recover;
INSERT 3

只要VACUUM没有执行垃圾回收工作pg_dirtyread就可以正常工作。VACUUM由自动清理程序启动,针对那些至少变化了20%的表,每分钟执行一次。如果您的错误DELETE影响了更多的行(或者引起了累积膨胀超过该阈值),在这种情况下,在发生之前您只有不到60秒的时间关闭数据库并关闭自动清理。(请务必将 autovacuum=on 保留为数据库的默认设置。虽然关闭它会使恢复删除的行更容易,但如果表不定期清理膨胀,会发生不好的事情。)

04 绝望的人使用整页书写

如果pg_dirtyread太晚到达,因为行已经被垃圾收集了,仍然有希望。PostgreSQL在预写式日志(WAL)中跟踪所有更改。虽然这些更改记录只包含更改后的数据,但在每次操作每个页面(PostgreSQL在磁盘上处理数据的8KB单元)时,都会将整个页面的镜像写入WAL。这些完整页写入(FPW)可以被收集以提取已删除的行。
首先,我们需要一些关于在哪里查找的低级信息:


db=# select oid from pg_database where datname = current_database();
 oid
-----
   5

db=# select relfilenode from pg_class where relname = 'addressbook';
 relfilenode
-------------
      125616

db=# select pg_walfile_name(pg_current_wal_lsn());
     pg_walfile_name      
--------------------------
 000000010000000700000037

我们可以使用 pg_waldump 来解码 WAL:

$ /usr/lib/postgresql/16/bin/pg_waldump --relation=1663/5/125616 16/main/pg_wal/000000010000000700000037 | grep DELETE
rmgr: Heap        len (rec/tot):     59/   359, tx:       1894, lsn: 7/373798E0, prev 7/373798A8, desc: DELETE xmax: 1894, off: 2, infobits: [KEYS_UPDATED], flags: 0x00, blkref #0: rel 1663/5/125616 blk 0 FPW
rmgr: Heap        len (rec/tot):     54/    54, tx:       1894, lsn: 7/37379A48, prev 7/373798E0, desc: DELETE xmax: 1894, off: 3, infobits: [KEYS_UPDATED], flags: 0x00, blkref #0: rel 1663/5/125616 blk 0
rmgr: Heap        len (rec/tot):     54/    54, tx:       1894, lsn: 7/37379A80, prev 7/37379A48, desc: DELETE xmax: 1894, off: 4, infobits: [KEYS_UPDATED], flags: 0x00, blkref #0: rel 1663/5/125616 blk 0

我们可以看到我们的3个已删除行,第一个WAL记录被标记为包含FPW。使用PG16的pg_waldump,我们可以将FPW提取到一个文件中:

$ /usr/lib/postgresql/16/bin/pg_waldump --save-fullpage=fpw --relation=1663/5/125616 16/main/pg_wal/000000010000000700000037 | grep DELETE
rmgr: Heap        len (rec/tot):     59/   359, tx:       1894, lsn: 7/373798E0, prev 7/373798A8, desc: DELETE xmax: 1894, off: 2, infobits: [KEYS_UPDATED], flags: 0x00, blkref #0: rel 1663/5/125616 blk 0 FPW
rmgr: Heap        len (rec/tot):     54/    54, tx:       1894, lsn: 7/37379A48, prev 7/373798E0, desc: DELETE xmax: 1894, off: 3, infobits: [KEYS_UPDATED], flags: 0x00, blkref #0: rel 1663/5/125616 blk 0
rmgr: Heap        len (rec/tot):     54/    54, tx:       1894, lsn: 7/37379A80, prev 7/37379A48, desc: DELETE xmax: 1894, off: 4, infobits: [KEYS_UPDATED], flags: 0x00, blkref #0: rel 1663/5/125616 blk 0

它实际上提取了两个 FPW,但是查看 LSN,我们只对第一个感兴趣,所以我删除了第二个。

让我们通过创建新表并连接 FPW 文件来形成表内容,将其反馈给 PostgreSQL:

db=# create table addressbook_fpw (like addressbook);
CREATE TABLE
db=# select relfilenode from pg_class where relname = 'addressbook';
 relfilenode
-------------
      125628

$ sudo systemctl stop postgresql
$ cat fpw/* > base/5/125628
$ sudo systemctl start postgresql

由于 FPW 中的某些行已被标记为已删除,我们仍然必须使用 pg_dirtyread:

db=# SELECT * FROM pg_dirtyread('addressbook_fpw') addressbook(name text, city text);
        name         |    city
---------------------+-------------
 Christoph Berg      | Krefeld
 Heinz Schmidt       | Berlin
 Heinz Schmidt       | Wien
 Heinz Schmidt       | Basel
 Hans-Jürgen Schönig | Wöllersdorf
(5 Zeilen)

db=# INSERT into addressbook
     SELECT * FROM pg_dirtyread('addressbook_fpw') addressbook(name text, city text)
     WHERE name = 'Heinz Schmidt';
INSERT 3
-- NOTICE:  hopefully for the last time

这种方法很脆弱,最好在最后一次检查点之后没有其他命令影响表时运行。如果在上次清理后删除了其他行,它们也可能重新出现。如果您的PostgreSQL 版本早于16,需要将–save-fullpage开关功能进行回溯移植。

翻译工具:ChatGPT 4.0
原文作者:Christoph Berg
原文链接
https://www.cybertec-postgresql.com/en/recovering-deleted-data-from-postgresql-tables/
#PG证书#PG考试#postgresql初级#postgresql中级#postgresql高级


网站公告

今日签到

点亮在社区的每一天
去签到