Folgendes Szenario. Eine Datenbank ist aufgrund eines Storage Fehlers komplett verloren gegangen. Alle Datendateien sind unwiederbringlich verloren. Das Backup wurde zwar gemacht, ist jedoch ebenfalls aufgrund des Storage Fehlers verloren. Einzig ein Datapump Export der Datenbank der einige Wochen zuvor erzeugt wurde und die archivierten Redo Informationen sind lückenlos vorhanden.
So oder so ähnlich sieht der Alptraum eines DBAs aus. Die Frage ob hier überhaupt noch etwas zu retten ist kommt einem als Erstes in den Sinn. Die ganz klaren Antwort ist Jein.
Der erzeugte Datapump Export kann uns zwar die Daten konsistent zu einem Zeitpunkt wiederherstellen, die vorhandenen Archive sind aber nicht einfach gegen eine neue Datenbank anzuwenden, da sie ja zu einer ganz anderen Datenbank gehören. Mit dem Logminer von Oracle können wir aber die Archive durchpflügen und Änderungsinformationen aus dem Archiv als (Redo) SQL anzeigen lassen. Durch anwenden dieser SQLs könnte die aus dem Export erstellte neue Datenbank also recovert werden auf einen Zeitpunkt unserer Wahl.
Hier kommen wir zur nächsten Problematik. Da die Quelldatenbank verloren ist, kann der Logminer keine Logminer Dictionary Informationen mehr finden. Diese Informationen werden benötigt um die Referenzierung in den Redo SQLs sinnhaft aufzulösen. Fehlen diese Informationen, wird das SQL ungefähr so aussehen:
insert into "UNKNOWN"."OBJ# 45522"("COL 1","COL 2","COL 3","COL 4") values (HEXTORAW('45465f4748'),HEXTORAW('546563686e6963616c20577269746572'), HEXTORAW('c229'),HEXTORAW('c3020b'));
Wie in dem Beispiel zu sehen ist, sind die Informationen wie Objektname, Spaltennamen und auch Datentypen nicht auflösbar. Es ist also nur ersichtlich, dass in diesem Beispiel ein Insert gegen ein Objekt mit 4 Spalten abgesetzt wurde. Das SQL kann also nicht einfach ausgeführt werden. Es muss eine manuelle Erfassung der fehlenden Informationen erfolgen.
Bevor wir zu einer Schritt für Schritt Anleitung kommen möchte ich euch warnen. Solltet ihr mit diesem Weg viele Tabellen mit einem komplexen Datenmodell wiederherstellen wollen, ist das eine nahezu unmögliche Aufgabe aufgrund der fehlenden Dictionary Informationen.
1. Logminer einrichten und ausführen
Um den Logminer nutzen zu können, benötigen wir eine funktionierende Datenbank. Am Besten wir spielen hier auf gleich das Schema aus unserem vorhandenen Export ein, von dem wir Daten retten wollen. Um mein Beispiel noch einigermaßen handhabbar zu halten, stellen wir uns vor, dass nur eine Tabelle aus unserem Schema relevant ist für die Datenrettung.
Also, den vorhandenen Dump importieren. Unser Beispielschema heißt MyApp.
impdp directory=DATA_PUMP_DIR dumpfile=MyApp_expdp_2018.dmp schemas=MyApp
Im Anschluss benötigen wir alle Archive die wir nach SQL durchsuchen wollen. Ich habe sie unter /DBTest/archive_restore/ abgelegt.
Nun können wir damit beginnen den Logminer zu konfigurieren. Eine offizielle Dokumentation ist hier zu finden. Wir müssen dem Logminer alle Archive benennen die wir durchsuchen wollen.
EXECUTE DBMS_LOGMNR.ADD_LOGFILE(LOGFILENAME => '/DBTest/archive_restore/arc_DBTEST_1500_1_956588287.log',OPTIONS => DBMS_LOGMNR.NEW);
EXECUTE DBMS_LOGMNR.ADD_LOGFILE(LOGFILENAME => '/DBTest/archive_restore/arc_DBTEST_1501_1_956588287.log',OPTIONS => DBMS_LOGMNR.ADDFILE);
EXECUTE DBMS_LOGMNR.ADD_LOGFILE(LOGFILENAME => '/DBTest/archive_restore/arc_DBTEST_1502_1_956588287.log',OPTIONS => DBMS_LOGMNR.ADDFILE);
EXECUTE DBMS_LOGMNR.ADD_LOGFILE(LOGFILENAME => '/DBTest/archive_restore/arc_DBTEST_1503_1_956588287.log',OPTIONS => DBMS_LOGMNR.ADDFILE);
EXECUTE DBMS_LOGMNR.ADD_LOGFILE(LOGFILENAME => '/DBTest/archive_restore/arc_DBTEST_1504_1_956588287.log',OPTIONS => DBMS_LOGMNR.ADDFILE);
Das erste Archiv muss mit dem Parameter DBMS_LOGMNR.NEW hinzugefügt werden. Alle weiteren mit DBMS_LOGMNR.ADDFILE.
Nachdem alle Logfiles hinzugefügt wurden, kann der Logminer gestartet werden.
DBMS_LOGMNR.START_LOGMNR;
Ob alle Redologs in der Logminer Konfiguration hinterlegt sind, kann über die View V$LOGMNR_LOGS verifiziert werden. Nun können die SQL Informationen aus der View V$LOGMNR_CONTENTS über die Spalte SQL_REDO ausgelesen werden.
2. SQLs zu Tabelle identifizieren
Jetzt kommen wir zum komplizierten Teil unserer Aufgabe. Wir müssen die korrekte Objekt ID finden um die richtigen SQLs selektieren zu können.
In unserem Beispiel geht es uns nur darum, alle DMLs von einer Tabelle mit dem Namen RESTORE_TEST zu finden. Wie oben erwähnt ist die Referenzierung aufgrund der fehlenden Dictionary Informationen nicht möglich. Wir wissen aber aufgrund unseres Datapump Dumps den wir wieder impotiert haben, wie die Tabelle aussieht.
desc RESTORE_TEST;
Name Null Typ
---------- ---- -------------
RES_ID NUMBER(3)
FIRST_NAME VARCHAR2(100)
LAST_NAME VARCHAR2(100)
RES_DATE DATE
Jetzt müssen wir uns also alle SQLs aus der V$LOGMNR_CONTENTS zusammensuchen, die Daten in eine Tabelle mit vier Spalten geschrieben oder geändert hat. Gerade bei Tabellen mit gängiger Spaltenanzahl wird unsere Abfrage hunderte mögliche Objekt IDs zurückliefern. Hier muss dann erneut bewertet werden, ob ein Recovery auf diesem Weg noch Sinn macht.
Hier das SQL, dass uns alle Informationen für Tabellen mit vier Spalten liefert.
select sql_redo from v$logmnr_contents where sql_redo like '%COL 4%' and sql_redo not like '%COL 5%';
SQL_REDO
----------------------------------------------------------------------------------------------------------------------------------------------------------
...
insert into "UNKNOWN"."OBJ# 43332"("COL 1","COL 2","COL 3","COL 4") values (HEXTORAW('c103'),HEXTORAW('4f52412441545f4f535f4f50545f53595f343238'),HEXTORAW('78750c1e0601010000000085d8'),HEXTORAW('c106'));
insert into "UNKNOWN"."OBJ# 102833"("COL 1","COL 2","COL 3","COL 4") values (HEXTORAW('c106'),HEXTORAW('566F726E616D65'),HEXTORAW('4E6163686E616D65'),HEXTORAW('78760109102D30'));
...
Wir haben jetzt zwei SQLs gefunden. Eines hat Daten in die Tabelle mit der Objekt ID 43332 eingefügt, das andere in eine Tabelle mit der Objekt ID 102833. Um verifizieren zu können, welche Objekt ID jetzt hinter der von uns gesuchten Tabelle steckt müssen wir die angezeigten HEX Werte einen nach dem anderen in die Datentypen der Tabelle umwandeln. Wenn die Konvertierung sinnvolle Werte zurückliefert, könnte es sich bei der Objekt ID um unsere gesuchte Tabelle handeln.
select utl_raw.cast_to_number('c103'), utl_raw.cast_to_varchar2('4f52412441545f4f535f4f50545f53595f343238'), utl_raw.cast_to_varchar2('78750c1e0601010000000085d8') FROM dual;
UTL_RAW.CAST_TO_NUMBER('C103') UTL_RAW.CAST_TO_VARCHAR2('4F52412441545F4F535F4F50545F53595F343238') UTL_RAW.CAST_TO_VARCHAR2('78750C1E0601010000000085D8')
-------------------------------------------------------------------------
2 ORA$AT_OS_OPT_SY_428 xu
select utl_raw.cast_to_number('c106'), utl_raw.cast_to_varchar2('566F726E616D65'), utl_raw.cast_to_varchar2('4E6163686E616D65'), (select to_date( to_char( to_number( substr( p_str, 1, 2 ), 'xx' ) - 100, 'fm00' ) || to_char( to_number( substr( p_str, 3, 2 ), 'xx' ) - 100, 'fm00' ) || to_char( to_number( substr( p_str, 5, 2 ), 'xx' ), 'fm00' ) || to_char( to_number( substr( p_str, 7, 2 ), 'xx' ), 'fm00' ) || to_char( to_number( substr( p_str,9, 2 ), 'xx' )-1, 'fm00' ) || to_char( to_number( substr( p_str,11, 2 ), 'xx' )-1, 'fm00' ) || to_char( to_number( substr( p_str,13, 2 ), 'xx' )-1, 'fm00' ), 'yyyymmddhh24miss' ) from (select 'c106' p_str from dual)) FROM dual;
UTL_RAW.CAST_TO_NUMBER('C106') UTL_RAW.CAST_TO_VARCHAR2('566F726E616D65') UTL_RAW.CAST_TO_VARCHAR2('4E6163686E616D65') (SELECTTO_DATE(TO_CHAR(TO_NUMBER(SUBSTR(P_STR,1,2),'XX')-100,'FM00')||TO_CHAR(TO_NUMBER(SUBSTR(P_STR,3,2),'XX')-100,'FM00')||TO_CHAR(TO_NUMBER(SUBSTR(P_STR,5,2),'XX'),'FM00')||TO_CHAR(TO_NUMBER(SUBSTR(P_STR,7,2),'XX'),'FM00')||TO_CHAR(TO_NUMBER(SUBSTR(P_S
-------------------------------------------------------------------------
5 Vorname Nachname 09.01.18 15:44:47
Wir sehen, dass die Konvertierung der Werte für das Objekt 43332 keine sinnvollen Werte ergibt. Die Konvertierung der Werte für das Objekt 102833 aber schon. Also könnte diese Objektnummer unsere gesuchte Tabelle sein.
Also selektieren wir uns allen Redo SQL mit dieser Objekt ID:
select sql_redo from v$logmnr_contents where table_name = 'OBJ# 102833';
3. Daten wiederherstellen
Hierzu benötigen wir zunächst eine Tabelle mit den gewünschten Spalten im RAW Format.
create table RESTORE_TEST_RAW ("COL1" raw(2000), "COL2" raw(2000), "COL3" raw(2000), "COL4" raw(2000));
Table created
Mit einer kleinen Anpassung der SQL Statements aus der V$LOGMNR_CONTENTS können die RAW Werte nun in unsere neue Tabelle eingefügt werden. Wir müssen hierzu den Objektnamen ändern und das Leerzeichen in den Spaltennamen (COL 1
wird zu COL1
usw.) entfernen.
insert into RESTORE_TEST_RAW ("COL1","COL2","COL3","COL4") values (HEXTORAW('c106'),HEXTORAW('566F726E616D65'),HEXTORAW('4E6163686E616D65'),HEXTORAW('78760109102D30'));
1 Row inserted.
Commit;
Als letzten Schritt unserer Datenrettung, müssen wir die RAW Werte in den Zieldatentyp konvertieren und in unsere ursprüngliche Tabelle einfügen.
insert into RESTORE_TEST select utl_raw.cast_to_number(COL1), utl_raw.cast_to_varchar2(COL2), utl_raw.cast_to_varchar2(COL3), (select to_date( to_char( to_number( substr( p_str, 1, 2 ), 'xx' ) - 100, 'fm00' ) || to_char( to_number( substr( p_str, 3, 2 ), 'xx' ) - 100, 'fm00' ) || to_char( to_number( substr( p_str, 5, 2 ), 'xx' ), 'fm00' ) || to_char( to_number( substr( p_str, 7, 2 ), 'xx' ), 'fm00' ) || to_char( to_number( substr( p_str,9, 2 ), 'xx' )-1, 'fm00' ) || to_char( to_number( substr( p_str,11, 2 ), 'xx' )-1, 'fm00' ) || to_char( to_number( substr( p_str,13, 2 ), 'xx' )-1, 'fm00' ), 'yyyymmddhh24miss' ) from (select COL4 p_str from RESTORE_TEST_RAW b where a.COL1 = b.COL1 order by COL1)) from RESTORE_TEST_RAW a;
1 Row inserted.
Commit;
Damit wäre die Datenrettung abgeschlossen. Ein letztes Mal der Hinweis, dass bei steigender Komplexität der Zeitaufwand um die korrekte Objekt ID herauszufinden rasant steigt. Ab einer gewissen Anzahl an Tabellen ist eine solche Wiederherstellungsstrategie schlicht nicht mehr durch einen Menschen zu leisten.
Philip