To recover a dropped or corrupt table with Percona Data Recovery Tool for InnoDB you need two things: media with records(ibdata1, *.ibd, disk image, etc.) and a table structure. Indeed, there is no information about the table structure in an InnoDB page. Normally we either recover the structure from .frm files or take it from some old backup.
A new tool sys_parser
can recover the table structure from InnoDB dictionary.
Why do we need a new tool anyway? It is absolutely critical to have an accurate table definition to ensure a successful recovery. Even an unnoticeable difference like NULL or NOT NULL can shift all values by a byte and thus will spoil the result. That’s why I prefer the structure from .frm files over taken from backups. But in some cases even .frm files is not an option:
- Table was dropped and innodb_file_per_table is ON
- Frm file corrupt, zeroed out, lost or SHOW CREATE TABLE crashes MySQL
There is yet another source of information about the table structure – InnoDB dictionary. Let’s review tables from the dictionary and see what it can give us. We will need four of them:
- SYS_TABLES
- SYS_INDEXES
- SYS_COLUMNS
- SYS_FIELDS
SYS_TABLES
Here InnoDB keeps correspondence between human readable table names and their internal identifiers.
1 2 3 4 5 6 7 8 9 10 11 | CREATE TABLE `SYS_TABLES` ( `NAME` varchar(255) NOT NULL DEFAULT '', `ID` bigint(20) unsigned NOT NULL DEFAULT '0', `N_COLS` int(10) DEFAULT NULL, `TYPE` int(10) unsigned DEFAULT NULL, `MIX_ID` bigint(20) unsigned DEFAULT NULL, `MIX_LEN` int(10) unsigned DEFAULT NULL, `CLUSTER_NAME` varchar(255) DEFAULT NULL, `SPACE` int(10) unsigned DEFAULT NULL, PRIMARY KEY (`NAME`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1 |
is a human readable table name in form database_name/table_name e.g. NAME
sakila/actor
. ID
is a table identifier. We will need the table id to find indexes of the table.
1 2 3 4 5 6 | mysql> select * from SYS_TABLES WHERE NAME='sakila/actor'; +--------------+-----+--------+------+--------+---------+--------------+-------+ | NAME | ID | N_COLS | TYPE | MIX_ID | MIX_LEN | CLUSTER_NAME | SPACE | +--------------+-----+--------+------+--------+---------+--------------+-------+ | sakila/actor | 741 | 4 | 1 | 0 | 0 | | 738 | +--------------+-----+--------+------+--------+---------+--------------+-------+ |
SYS_INDEXES
This table lists all indexes the table has, secondary as well as the primary.
1 2 3 4 5 6 7 8 9 10 | CREATE TABLE `SYS_INDEXES` ( `TABLE_ID` bigint(20) unsigned NOT NULL DEFAULT '0', `ID` bigint(20) unsigned NOT NULL DEFAULT '0', `NAME` varchar(120) DEFAULT NULL, `N_FIELDS` int(10) unsigned DEFAULT NULL, `TYPE` int(10) unsigned DEFAULT NULL, `SPACE` int(10) unsigned DEFAULT NULL, `PAGE_NO` int(10) unsigned DEFAULT NULL, PRIMARY KEY (`TABLE_ID`,`ID`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1 |
So,
is our table_id. TABLE_ID
here is the index identifier. We need it to find InnoDB pages which belong to the table’s index. Which one? A table can have many secondary indexes, but only in the primary index we can find all fields. It must exist for any InnoDB table. If explicitely defined its ID
is PRIMARY. If the primary key is not defined InnoDB will use a unique secondary index as the primary. If there is no any unique index InnoDB will create one implicitely. Its name will be GEN_CLUST_INDEX.NAME
It doesn’t matter how the primary index gets created it will have minimal
among the indexes of the table.ID
1 2 3 4 5 6 7 | mysql> select * from SYS_INDEXES WHERE TABLE_ID=741; +----------+------+---------------------+----------+------+-------+---------+ | TABLE_ID | ID | NAME | N_FIELDS | TYPE | SPACE | PAGE_NO | +----------+------+---------------------+----------+------+-------+---------+ | 741 | 1679 | PRIMARY | 1 | 3 | 738 | 3 | | 741 | 1680 | idx_actor_last_name | 1 | 0 | 738 | 4 | +----------+------+---------------------+----------+------+-------+---------+ |
SYS_COLUMNS
Table SYS_COLUMNS
stores fields names and type information of the table.
1 2 3 4 5 6 7 8 9 10 | CREATE TABLE `SYS_COLUMNS` ( `TABLE_ID` bigint(20) unsigned NOT NULL, `POS` int(10) unsigned NOT NULL, `NAME` varchar(255) DEFAULT NULL, `MTYPE` int(10) unsigned DEFAULT NULL, `PRTYPE` int(10) unsigned DEFAULT NULL, `LEN` int(10) unsigned DEFAULT NULL, `PREC` int(10) unsigned DEFAULT NULL, PRIMARY KEY (`TABLE_ID`,`POS`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1 |
Here
is a well known table identifier, TABLE_ID
– a position of a field in the table. POS
is the name of a field, NAME
and MTYPE
store information about the field type, encoding, NULL/NOT NULL properties etc.PRTYPE
is the maximum number of bytes a field uses to store a value. I’m not sure what LEN
PREC
is used for. It sounds like a short from “precision”, but at least for DECIMAL type where it would make sense it is still zero. If you know how InnoDB uses
please let me know.PREC
1 2 3 4 5 6 7 8 9 | mysql> select * from SYS_COLUMNS WHERE TABLE_ID=741; +----------+-----+-------------+-------+---------+------+------+ | TABLE_ID | POS | NAME | MTYPE | PRTYPE | LEN | PREC | +----------+-----+-------------+-------+---------+------+------+ | 741 | 0 | actor_id | 6 | 1794 | 2 | 0 | | 741 | 1 | first_name | 12 | 2162959 | 135 | 0 | | 741 | 2 | last_name | 12 | 2162959 | 135 | 0 | | 741 | 3 | last_update | 6 | 1799 | 4 | 0 | +----------+-----+-------------+-------+---------+------+------+ |
So, we know all fields of the table, we can get the type. Is it enough for the recovery? No.
SYS_FIELDS
We need to know what fields form the primary key. The matter is regardless at what position primary key fields are defined in CREATE TABLE
statement internally they always go first in a record. The second issue we should take into account is internal fields DB_TRX_ID
and DB_ROLL_PTR
. These two fields always reside between the primary key fields and the rest of the fields.
SYS_FIELDS
lists fields of all indexes, including the primary.
1 2 3 4 5 6 | CREATE TABLE `SYS_FIELDS` ( `INDEX_ID` bigint(20) unsigned NOT NULL, `POS` int(10) unsigned NOT NULL, `COL_NAME` varchar(255) DEFAULT NULL, PRIMARY KEY (`INDEX_ID`,`POS`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1 |
Fields names explain their content.
Index id in our example is 1679:
1 2 3 4 5 6 | mysql> SELECT * FROM SYS_FIELDS WHERE INDEX_ID = 1679; +----------+-----+----------+ | INDEX_ID | POS | COL_NAME | +----------+-----+----------+ | 1679 | 0 | actor_id | +----------+-----+----------+ |
Which means the primary key of the table is one field actor_id.
Now we have all necessary information to generate CREATE TABLE statement.
0. Download the latest revision of the recovery tool
1 | bzr branch lp:percona-data-recovery-tool-for-innodb |
1. Compile the dictionary parsers
1 | make dict_parsers |
2. Split ibdata1 with page_parser
1 | ./page_parser -f /var/lib/mysql/ibdata1 |
3. Recover SYS_TABLES, SYS_INDEXES, SYS_COLUMNS and SYS_FIELDS from indexes 0-1, 0-3, 0-2 and 0-4 respectively.
1 | ./bin/constraints_parser.SYS_FIELDS -4f pages-ibdata1/FIL_PAGE_INDEX/<index_id> |
4. Load dumps of the dictionary tables into some MySQL server. Use LOAD DATA INFILE
constraints_parser
generates
1 | mysql>LOAD DATA INFILE '/path/to/SYS_FIELDS' REPLACE INTO TABLE `SYS_FIELDS` FIELDS TERMINATED BY 't' OPTIONALLY ENCLOSED BY '"' LINES STARTING BY 'SYS_FIELDSt' (`INDEX_ID`, `POS`, `COL_NAME`); |
5. Now everything is ready to generate a CREATE TABLE
statement for a table:
1 2 3 4 5 6 7 8 | ./sys_parser -u root sakila/actor CREATE TABLE `actor`( `actor_id` SMALLINT UNSIGNED NOT NULL, `first_name` VARCHAR(45) CHARACTER SET 'utf8' COLLATE 'utf8_general_ci' NOT NULL, `last_name` VARCHAR(45) CHARACTER SET 'utf8' COLLATE 'utf8_general_ci' NOT NULL, `last_update` TIMESTAMP NOT NULL, PRIMARY KEY (`actor_id`) ) ENGINE=InnoDB; |
In the end there are two notes:
- The generated structure differs from the original structure of the table, but it is good enough for the recovery.
- DECIMAL fields are not fully supported. If a field was of DECIMAL(5,2) type sys_parser will generate DECIMAL(5,0). It has to be corrected manually
Добрый день. Если я на руках имею только ibdata и файлы ib_logfile – имеется ли шанс восстановить базу InnoDB?
indapublic ,
Too little of information to be certain. What was innodb_file_per_table? What happened to the database?
But if your database is still in ibdata1 (you can grep it to be sure) then the answer is closer to “yes”.
1) innodb_file_per_table видимо по умолчанию, ибо в my.ini он отсутствует.
2) говорят, что полетел дисковый массив у хостера. бэкапы отсутствуют. сервер достался мне так сказать в наследство
3) да, база в ibdata1
Поиски пути восстановления без наличия frm файлов не дали никаких результатов. Можете хотя бы подсказать начало? Спасибо
1) innodb_file_per_table is OFF by default, which means all tables are in ibdata1.
2) if it was HW problem and ibdata1 was saved then ibdata1 should be valid, maybe with some corruption.
The tables structure you can get from the dictionary as it is described here.
Once you have the structure you can get your tables back with page_parser and constraints_parser tools.
You can check the docs how to do it https://www.percona.com/docs/wiki/innodb-data-recovery-tool:mysql-data-recovery:start
.
The biggest challenge here is to find index_id for each table – https://www.percona.com/docs/wiki/innodb-data-recovery-tool:mysql-data-recovery:advanced_techniques#recovering_the_data_dictionary
Несколько таблиц были ошибочно “восстановлены” из старых, неверных копий. Можно ли как то восстановить предыдущее состояние? Если файлы ib_logfile0 ib_logfile0 (260 Mb) и ibdata1 (34Mb)
This utility works well. I was wondering if there is also a way to restore SYS_FOREIGN to be able to get information on foreign key constraints.
Garima,
It’s possible to restore secondary indexes and FK but not implemented yet