Prerequisites
Of course, the majority of individuals do not want their databases to be harmed. These people will benefit from staying away from the strategies mentioned in this article. But some people might find it useful to corrupt a database on purpose, such as when testing a tool or process that will be used to find or fix corrupted data.
Prerequisites
We require a database with some data in it as well as active activity for several of our studies. The built-in PostgreSQL benchmark pgbench can be used for that. As a result of using a scale factor of 100, the largest table has 10 million rows:
|
1
2
3
4
5
6
7
8
|
$ pgbench -q -i -s 100dropping old tables...creating tables...generating data (client-side)...10000000 of 10000000 tuples (100%) done (elapsed 7.44 s, remaining 0.00 s)vacuuming...creating primary keys...done in 10.12 s (drop tables 0.18 s, create tables 0.01 s, client-side generate 7.52 s, vacuum 0.14 s, primary keys 2.28 s). |
Load will be generated with 5 concurrent client sessions:
|
1
|
$ pgbench -c 5 -T 3600 |
Creating a corrupt database by setting fsync = off
Let’s turn off the server while it is under load by setting fsync = off in postgresql.conf.
The AMCHEK plugin allows us to quickly identify data corruption.
|
1
2
3
4
5
6
7
|
postgres=# CREATEEXTENSION amcheck;CREATEEXTENSIONpostgres=# SELECTbt_index_parent_check('pgbench_accounts_pkey', TRUE, TRUE);WARNING: concurrent deleteinprogress within table"pgbench_accounts"ERROR: could notaccess status oftransaction1949706DETAIL: Could notreadfromfile "pg_subtrans/001D"atoffset 196608: readtoo few bytes.CONTEXT: while checking uniqueness oftuple (131074,45) inrelation "pgbench_accounts" |
What happened? Data were no longer flushed to disk in the correct order, so that data modifications could hit the disk before the WAL did. This leads to data corruption during crash recovery.
Creating a corrupt database from a backup
While pgbench is running, we create a base backup:
|
1
2
3
4
5
6
|
$ psqlpostgres=# SELECTpg_backup_start('test'); pg_backup_start ═════════════════ 1/47F8A130(1 row) |
The function to start backup mode is pg backup start() rather than pg start backup because I am using PostgreSQL v15. This is due to PostgreSQL v15’s removal of the exclusive backup API, which had been deprecated since PostgreSQL 9.6. Read my revised post on the linked page to learn more.
Let’s determine the database’s and pgbench accounts’ primary key index object IDs:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
postgres=# SELECTrelfilenode FROMpg_class WHERErelname = 'pgbench_accounts_pkey'; relfilenode ═════════════ 16430(1 row)postgres=# SELECToid FROMpg_database WHEREdatname = 'postgres'; oid ═════ 5(1 row) |
We copy the data directory to build a backup. Then, to make sure they are more current than the others, we duplicate the pgbench accounts primary key index and commit log:
|
1
2
3
4
|
$ cp-r data backup$ cpdata/base/5/16430* backup/base/5$ cpdata/pg_xact/* backup/pg_xact/$ rmbackup/postmaster.pid |
The crucial part: do not create backup_label
Now we exit backup mode, but ignore the contents of the backup_label file returned from pg_backup_stop():
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
postgres=# SELECTlabelfile FROMpg_backup_stop();NOTICE: WAL archiving isnotenabled; you must ensure that allrequired WAL segments are copied through other means tocomplete the backup labelfile ════════════════════════════════════════════════════════════════ START WAL LOCATION: 1/47F8A130 (file 000000010000000100000047)↵ CHECKPOINTLOCATION: 1/65CD24F0 ↵ BACKUP METHOD: streamed ↵ BACKUP FROM: primary ↵ START TIME: 2022-07-05 08:32:47 CEST ↵ LABEL: test ↵ START TIMELINE: 1 ↵ (1 row) |
Then, let’s make sure that the last checkpoint in the control file is different:
|
1
2
3
|
$ pg_controldata -D backup | grepREDOLatest checkpoint's REDO location: 1/890077D0Latest checkpoint's REDO WAL file: 000000010000000100000089 |
Great! Let’s start the server:
|
1
2
3
4
|
$ echo'port = 5555'>> backup/postgresql.auto.conf$ pg_ctl -D backup startwaiting forserver to start..... doneserver started |
Now an index scan on pgbench_accounts fails, because the index contains more recent data than the table:
postgres=# SELECT * FROM pgbench_accounts ORDER BY aid; ERROR: could not read block 166818 in file "base/5/16422.1": read only 0 of 8192 bytes
What took place? We recovered from the incorrect checkpoint because the backup label file was left out, and as a result, the data in the table and its index were no longer consistent. I merely wanted to underline the significance of the backup label; keep in mind that we can achieve the same result without pg backup start() and pg backup stop().
corrupting a database using pg resetwal
We crash the database while pgbench is loading it.
|
1
|
pg_ctl stop -m immediate -D data |
Then we run pg_resetwal:
|
1
2
3
4
5
6
|
pg_resetwal -D dataThe database server was not shut down cleanly.Resetting the write-ahead log might cause data to be lost.If you want to proceed anyway, use -f to force reset.$ pg_resetwal -f -D dataWrite-ahead log reset |
Then we start the server and use amcheck like before to check the index for integrity:
|
1
2
3
4
5
6
7
|
postgres=# CREATEEXTENSION amcheck;CREATEEXTENSIONpostgres=# SELECTbt_index_parent_check('pgbench_accounts_pkey', TRUE, TRUE);WARNING: concurrent deleteinprogress within table"pgbench_accounts"ERROR: could notaccess status oftransaction51959DETAIL: Could notreadfromfile "pg_subtrans/0000"atoffset 204800: readtoo few bytes.CONTEXT: while checking uniqueness oftuple (1,1) inrelation "pgbench_accounts" |
What happened? pg_resetwal is only safe to use on a cluster that was shutdown cleanly. The option -fis intended as a last-ditch effort to get a corrupted server to start and salvage some data. Only experts should use it.
Creating a corrupt database with pg_upgrade --link
We create a second cluster with initdb:
|
1
|
$ initdb -E UTF8 --locale=C -U postgres data2 |
Then we edit postgresql.conf and choose a different port number. After shutting down the original cluster, we run an “upgrade” in link mode:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
$ pg_upgrade -d /home/laurenz/data-D /home/laurenz/data2\> -b /usr/pgsql-15/bin-B /usr/pgsql-15/bin-U postgres --linkPerforming Consistency Checks...Performing Upgrade...Adding ".old"suffix to old global/pg_control okIf you want to start the old cluster, you will need to removethe ".old"suffix from /home/laurenz/data/global/pg_control.old.Because "link"mode was used, the old cluster cannot be safelystarted once the new cluster has been started....Upgrade Complete----------------Optimizer statistics are not transferred by pg_upgrade.Once you start the new server, consider running: /usr/pgsql-15/bin/vacuumdb-U postgres --all --analyze-in-stagesRunning this script will delete the old cluster's data files: ./delete_old_cluster.sh |
pg_upgrade renamed the control file of the old cluster, so that it cannot get started by accident. We’ll undo that:
|
1
2
|
mv/home/laurenz/data/global/pg_control.old \> /home/laurenz/data/global/pg_control |
Now we can start both clusters and run pgbench on both. Soon we will see error messages like
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
ERROR: unexpected data beyond EOF in block 1 of relation base/5/16397HINT: This has been seen to occur with buggy kernels; consider updating your system.ERROR: duplicate key value violates unique constraint "pgbench_accounts_pkey"DETAIL: Key (aid)=(8040446) already exists.WARNING: could not write block 13 of base/5/16404DETAIL: Multiple failures --- write error might be permanent.ERROR: xlog flush request 0/98AEE3E0 is not satisfied --- flushed only to 0/648CDC58CONTEXT: writing block 13 of relation base/5/16404ERROR: could not access status of transaction 39798DETAIL: Could not read from file "pg_subtrans/0000" at offset 155648: read too few bytes. |
What happened? Since both clusters share the same data files, we managed to start two servers on the same data files. This leads to data corruption.
Creating a corrupt database by manipulating data files
For that, we figure out the file name that belongs to the table pgbench_accounts:
|
1
2
3
4
5
6
|
postgres=# SELECTrelfilenode FROMpg_class WHERErelname = 'pgbench_accounts'; relfilenode ═════════════ 16396(1 row) |
Now we stop the server and write some garbage into the first data block:
|
1
2
3
4
|
yes'this is garbage'| ddof=data/base/5/16396bs=1024 seek=2 count=1 conv=notrunc0+1 records in0+1 records out1024 bytes (1.0 kB, 1.0 KiB) copied, 0.00031255 s, 3.3 MB/s |
Then we start the server and try to select from the table:
|
1
2
|
postgres=# TABLEpgbench_accounts ;ERROR: compressed pglz data iscorrupt |
What happened? We tampered with the data files, so it’s unsurprising that the table is corrupted.
Creating a corrupt database with catalog modifications
Who needs ALTER TABLE to drop a table column? We can simply run
|
1
2
3
|
DELETEFROMpg_attributeWHEREattrelid = 'pgbench_accounts'::regclass ANDattname = 'bid'; |
After that, an attempt to query the table will result in an error:
|
1
|
ERROR: pg_attribute catalog is missing 1 attribute(s) for relation OID 16396 |
What took place? We disregarded the fact that deleting a column in the pg attribute really removes the entry instead of setting the drop attribute to TRUE. Additionally, we failed to lock the table against concurrent access and failed to correctly check for dependencies in pg depend. It is not recommended to modify catalog tables because you get to keep both parts of the database if it fails.
Conclusion
There are numerous ways to corrupt a PostgreSQL database, as we have seen. Some of these were evident, but others could surprise a novice. A damaged database is not something you want.
- Keep track of the system catalogs.
- Don’t ever make changes to the data directory (with the exception of configuration files).
- Run without FSYNC turned off.
- Avoid calling pg resetwal -f on a wrecked server; instead, use pg upgrade —link to delete the old cluster following an upgrade.
- Never remove or forget the backup label.
- To avoid known software flaws, run PostgreSQL in a known version on dependable hardware.
With this information, hopefully some databases can be saved! Read my post on join strategies if you want to learn more about PostgreSQL performance problems.
About Enteros
Enteros offers a patented database performance management SaaS platform. It proactively identifies root causes of complex business-impacting database scalability and performance issues across a growing number of clouds, RDBMS, NoSQL, and machine learning database platforms.
The views expressed on this blog are those of the author and do not necessarily reflect the opinions of Enteros Inc. This blog may contain links to the content of third-party sites. By providing such links, Enteros Inc. does not adopt, guarantee, approve, or endorse the information, views, or products available on such sites.
Are you interested in writing for Enteros’ Blog? Please send us a pitch!
RELATED POSTS
Empowering the Cloud Center of Excellence: How Enteros Uses Generative AI for Real-Time Monitoring and Performance Optimization in the Technology Sector
- 4 November 2025
- Database Performance Management
Introduction In the era of digital transformation, the technology sector stands at the forefront of innovation, harnessing cloud computing, artificial intelligence, and big data to drive performance and efficiency. However, as cloud infrastructures scale in size and complexity, managing performance, resource allocation, and cost optimization becomes increasingly challenging. Enter the Cloud Center of Excellence (CCoE) … Continue reading “Empowering the Cloud Center of Excellence: How Enteros Uses Generative AI for Real-Time Monitoring and Performance Optimization in the Technology Sector”
AI SQL Meets Healthcare Innovation: Enteros’ Breakthrough in Database Performance Optimization
Introduction In the modern healthcare landscape, data has become both a vital asset and a formidable challenge. Hospitals, research institutions, and digital health startups generate and process massive amounts of data—from patient records and clinical trial results to real-time monitoring devices and medical imaging. Yet, the performance of these complex data ecosystems often determines how … Continue reading “AI SQL Meets Healthcare Innovation: Enteros’ Breakthrough in Database Performance Optimization”
Redefining Insurance RevOps: Enteros’ AIOps-Powered Approach to Smarter Database Performance Management
- 3 November 2025
- Database Performance Management
Introduction The insurance industry has always been data-intensive—dealing with massive volumes of customer information, risk assessments, policy records, and claims data. But in today’s fast-evolving digital landscape, traditional data management models no longer suffice. The sector now faces unprecedented demands for real-time insights, operational agility, and cost efficiency. Modern insurers must process data from a … Continue reading “Redefining Insurance RevOps: Enteros’ AIOps-Powered Approach to Smarter Database Performance Management”
Enteros for Energy Leaders: Redefining Performance Optimization Through Generative AI and Cloud FinOps Intelligence
Introduction The global energy sector is undergoing a monumental transformation. As the demand for clean, efficient, and sustainable energy solutions grows, so too does the complexity of managing massive data flows across power grids, refineries, renewables, and smart infrastructure. Energy companies are no longer just producers and distributors—they’re becoming data-driven enterprises, leveraging cloud computing, AI, … Continue reading “Enteros for Energy Leaders: Redefining Performance Optimization Through Generative AI and Cloud FinOps Intelligence”