This guide covers recovering the original data before accidental UPDATE operations from a WAL archive or a preserved copy of PGDATA/pg_wal. PDU extracts the pre-update values so incorrectly modified data can be restored.
First query PostgreSQL for the actual data directory and the complete paths of the target table and its TOAST relation. The result can be used directly for file copying:
SHOW data_directory;SELECT n.nspname, c.relname, c.oid, c.relfilenode, current_setting('data_directory') || '/' || pg_relation_filepath(c.oid) AS data_file_path, c.reltoastrelid AS toast_oid, CASE WHEN c.reltoastrelid = 0 THEN NULL ELSE current_setting('data_directory') || '/' || pg_relation_filepath(c.reltoastrelid) END AS toast_file_pathFROM pg_class cJOIN pg_namespace n ON n.oid = c.relnamespaceWHERE n.nspname = 'public' AND c.relname = 't_user_profiles';
If WAL archiving is enabled, force the WAL containing the incident into the configured archive directory:
SELECT pg_switch_wal();CHECKPOINT;
If WAL archiving was not enabled, use the directory returned by SHOW data_directory and immediately copy all files under $PGDATA/pg_wal to a separate directory. If pg_wal is a symbolic link, copy files from its real target:
mkdir -p /data/pdu-backup/incident-20260803/pg_walcp -a /home/pg/data/pg_wal/. /data/pdu-backup/incident-20260803/pg_wal/
Set PGDATA to the value returned by SHOW data_directory. Set ARCHIVE_DEST to the existing archive directory, or to the copied pg_wal directory when archiving was not enabled:
[root@node1 pdu]# cat pdu.ini#PostgreSQL Data DirectoryPGDATA=/home/pg/data/#WAL source directory: archive directory or copied pg_wal directoryARCHIVE_DEST=/data/pdu-backup/incident-20260803/pg_wal#Disk for dropScan to scanDISK_PATH=#Number of data blocks to skip during dropScanBLOCK_INTERVAL=20
PDU.public=# use mydb;────────────────────────────────────────│ Schema │ Tab Num │────────────────────────────────────────│ public │ 1071 │────────────────────────────────────────mydb.public=#
The scan command works the same way, but now scans for UPDATE operations instead of DELETE:
mydb.public=# scan t_user_profiles;Scanning updated Records for table<t_user_profiles>...▌ Scanning Archived Wal Directory StartWal: 000000010000000500000010 EndWal: 00000001000000050000002F▌ Time Range Restore Mode [Displayed all within Time Range]────────────────────────────────────────▌ End of Scanning, current time range: Start: 2025-03-10 14:22:15.123456 EST End: 2025-03-10 14:35:42.789012 EST▌ Time Range Details──────────────────────────────────────── Start Time: 2025-03-10 14:22:15.123456 EST End Time: 2025-03-10 14:35:42.789012 EST LSN: 5/10A23450 - 5/2F8B1230 Recommended startwal: 000000010000000500000010 Recommended endwal: 00000001000000050000002F -------------------.-------------------- ● Datafile OID: 156789 ● Toastfile OID: 0 ● Records updated in the Time Range: 2500────────────────────────────────────────Execution Time 15.32 seconds