https://www.percona.com/wp-content/uploads/2026/10/innodump1.jpg
Probably everyone responsible for managing database servers at some point realizes that it would be helpful to have a tool capable of extracting data directly from raw database files. Something completely offline, without the need to have a running database server, and that would work with data directories, files, or binary backups (storage volume snapshots, XtraBackup, etc).
There are a couple of use cases for that I could think of:
- Crashed/corrupted data recovery – if the database service refuses to start or enters a crash-restart loop and there are no good backups (PITR!) available.
- Backup (additional) verification and data files health check.
- Extraction of individual databases or tables from a full backup.
- Quick provisioning of staging/QA instances directly from live production.
- Recovery of accidentally deleted rows.
- Forensics (i.e., quick seek for data across many old backups without the need to reload them).
- Import/migrate data from very old/legacy database versions.
Backups!
Here, let me drop an obvious yet often neglected statement: nothing will replace good backups! Your database infrastructure should always be planned with the assumption that your data may get damaged or lost at any time. Visit here if you want to check if your current backup policy is up to date: https://www.percona.com/blog/mysql-backup-and-recovery-best-practices/
Now, would an offline tool with the ability to read SQL data from files be another backup method? NO! Modern ACID- and MVCC-compliant data engines are too complex in terms of data durability and consistency. So, reading directly from database files does not guarantee the dump would be consistent. Dedicated backup tools like XtraBackup engage additional steps to ensure consistency, like following the redo log and using backup locks.
Corruption and Crash Recovery
Typically, when MySQL / MariaDB server detects severely corrupted data, it may need to perform an emergency shutdown with the intention of preventing further damage. A lot depends on the scope of the damage. Some minor issues with secondary indexes could often be fixed by re-creating the index, but clustered key corruption often leads to an engine crash whenever the corrupted page is accessed. You may find yourself in a situation similar to these reports:
https://bugs.mysql.com/bug.php?id=111610
https://bugs.mysql.com/bug.php?id=113706
Again, with good backups, this should be no problem. But a bad surprise may force you to try to read all healthy rows in the hope of restoring as much as possible. The task will be, in most cases, very tedious – an attempt to read a row sitting in a corrupted page would crash the server, so you have to wait for a restart before you can try reading the next row.
This was one of the reasons for me to try to come up with a tool fully capable of extracting rows without needing to run MySQL. Otherwise, a challenging project was affordable using vibe-coding, where the agent could build data decoding by reading the relevant engine’s source code. So, let me introduce a new open-source project: innodump! Up to this point, I have implemented all three most popular MySQL and MariaDB engines: InnoDB, MyISAM, and MyRocks (RocksDB). The tool can read and dump the whole data directory, or specific tables, either from a common or general tablespace or from a per-table file. It decodes both the table schema and rows into SQL files or a TSV structure compatible with MySQL Shell dump utilities. The latter allows you to load the dump into another instance very efficiently.
There is an option relevant from the corruption point of view:
--skip-corrupted – in case a corrupted data page is encountered, it prints a comment and skips to the next page, effectively reading as much data as possible.
Example run:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
$ innodump —file innodump_work/instant_t.ibd —verbose —skip–corrupted File: innodump_work/instant_t.ibd (130023424 bytes) Space ID: 409 Pages: 7936 Page size: 16384 bytes FSP flags: 0x00004021 (post_antelope=true atomic_blobs=true data_dir=false shared=false temporary=false encryption=false sdi=true) SDI: present (written by MySQL 8.0.33, dd_version 80023, sdi_version 80019) Tables in SDI: 1 Row format: DYNAMIC Clustered index: "PRIMARY" (id 943), root page 4 Secondary indexes: 5 Physical fields: 31 (29 output) === innodump 0.8.1 === Table: db1.instant_t (row_format=DYNAMIC) Columns: 29 output (31 total incl. system/hidden) warning: corrupted page 100 (index "PRIMARY", id 943): LSN low bytes at the start and end of the page disagree Schema file: sqldump_2026–10–21_16.42/db1.instant_t–schema.sql Data file: sqldump_2026–10–21_16.42/db1.instant_t–data.sql (100680 row(s) written, 1 corrupted page(s) skipped – see warnings above) |
Data Verification
--examine – this option makes the tool read every table and decode every row without storing them; it produces a report of how many rows can be found and how many of them are corrupted.
Note: the tool scans only the clustered (primary) index, so it will not find possible secondary index corruptions; it is expected to only ensure which rows can be restored.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
$ innodump —examine —source–dir /data/sandboxes/msb_11_6_2/data/ === innodump 0.8.1 === Following /data/sandboxes/msb_11_6_2/data/ recursively — recognized as a single instance‘s datadir (found ibdata1), so every InnoDB table found inside (file-per-table .ibd files, plus any table found only in ibdata1’s own shared tablespace) and every MyISAM table (.MYD files), plus every MyRocks table (read from #rocksdb) will be examined (nothing is written). Source: /data/sandboxes/msb_11_6_2/data/ (6 .ibd file(s), 129 .frm–only table(s) to resolve via ibdata1, a MyRocks data directory (#rocksdb) found) db1.t0 InnoDB 8 row(s) OK [row_format=COMPACT] db1.t1 InnoDB 5 row(s) OK [row_format=COMPACT] mysql.gtid_slave_pos InnoDB 0 row(s) OK [row_format=COMPACT] mysql.innodb_index_stats InnoDB 13 row(s) OK [row_format=COMPACT] mysql.innodb_table_stats InnoDB 3 row(s) OK [row_format=COMPACT] mysql.transaction_registry InnoDB 0 row(s) OK [row_format=COMPACT] db1.sbtest1 ROCKSDB 1000000 row(s) OK
Summary InnoDB 6 table(s), 29 row(s) ROCKSDB 1 table(s), 1000000 row(s) total 7 table(s), 1000029 row(s) Note: 129 .frm file(s) with no matching .ibd had no InnoDB entry in ibdata1‘s own dictionary either – likely non–InnoDB tables, VIEWs, or a general tablespace – not examined Result: every table read cleanly |
If you are looking for a way to scan InnoDB files for potential corruption, you should always start with the official innochecksum. With innodump’s examination, the main goal is to show what’s actually readable.
Staging/dev/QA Data Provisioned the Easy and Unintrusive Way?
Quite often, a DBA may be tasked with setting up a non-production MySQL or MariaDB instance having a recent data snapshot from production. And typically, this kind of data copy for staging purposes does not have to be point-in-time consistent. And note that using this tool on a live instance or on an unprepared backup will mean various data consistency issues, like dirty pages may not be on disk yet, pages can be mid-write, it can produce rows that are missing, stale, or wrongly flagged, and even whole tables can be missing if created recently enough inside a common space.
Now, if the last regular available backup is up to date enough, it may still be too costly to restore the full backup while only a handful of databases are needed. Here, innodump can dump from specific database folders or files:
|
$ innodump —source–dir /data/sandboxes/msb_9_7_0/data/db2/ —format tsv —compression zstd === innodump 0.8.1 === Following /data/sandboxes/msb_9_7_0/data/db2/ recursively — recognized as a single database directory (no subdirectories of its own), so every InnoDB table found inside (file–per–table .ibd files, plus any table found only in ibdata1‘s own shared tablespace) and every MyISAM table (.MYD files) will be dumped. Source: /data/sandboxes/msb_9_7_0/data/db2/ (5 .ibd file(s) found) db2.categories: 10000 row(s) db2.cities: 100000 row(s) db2.products: 100000 row(s) db2.sales: 1049877 row(s) db2.stores: 100000 row(s) Files: 5/5 .ibd file(s) processed Tables: 5 extracted Rows: 1359877 written Output: tsvdump_2026–10–21_16.58 |
The exported folder can use the format compatible with fast MySQL Shell import:
|
$ ls tsvdump_2026–10–21_16.58/ db2@categories.json db2@categories.tsv.zst db2@cities.sql db2.json db2@products.sql db2@sales.json db2@sales.tsv.zst db2@stores.json db2@stores.tsv.zst @.json db2@categories.sql db2@cities.json db2@cities.tsv.zst db2@products.json db2@products.tsv.zst db2@sales.sql db2.sql db2@stores.sql @.done.json |
What if no recent backup is available but there is a need to fetch staging data quickly from production? Bearing in mind that reading from a live datadir has the caveats above, which is fine for a throwaway staging copy, the tool can do it gently, as it allows you to limit the IO throughput during exporting. Moreover, it can bypass the OS page cache via the O_DIRECT flag, so as not to trash the server’s cache with garbage and avoid causing additional memory pressure (the kernel might decide to swap out some least recently used user space memory to make room for page cache!). Therefore, to make the export from production with as little impact as possible, I recommend using --io-limit (RATE bytes per second) and --direct-io options, plus being gentle on the CPU side, running the tool with nice.
Example run results without these options used:
|
$ time innodump —file /data/sandboxes/msb_9_7_0/data/tpcdh/orders.ibd —format tsv —compression zstd === innodump 0.8.1 === Table: tpcdh.orders (row_format=DYNAMIC) Columns: 9 output (11 total incl. system/hidden) Dump dir: tsvdump_2026–10–21_17.25 (MySQL Shell util.loadDump()–compatible, zstd level 1) Rows: 1500000 written
real 0m1.726s user 0m6.601s sys 0m0.362s
$ fincore /data/sandboxes/msb_9_7_0/data/tpcdh/orders.ibd RES PAGES SIZE FILE 192.1M 49168 228M /data/sandboxes/msb_9_7_0/data/tpcdh/orders.ibd |
And with these safety settings (time longer bc io limit, and page cache is skipped bc direct io):
|
$ time innodump —file /data/sandboxes/msb_9_7_0/data/tpcdh/orders.ibd —format tsv —compression zstd —direct–io —io–limit 30M === innodump 0.8.1 === Table: tpcdh.orders (row_format=DYNAMIC) Columns: 9 output (11 total incl. system/hidden) Dump dir: tsvdump_2026–10–21_17.28 (MySQL Shell util.loadDump()–compatible, zstd level 1) Rows: 1500000 written
real 0m9.241s user 0m8.024s sys 0m0.486s
$ fincore /data/sandboxes/msb_9_7_0/data/tpcdh/orders.ibd RES PAGES SIZE FILE 0B 0 228M /data/sandboxes/msb_9_7_0/data/tpcdh/orders.ibd |
Recover Deleted Rows?
InnoDB does not immediately purge or overwrite deleted rows in the table spaces. The record is first only delete-marked and stays in the page. If no new writes have actually reclaimed the space yet, such rows usually can be decoded. So, if done quickly, there is a good chance you can restore those rows, and innodump makes it easy via the --deleted-only option!
|
$ innodump —file /data/sandboxes/msb_8_4_3/data/sbtest/sbtest1.ibd —deleted–only === innodump 0.8.1 === Table: sbtest.sbtest1 (row_format=DYNAMIC) Columns: 4 output (6 total incl. system/hidden) Schema file: sqldump_2026–10–21_17.46/sbtest.sbtest1–schema.sql Data file: sqldump_2026–10–21_17.46/sbtest.sbtest1–data–deleted.sql (2100 delete–marked row(s) written) |
I hope you may find the tool handy for the cases mentioned above as well as possibly others, like extracting data from some ancient/legacy database versions, where the regular backup tools don’t even work.
Note: it is not an officially supported Percona software, but rather an experimental personal project.
The tool can be found on GitHub, and all feedback is welcome!
https://github.com/PrzemekMalkowski/innodump
The article was created by a human.
Planet for the MySQL Community



