Point-in-time recovery désigne une restauration de base de données consistante à un point précis soit dans le temps (STOPAT), soit dans une séquence de transactions (STOPATMARK, STOPBEFOREMARK). On ne peut faire du PIT recovery que lorsque la base est en mode de restauration complet.
Le problème inhérent à ce genre de restauration, c’est qu’on ne sait pas exactement jusqu’où restaurer. Lorsque votre collègue vous dit qu’il a supprimé accidentellement les données de la table principale vers 13h45, c’est assez vague comme réponse. Si vous restaurez à 13h44, que fait-on des transactions validées entre 13h44 et le moment précis où le delete a été exécuté ? Il faut jouer avec RESTORE LOG WITH STOPAT, STANDBY pour déterminer le point dans le temps où les données ne sont plus dans la table. Milliseconde par milliseconde ? Bof bof bof…
L’alternative est d’utiliser des marques de transactions:
BEGIN TRAN MiseAJourProduits GO DELETE FROM PRODUITS -- WHERE ID_PRODUIT = @id_produit (aïeuuu) GO ... COMMIT TRAN MiseAJourProduits GO
Dès lors il est possible de restaurer avant la transaction qui pose problème en utilisant l’option STOPBEFOREMARK de RESTORE LOG:
RESTORE LOG maBASE FROM DISK='C:\UTRECHT\MSSQL.1\MSSQL\Backup\maBase.20110712.trn'
WITH RECOVERY, STOPBEFOREMARK = 'MiseAJourProduits'
GO
Seul problème, c’est à l’initiative du développeur de le faire, or la plupart du temps les transactions sont non marquées, je ne vous parle même pas des progiciels…
Fiat lux:
J’ai longtemps regretté l’absence d’un outil built-in tel que LogMiner pour pouvoir lire le contenu d’un backup log, et pouvoir effectuer une restauration précise à la transaction près. En effet, STOPATMARK / STOPBEFOREMARK permettent d’utiliser une syntaxe telle que :
RESTORE LOG ... WITH STOPBEFOREMARK = 'lsn:<LSN number>'.
Seulement voilà, comment connaître les différentes transactions embarquées dans un backup log ? fn_dblog() permet d’obtenir cette information sur le journal lui-même, mais quid des backups…
Et il y a quelques jours, je joue avec OPENROWSET() et là je tombe sur une fonction système non-documentée dans la base resource : fn_dump_dblog(). En lisant le nom, l’émotion m’étreint. Le souffle court, je jette un coup d’oeil au source de la fonction:
create function sys.fn_dump_dblog
(
@start nvarchar (25) = NULL,
@end nvarchar (25) = NULL,
@devtype nvarchar (260) = NULL, -- NULL(DISK) | DISK | TAPE | VIRTUAL_DEVICE
@seqnum Int = 1,
@fname1 nvarchar (260) = NULL,
@fname2 nvarchar (260) = NULL,
@fname3 nvarchar (260) = NULL,
...
@fname64 nvarchar (260) = NULL
)
returns table
as
return select
[Current LSN], [Operation], [Context], [Transaction ID], [Tag Bits],
[Log Record Fixed Length], [Log Record Length], [Previous LSN],
(...)
from OpenRowset (DBLog, @start, @end, @devtype, @seqnum,
@fname1, @fname2, @fname3, @fname4, @fname5,
(...)
@fname62, @fname63, @fname64)
En lisant le paramètre @devtype j’avale mon sandwich indien poulet-curry de travers. Ça fait dix mois que je bosse sur un provider OLEDB qui permette de lire dans un backup full (un peu l’équivalent de l’Archive Database Access sous Sybase ASE), et là je découvre qu’il existe une fonction qui fait la même chose sur les backups log. Depuis la 2005 RTM. Argl.
Et puis en gouglant un peu, je tombe sur cet article de Dimitri Furman, une personne de MS Consulting Services à New-York qui décrit brièvement la fonction et ses arguments. Je ne peux pas résister, il va falloir tester ça.
Scénario de test:
On va donc créer une base de démo, peupler une table et supprimer des intervalles de valeurs en les entrelaçant de backup logs. On arrive à une suppression par erreur, et on souhaite remonter les données juste à la transaction d’avant, sachant que les transactions ne sont pas marquées.
/************************************************************************************* Obj: DEMO RESTAURATION SUITE A ERREUR MANUELLE UTILISATION DU MODE STANDBY POUR EFFECUTER UN POINT-IN-TIME RECOVERY Aut: dbaffaleuf@capdata Crdate: 2011/07/13 */ -- Population de la base use master GO if exists (select 1 from sys.databases where name='demorecovery') drop database demorecovery GO create database demorecovery GO use demorecovery GO create table T1( a numeric identity, b char(4000) default replicate('b',4000), c bigint default round(rand()*100,0)) GO insert into T1 default values GO 1000 create unique clustered index IDX_T1C on T1(a) GO create index IDX_T1NC__c on T1(c) GO -- Activation du mode de récupération complet alter database demorecovery set recovery full GO backup database demorecovery to disk='C:\UTRECHT\MSSQL.1\MSSQL\Backup\demorecovery_FULL.bak' with init, stats GO -- Quelques transactions et quelques backups de transactions delete from T1 where a < 10 GO backup log demorecovery to disk='C:\UTRECHT\MSSQL.1\MSSQL\Backup\demorecovery_LOG1.bak' with init, stats GO delete from T1 where a < 20 GO backup log demorecovery to disk='C:\UTRECHT\MSSQL.1\MSSQL\Backup\demorecovery_LOG2.bak' with init, stats GO delete from T1 where a < 30 GO backup log demorecovery to disk='C:\UTRECHT\MSSQL.1\MSSQL\Backup\demorecovery_LOG3.bak' with init, stats GO -- Trois autres transactions à suivre, la dernière (a<60) est une erreur manuelle delete from T1 where a < 40 GO delete from T1 where a < 50 GO delete from T1 where a < 60 -- <-- CES DONNEES SONT EFFACEES PAR ERREUR !!! GO backup log demorecovery to disk='C:\UTRECHT\MSSQL.1\MSSQL\Backup\demorecovery_LOG4.bak' with init, stats GO -- Une autre transaction valide passe entre-temps delete from T1 where a > 990 GO -- Le téléphone sonne, l'utilisateur demande s'il est possible de récupérer les données avant le delete where a < 60 use master GO -- tail log backup backup log demorecovery to disk='C:\UTRECHT\MSSQL.1\MSSQL\Backup\demorecovery_TAILLOG.bak' with init, stats, NO_TRUNCATE, NORECOVERY GO select state_desc from sys.databases where name='demorecovery' GO state_desc ------------------------------------------------------------ RESTORING -- On identifie le backup de journal qui embarque l'erreur: demorecovery_LOG4.bak -- on recherche les différentes transactions contenues à l'intérieur avec fn_dump_dblog(): select *, [Current LSN] ,Operation ,[Transaction ID] ,AllocUnitId ,[Begin Time] ,[Transaction Name] ,[End Time] ,[Description] from fn_dump_dblog(DEFAULT, DEFAULT,DEFAULT, DEFAULT, 'C:\UTRECHT\MSSQL.1\MSSQL\Backup\demorecovery_LOG4.bak', DEFAULT,DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT) where Operation = 'LOP_BEGIN_XACT' Current LSN Operation Transaction ID Begin Time Transaction Name Description ----------------------- ----------------- -------------- ------------------------ ------------------- ----------------------------------------------------------------------- 00000041:00000338:000e LOP_BEGIN_XACT 0000:000008b8 2011/07/13 15:59:43:580 DELETE DELETE;0x0105000000000005150000006aa3aebb1b5c3491642b62e2e8030000 00000042:00000021:0001 LOP_BEGIN_XACT 0000:000008b9 2011/07/13 15:59:43:587 DELETE DELETE;0x0105000000000005150000006aa3aebb1b5c3491642b62e2e8030000 00000042:00000076:0001 LOP_BEGIN_XACT 0000:000008ba 2011/07/13 15:59:43:630 DELETE DELETE;0x0105000000000005150000006aa3aebb1b5c3491642b62e2e8030000 -- On restaure jusqu'au N-1 backup log avant pb restore database demorecovery from disk='C:\UTRECHT\MSSQL.1\MSSQL\Backup\demorecovery_FULL.bak' with stats, norecovery GO restore log demorecovery from disk='C:\UTRECHT\MSSQL.1\MSSQL\Backup\demorecovery_LOG1.bak' with stats, norecovery GO restore log demorecovery from disk='C:\UTRECHT\MSSQL.1\MSSQL\Backup\demorecovery_LOG2.bak' with stats, norecovery GO restore log demorecovery from disk='C:\UTRECHT\MSSQL.1\MSSQL\Backup\demorecovery_LOG3.bak' with stats, norecovery GO -- On restaure en standby mode à la première transaction pour déterminer si les données entre 50 et 60 sont toujours là restore log demorecovery from disk='C:\UTRECHT\MSSQL.1\MSSQL\Backup\demorecovery_LOG4.bak' with stats, standby='C:\UTRECHT\MSSQL.1\MSSQL\Backup\stdby_files.bak', stopbeforemark='lsn:0x00000042:00000021:0001' GO select state_desc, is_in_standby from sys.databases where name='demorecovery' GO state_desc is_in_standby ------------- -------------- ONLINE 1 use demorecovery GO select * from T1 where a < 60 GO a b c ------ ---------------------------- ---- 40 bbbbbbbbbbbbbbbbbbbbbbbbbbbb 31 41 bbbbbbbbbbbbbbbbbbbbbbbbbbbb 28 42 bbbbbbbbbbbbbbbbbbbbbbbbbbbb 77 43 bbbbbbbbbbbbbbbbbbbbbbbbbbbb 18 44 bbbbbbbbbbbbbbbbbbbbbbbbbbbb 12 45 bbbbbbbbbbbbbbbbbbbbbbbbbbbb 42 46 bbbbbbbbbbbbbbbbbbbbbbbbbbbb 2 47 bbbbbbbbbbbbbbbbbbbbbbbbbbbb 46 48 bbbbbbbbbbbbbbbbbbbbbbbbbbbb 39 49 bbbbbbbbbbbbbbbbbbbbbbbbbbbb 74 50 bbbbbbbbbbbbbbbbbbbbbbbbbbbb 45 51 bbbbbbbbbbbbbbbbbbbbbbbbbbbb 2 52 bbbbbbbbbbbbbbbbbbbbbbbbbbbb 32 53 bbbbbbbbbbbbbbbbbbbbbbbbbbbb 10 54 bbbbbbbbbbbbbbbbbbbbbbbbbbbb 2 55 bbbbbbbbbbbbbbbbbbbbbbbbbbbb 99 56 bbbbbbbbbbbbbbbbbbbbbbbbbbbb 71 57 bbbbbbbbbbbbbbbbbbbbbbbbbbbb 38 58 bbbbbbbbbbbbbbbbbbbbbbbbbbbb 2 59 bbbbbbbbbbbbbbbbbbbbbbbbbbbb 41 -- OK, allons voir à la transaction suivante use master GO restore log demorecovery from disk='C:\UTRECHT\MSSQL.1\MSSQL\Backup\demorecovery_LOG4.bak' with stats, standby='C:\UTRECHT\MSSQL.1\MSSQL\Backup\stdby_files.bak', stopbeforemark='lsn:0x00000042:00000076:0001' GO use demorecovery GO select * from T1 where a < 60 GO a b c ------ ---------------------------- ---- 50 bbbbbbbbbbbbbbbbbbbbbbbbbbbb 45 51 bbbbbbbbbbbbbbbbbbbbbbbbbbbb 2 52 bbbbbbbbbbbbbbbbbbbbbbbbbbbb 32 53 bbbbbbbbbbbbbbbbbbbbbbbbbbbb 10 54 bbbbbbbbbbbbbbbbbbbbbbbbbbbb 2 55 bbbbbbbbbbbbbbbbbbbbbbbbbbbb 99 56 bbbbbbbbbbbbbbbbbbbbbbbbbbbb 71 57 bbbbbbbbbbbbbbbbbbbbbbbbbbbb 38 58 bbbbbbbbbbbbbbbbbbbbbbbbbbbb 2 59 bbbbbbbbbbbbbbbbbbbbbbbbbbbb 41 -- On dirait bien qu'on est dans l'état attendu. Allons voir à la dernière transaction pour confirmer: use master GO restore log demorecovery from disk='C:\UTRECHT\MSSQL.1\MSSQL\Backup\demorecovery_LOG4.bak' with stats, standby='C:\UTRECHT\MSSQL.1\MSSQL\Backup\stdby_files.bak' GO use demorecovery GO select * from T1 where a < 60 GO a b c ------ ---------------------------- ---- -- Ouaip, donc il faut revenir au point précédent, en rechargeant tout depuis le backup FULL: use master GO restore database demorecovery from disk='C:\UTRECHT\MSSQL.1\MSSQL\Backup\demorecovery_FULL.bak' with stats, norecovery GO restore log demorecovery from disk='C:\UTRECHT\MSSQL.1\MSSQL\Backup\demorecovery_LOG1.bak' with stats, norecovery GO restore log demorecovery from disk='C:\UTRECHT\MSSQL.1\MSSQL\Backup\demorecovery_LOG2.bak' with stats, norecovery GO restore log demorecovery from disk='C:\UTRECHT\MSSQL.1\MSSQL\Backup\demorecovery_LOG3.bak' with stats, norecovery GO restore log demorecovery from disk='C:\UTRECHT\MSSQL.1\MSSQL\Backup\demorecovery_LOG4.bak' with stats, stopbeforemark='lsn:0x00000042:00000076:0001', recovery GO use demorecovery GO select * from T1 where a < 60 GO a b c ------ ---------------------------- ---- 50 bbbbbbbbbbbbbbbbbbbbbbbbbbbb 45 51 bbbbbbbbbbbbbbbbbbbbbbbbbbbb 2 52 bbbbbbbbbbbbbbbbbbbbbbbbbbbb 32 53 bbbbbbbbbbbbbbbbbbbbbbbbbbbb 10 54 bbbbbbbbbbbbbbbbbbbbbbbbbbbb 2 55 bbbbbbbbbbbbbbbbbbbbbbbbbbbb 99 56 bbbbbbbbbbbbbbbbbbbbbbbbbbbb 71 57 bbbbbbbbbbbbbbbbbbbbbbbbbbbb 38 58 bbbbbbbbbbbbbbbbbbbbbbbbbbbb 2 59 bbbbbbbbbbbbbbbbbbbbbbbbbbbb 41 -- Note: la transaction suivante (a>990) est perdue. On ne peut pas la recharger car il y aurait une cassure dans la chaîne de LSNs. -- Si on avait tenté de restaurer le tail-log à la suite: restore log demorecovery from disk='C:\UTRECHT\MSSQL.1\MSSQL\Backup\demorecovery_TAILLOG.bak' with stats, recovery GO Msg 4305, Niveau 16, État 1, Ligne 1 Le journal dans ce jeu de sauvegarde commence au numéro de séquence d'enregistrement 66000000017600001, ce qui est trop récent pour une application à la base de données. Une sauvegarde de fichier journal antérieure qui inclut le numéro de séquence d'enregistrement 66000000009200001 peut être restaurée. Msg 3013, Niveau 16, État 1, Ligne 1 RESTORE LOG s'est terminé anormalement.
Donc voilà enfin une méthode pour récupérer une base pile à l’endroit où on veut la récupérer, encore une fois grâce à une fonction non documentée qui mériterait d’être plus connue.
A+. David B.
Continuez votre lecture sur le blog :
- Suppression accidentelle de ligne : comment retrouver le coupable ? (David Baffaleuf) [SQL Server]
- Modes de récupération et journal de transactions, épisode 2 (David Baffaleuf) [SQL ServerVintage]
- Modes de récupération et journal de transactions, épisode 1 (David Baffaleuf) [SQL ServerVintage]
- How-To: réduire la taille du journal de transactions sur disque (David Baffaleuf) [SQL Server]
- Réplication MySQL : Resynchronisation d’un Slave MySQL (Capdata team) [MySQL]