Data Warehouse Fault Tolerance Part 3: Restoring

Hopefully you've read the introduction, Part 1, and Part 2. Those posts detailed methods for building fault-tolerant ETL code, with a strong bias in favor of using Oracle Database features. Now I'll drill into the backup and recovery aspect of data warehousing fault tolerance, and tackle the age-old question of whether to ARCHIVELOG or NOARCHIVELOG in a BI/DW environment.

When I engage with clients that have a data warehouse operating in NOARCHIVELOG mode, their usual reasoning for this decision is a perceived performance gain. This makes sense on the surface... because NOARCHIVELOG prevents the generation of all that unwanted and unneeded REDO, right?

Not exactly. There is misconception about what NOARCHIVELOG actually means, and hopefully, I can clear that up with a demonstration. I have a database in NOARCHIVELOG, and I'll test to see whether my statements generate REDO:

SQL> SELECT log_mode
  2    FROM v$database;

LOG_MODE
------------
NOARCHIVELOG

1 row selected.

Elapsed: 00:00:00.00
SQL>
SQL> CREATE TABLE target.sales
  2      AS SELECT *
  3           FROM sh.sales
  4          WHERE 1=0;

Table created.

Elapsed: 00:00:00.59
SQL>
SQL> SET autotrace on statistics
SQL>
SQL> INSERT INTO target.sales
  2         SELECT *
  3           FROM sh.sales;

918843 rows created.

Elapsed: 00:00:02.92

Statistics
----------------------------------------------------------
       1897  recursive calls
      40779  db block gets
       7062  consistent gets
       1585  physical reads
   38832896  redo size
        742  bytes sent via SQL*Net to client
        958  bytes received via SQL*Net from client
          4  SQL*Net roundtrips to/from client
          2  sorts (memory)
          0  sorts (disk)
     918843  rows processed

SQL>
SQL> ROLLBACK;

Rollback complete.

Elapsed: 00:00:01.32
SQL>
SQL> INSERT /*+ APPEND */ INTO target.sales
  2         SELECT *
  3           FROM sh.sales;

918843 rows created.

Elapsed: 00:00:06.00

Statistics
----------------------------------------------------------
       1042  recursive calls
       5581  db block gets
       2874  consistent gets
       1052  physical reads
      92108  redo size
        732  bytes sent via SQL*Net to client
        975  bytes received via SQL*Net from client
          4  SQL*Net roundtrips to/from client
          5  sorts (memory)
          0  sorts (disk)
     918843  rows processed

SQL>
SQL> ROLLBACK;

Rollback complete.

Elapsed: 00:00:00.00
SQL>  

The regular insert statement generated 38M of REDO in a NOARCHIVELOG database. Interesting. And the INSERT /*+ APPEND */ statement generated only 92K. Though it would appear that neither of these statements actually executed in NOLOGGING mode, the truth is that the APPEND statement did. All statements generate a little bit of REDO, because updates to the data dictionary are always logged.

So why do regular inserts generate REDO on a NOARCHIVELOG database? There is a myth in the Oracle world that NOARCHIVELOG means that no REDO is generated, but that is not the case. Choosing NOARCHIVELOG mode simply means that we are foregoing the option to use media recovery (restoring datafiles, rolling forward). Think about it: REDO is not simply for media recovery, it's also for crash recovery. If all REDO generation was suspended, Oracle wouldn't be able to open after a simple server crash. In NOARCHIVELOG mode, there are situations where we can suspend most of the REDO generated, and one of those situations involves using the INSERT /*+ APPEND */ statement. So why would the database allow these NOLOGGING operations? Because direct-path operations write blocks directly into datafiles, bypassing the buffer cache. We wouldn't have to rely on the online REDO logs to recover those transactions, and so Oracle allows us to minimize the REDO generated.

So if you have your database in NOARCHIVELOG mode for performance reasons, but you are using ETL tools that don't support true direct-path writes on Oracle (a lot of the third-party tools don't), or you are using cursor-based, row-by-row load scenarios, the same amount of REDO is generated if the database was in ARCHIVELOG mode. The only thing gained from operating in this manner is the privilege of having to shut down the database whenever a backup is needed.

Perhaps another myth that gets perpetuated is that we can't have the best of both worlds, but in fact we can. We can minimize the amount of REDO generated, we can operate in ARCHIVELOG mode, we can backup our database in online mode, and we would be able to restore from that backup. The solution: NOLOGGING tables and indexes. I'll put the database in ARCHIVELOG mode, and rerun the test case above with one small change: I'll change the table to be NOLOGGING:

SQL> startup mount
ORACLE instance started.

Total System Global Area  422670336 bytes
Fixed Size                  1336960 bytes
Variable Size             343935360 bytes
Database Buffers           71303168 bytes
Redo Buffers                6094848 bytes
Database mounted.
SQL> alter database
  2  archivelog;

Database altered.

SQL> alter database
  2  open;

Database altered.

SQL> SELECT log_mode
  2    FROM v$database;

LOG_MODE
------------
ARCHIVELOG

1 row selected.

Elapsed: 00:00:00.06
SQL>
SQL> ALTER TABLE target.sales
  2        nologging;

Table altered.

Elapsed: 00:00:01.02
SQL>
SQL> SET autotrace on statistics
SQL>
SQL> INSERT INTO target.sales
  2         SELECT *
  3           FROM sh.sales;

918843 rows created.

Elapsed: 00:00:02.47

Statistics
----------------------------------------------------------
      15560  recursive calls
      33573  db block gets
      13861  consistent gets
       6260  physical reads
   38289752  redo size
        740  bytes sent via SQL*Net to client
        958  bytes received via SQL*Net from client
          4  SQL*Net roundtrips to/from client
        154  sorts (memory)
          0  sorts (disk)
     918843  rows processed

SQL>
SQL> ROLLBACK;

Rollback complete.

Elapsed: 00:00:01.45
SQL>
SQL> INSERT /*+ APPEND */ INTO target.sales
  2         SELECT *
  3           FROM sh.sales;

918843 rows created.

Elapsed: 00:00:03.51

Statistics
----------------------------------------------------------
          1  recursive calls
       4628  db block gets
       1718  consistent gets
         59  physical reads
       8072  redo size
        732  bytes sent via SQL*Net to client
        975  bytes received via SQL*Net from client
          4  SQL*Net roundtrips to/from client
          1  sorts (memory)
          0  sorts (disk)
     918843  rows processed

SQL>
SQL> ROLLBACK;

Rollback complete.

Elapsed: 00:00:00.03
SQL> 

We get the exact same behavior with a NOLOGGING table in ARCHIVELOG mode than we did with NOARCHIVELOG mode. But is having the database in ARCHIVELOG mode of any value when all of our ETL processes are NOLOGGING? We can perform an online backup, but would we even be able to restore from that backup if we have transactions that executed as NOLOGGING?

The answer is "yes" and "yes". We just need one small change to our backup strategy: a well-placed incremental backup.

To increase the performance of our incremental backup, we need to create a block change tracking file. The database keeps a list of all changed blocks so that RMAN will know exactly what to backup during an incremental:

SQL> alter database enable block change tracking
  2  using file '/oracle/oradata/bidw1/change_blocks.bct';

Database altered.

Elapsed: 00:00:02.16
SQL> select * from
  2  v$block_change_tracking;

STATUS       | FILENAME                                 |      BYTES
------------ | ---------------------------------------- | ----------
ENABLED      | /oracle/oradata/bidw1/change_blocks.bct  |   11599872

1 row selected.

Elapsed: 00:00:00.01
SQL>  

We start by taking the initial incremental level 0 backup:

RMAN> backup incremental
2> level 0 database
3> plus archivelog;

Starting backup at 11-FEB-10
current log archived
using target database control file instead of recovery catalog
allocated channel: ORA_DISK_1
channel ORA_DISK_1: SID=45 device type=DISK
channel ORA_DISK_1: starting archived log backup set
channel ORA_DISK_1: specifying archived log(s) in backup set
input archived log thread=1 sequence=18 RECID=47 STAMP=710646180
input archived log thread=1 sequence=19 RECID=48 STAMP=710646955
channel ORA_DISK_1: starting piece 1 at 11-FEB-10
channel ORA_DISK_1: finished piece 1 at 11-FEB-10
piece handle=/oracle/flash_recovery_area/BIDW1/backupset/2010_02_11/o1_mf_annnn_TAG20100211T015555_5q7bhw0c_.bkp tag=TAG20100211T015555 comment=NONE
channel ORA_DISK_1: backup set complete, elapsed time: 00:00:02
Finished backup at 11-FEB-10

Starting backup at 11-FEB-10
using channel ORA_DISK_1
channel ORA_DISK_1: starting incremental level 0 datafile backup set
channel ORA_DISK_1: specifying datafile(s) in backup set
input datafile file number=00001 name=/oracle/oradata/bidw1/system01.dbf
input datafile file number=00002 name=/oracle/oradata/bidw1/sysaux01.dbf
input datafile file number=00003 name=/oracle/oradata/bidw1/undotbs01.dbf
input datafile file number=00004 name=/oracle/oradata/bidw1/users01.dbf
input datafile file number=00005 name=/oracle/oradata/bidw1/example01.dbf
input datafile file number=00007 name=/oracle/oradata/bidw1/target01.dbf
input datafile file number=00006 name=/oracle/oradata/bidw1/tdrep01.dbf
channel ORA_DISK_1: starting piece 1 at 11-FEB-10
channel ORA_DISK_1: finished piece 1 at 11-FEB-10
piece handle=/oracle/flash_recovery_area/BIDW1/backupset/2010_02_11/o1_mf_nnnd0_TAG20100211T015557_5q7bhz1o_.bkp tag=TAG20100211T015557 comment=NONE
channel ORA_DISK_1: backup set complete, elapsed time: 00:05:26
channel ORA_DISK_1: starting incremental level 0 datafile backup set
channel ORA_DISK_1: specifying datafile(s) in backup set
including current control file in backup set
including current SPFILE in backup set
channel ORA_DISK_1: starting piece 1 at 11-FEB-10
channel ORA_DISK_1: finished piece 1 at 11-FEB-10
piece handle=/oracle/flash_recovery_area/BIDW1/backupset/2010_02_11/o1_mf_ncsn0_TAG20100211T015557_5q7btbnf_.bkp tag=TAG20100211T015557 comment=NONE
channel ORA_DISK_1: backup set complete, elapsed time: 00:00:09
Finished backup at 11-FEB-10

Starting backup at 11-FEB-10
current log archived
using channel ORA_DISK_1
channel ORA_DISK_1: starting archived log backup set
channel ORA_DISK_1: specifying archived log(s) in backup set
input archived log thread=1 sequence=20 RECID=49 STAMP=710647302
channel ORA_DISK_1: starting piece 1 at 11-FEB-10
channel ORA_DISK_1: finished piece 1 at 11-FEB-10
piece handle=/oracle/flash_recovery_area/BIDW1/backupset/2010_02_11/o1_mf_annnn_TAG20100211T020143_5q7btr8x_.bkp tag=TAG20100211T020143 comment=NONE
channel ORA_DISK_1: backup set complete, elapsed time: 00:00:03
Finished backup at 11-FEB-10

RMAN>

Now I'll load the SALES table with another INSERT /*+ APPEND */ to make sure we have a NOLOGGING operation since our last backup.

SQL> insert /*+ APPEND */
  2  into target.sales
  3  select * from
  4  sh.sales;

918843 rows created.

Elapsed: 00:00:21.06

Statistics
----------------------------------------------------------
       2780  recursive calls
       6081  db block gets
       2434  consistent gets
       5442  physical reads
     136036  redo size
       1536  bytes sent via SQL*Net to client
       1155  bytes received via SQL*Net from client
          6  SQL*Net roundtrips to/from client
         10  sorts (memory)
          0  sorts (disk)
     918843  rows processed

SQL> commit;

Commit complete.

Elapsed: 00:00:00.07
SQL> 

This is the step in our process that requires a slight change to our backup and recovery strategy: we should get an incremental level 1 backup as soon as the load is complete. This will physically backup all blocks that have been affected by the load, and we wouldn't need to logically apply the REDO logs that are missing the NOLOGGING operations. Since we have changed block tracking, this step will be extremely fast, and I recommend that the ETL process flow or main driving script execute the backup as the very last step in the batch load.

RMAN> backup incremental
2> level 1 database
3> plus archivelog;

Starting backup at 11-FEB-10
current log archived
using target database control file instead of recovery catalog
allocated channel: ORA_DISK_1
channel ORA_DISK_1: SID=30 device type=DISK
channel ORA_DISK_1: starting archived log backup set
channel ORA_DISK_1: specifying archived log(s) in backup set
input archived log thread=1 sequence=18 RECID=47 STAMP=710646180
input archived log thread=1 sequence=19 RECID=48 STAMP=710646955
input archived log thread=1 sequence=20 RECID=49 STAMP=710647302
input archived log thread=1 sequence=21 RECID=50 STAMP=710648694
channel ORA_DISK_1: starting piece 1 at 11-FEB-10
channel ORA_DISK_1: finished piece 1 at 11-FEB-10
piece handle=/oracle/flash_recovery_area/BIDW1/backupset/2010_02_11/o1_mf_annnn_TAG20100211T022455_5q7d67t6_.bkp tag=TAG20100211T022455 comment=NONE
channel ORA_DISK_1: backup set complete, elapsed time: 00:00:01
Finished backup at 11-FEB-10

Starting backup at 11-FEB-10
using channel ORA_DISK_1
channel ORA_DISK_1: starting incremental level 1 datafile backup set
channel ORA_DISK_1: specifying datafile(s) in backup set
input datafile file number=00001 name=/oracle/oradata/bidw1/system01.dbf
input datafile file number=00002 name=/oracle/oradata/bidw1/sysaux01.dbf
input datafile file number=00003 name=/oracle/oradata/bidw1/undotbs01.dbf
input datafile file number=00004 name=/oracle/oradata/bidw1/users01.dbf
input datafile file number=00005 name=/oracle/oradata/bidw1/example01.dbf
input datafile file number=00007 name=/oracle/oradata/bidw1/target01.dbf
input datafile file number=00006 name=/oracle/oradata/bidw1/tdrep01.dbf
channel ORA_DISK_1: starting piece 1 at 11-FEB-10
channel ORA_DISK_1: finished piece 1 at 11-FEB-10
piece handle=/oracle/flash_recovery_area/BIDW1/backupset/2010_02_11/o1_mf_nnnd1_TAG20100211T022457_5q7d6cgv_.bkp tag=TAG20100211T022457 comment=NONE
channel ORA_DISK_1: backup set complete, elapsed time: 00:00:15
channel ORA_DISK_1: starting incremental level 1 datafile backup set
channel ORA_DISK_1: specifying datafile(s) in backup set
including current control file in backup set
including current SPFILE in backup set
channel ORA_DISK_1: starting piece 1 at 11-FEB-10
channel ORA_DISK_1: finished piece 1 at 11-FEB-10
piece handle=/oracle/flash_recovery_area/BIDW1/backupset/2010_02_11/o1_mf_ncsn1_TAG20100211T022457_5q7d6t16_.bkp tag=TAG20100211T022457 comment=NONE
channel ORA_DISK_1: backup set complete, elapsed time: 00:00:01
Finished backup at 11-FEB-10

Starting backup at 11-FEB-10
current log archived
using channel ORA_DISK_1
channel ORA_DISK_1: starting archived log backup set
channel ORA_DISK_1: specifying archived log(s) in backup set
input archived log thread=1 sequence=22 RECID=51 STAMP=710648715
channel ORA_DISK_1: starting piece 1 at 11-FEB-10
channel ORA_DISK_1: finished piece 1 at 11-FEB-10
piece handle=/oracle/flash_recovery_area/BIDW1/backupset/2010_02_11/o1_mf_annnn_TAG20100211T022515_5q7d6vg7_.bkp tag=TAG20100211T022515 comment=NONE
channel ORA_DISK_1: backup set complete, elapsed time: 00:00:01
Finished backup at 11-FEB-10

RMAN>

Now, let's see if we can restore:

RMAN> startup mount

Oracle instance started
database mounted

Total System Global Area     422670336 bytes

Fixed Size                     1336960 bytes
Variable Size                356518272 bytes
Database Buffers              58720256 bytes
Redo Buffers                   6094848 bytes

RMAN> restore database;

Starting restore at 11-FEB-10
allocated channel: ORA_DISK_1
channel ORA_DISK_1: SID=18 device type=DISK

channel ORA_DISK_1: starting datafile backup set restore
channel ORA_DISK_1: specifying datafile(s) to restore from backup set
channel ORA_DISK_1: restoring datafile 00001 to /oracle/oradata/bidw1/system01.dbf
channel ORA_DISK_1: restoring datafile 00002 to /oracle/oradata/bidw1/sysaux01.dbf
channel ORA_DISK_1: restoring datafile 00003 to /oracle/oradata/bidw1/undotbs01.dbf
channel ORA_DISK_1: restoring datafile 00004 to /oracle/oradata/bidw1/users01.dbf
channel ORA_DISK_1: restoring datafile 00005 to /oracle/oradata/bidw1/example01.dbf
channel ORA_DISK_1: restoring datafile 00006 to /oracle/oradata/bidw1/tdrep01.dbf
channel ORA_DISK_1: restoring datafile 00007 to /oracle/oradata/bidw1/target01.dbf
channel ORA_DISK_1: reading from backup piece /oracle/flash_recovery_area/BIDW1/backupset/2010_02_11/o1_mf_nnnd0_TAG20100211T015557_5q7bhz1o_.bkp
channel ORA_DISK_1: piece handle=/oracle/flash_recovery_area/BIDW1/backupset/2010_02_11/o1_mf_nnnd0_TAG20100211T015557_5q7bhz1o_.bkp tag=TAG20100211T015557
channel ORA_DISK_1: restored backup piece 1
channel ORA_DISK_1: restore complete, elapsed time: 00:06:37
Finished restore at 11-FEB-10

RMAN> recover database;

Starting recover at 11-FEB-10
using channel ORA_DISK_1
channel ORA_DISK_1: starting incremental datafile backup set restore
channel ORA_DISK_1: specifying datafile(s) to restore from backup set
destination for restore of datafile 00001: /oracle/oradata/bidw1/system01.dbf
destination for restore of datafile 00002: /oracle/oradata/bidw1/sysaux01.dbf
destination for restore of datafile 00003: /oracle/oradata/bidw1/undotbs01.dbf
destination for restore of datafile 00004: /oracle/oradata/bidw1/users01.dbf
destination for restore of datafile 00005: /oracle/oradata/bidw1/example01.dbf
destination for restore of datafile 00006: /oracle/oradata/bidw1/tdrep01.dbf
destination for restore of datafile 00007: /oracle/oradata/bidw1/target01.dbf
channel ORA_DISK_1: reading from backup piece /oracle/flash_recovery_area/BIDW1/backupset/2010_02_11/o1_mf_nnnd1_TAG20100211T022457_5q7d6cgv_.bkp
channel ORA_DISK_1: piece handle=/oracle/flash_recovery_area/BIDW1/backupset/2010_02_11/o1_mf_nnnd1_TAG20100211T022457_5q7d6cgv_.bkp tag=TAG20100211T022457
channel ORA_DISK_1: restored backup piece 1
channel ORA_DISK_1: restore complete, elapsed time: 00:00:15

starting media recovery
media recovery complete, elapsed time: 00:00:03

Finished recover at 11-FEB-10

RMAN> alter database open;

database opened

RMAN> 

So that's it for the Three "R"'s. I had a lot of fun revisiting the "operations" side of the house, and logging in as SYSDBA again. It's amazing how it all just came back to me... I didn't have to look at the manuals at all. Okay... maybe once.