The old question about truncate and undo (“does a truncate generate undo or not”) appeared on the OTN database forum over the week-end and then devolved into “what really happens on a truncate” and then carried on.
The quick answer to the traditional question is essentially this: the actual truncate activity typically generates very little undo (and redo) compared to a full delete of all the data because all it does is tidy up any space management blocks and update the data dictionary; the undo and redo generated is only about the metadata, not about the data itself.
Of course, a reasonable response to the quick answer is: “how do you prove that?” – so I suggested that all you had to do was “switch logfile, truncate a table, dump logfile”. Unfortunately I realised that I had never bothered to do this myself and, despite having far more useful things to do, I couldn’t resist wasting some of my evening doing it. Here’s the little script I wrote to help
create table t2 (v1 varchar2(32)); insert into t2 values (rpad('A',32)); commit; create table t1 nologging as with generator as ( select --+ materialize rownum id from dual connect by level <= 1e4 ) select rownum id, rpad('x',100) padding from generator v1, generator v2 where rownum <= 1e5 ; create index t1_i1 on t1(id); alter system flush buffer_cache; execute dbms_lock.sleep(3) alter system switch logfile; insert into t2 values(rpad('X',32)); truncate table t1; insert into t2 values(rpad('Y',32)); commit; execute dump_log
Procedure dump_log simply dumps the current log file. The call to switch logfile keeps the dumped log file as small as possible; and I’ve flushed the buffer cache with a three second sleep to minimise the number of misleading “Block Written Record” entries that might otherwise appear in the log file after the truncate. There were all sorts of interesting little details in the resulting activity when I tested this on 126.96.36.199 – here’s one that’s easy to spot before you even look at the trace file:
SQL> select object_id, data_object_id, object_name from user_objects where object_name like 'T1%'; OBJECT_ID DATA_OBJECT_ID OBJECT_NAME ---------- -------------- -------------------- 108705 108706 T1_I1 108704 108707 T1
Notice how the data_object_id of the index is smaller than that of the table after the truncate ? Oracle truncates (and renumbers) the index before truncating the table.
The truncate activity was pretty much as I had assumed it would be – with one significant variation. The total number of change vectors reported was 272 in 183 redo records (your numbers may vary slightly if you try to reproduce the example), and here’s a summary of the redo OP codes that showed up in those change vectors in order of frequency:
Change operations ================= 1 OP:10.25 Format root block 1 OP:11.11 Insert multiple rows (table) 1 OP:24.1 DDL 1 OP:4.1 Block cleanout record 2 OP:10.4 Delete leaf row 2 OP:13.28 HWM on segment header block 3 OP:10.2 Insert leaf row 3 OP:17.28 standby metadata cache invalidation 4 OP:11.19 Array update (index) 4 OP:11.5 Update row (index) 10 OP:13.24 Bitmap Block state change (Level 2) 11 OP:23.1 Block written record 12 OP:14.1 redo: clear extent control lock 12 OP:22.5 File BitMap Block Redo 14 OP:14.2 redo - lock extent (map) 14 OP:14.4 redo - redo operation on extent map 14 OP:5.4 Commit / Rollback 15 OP:18.3 Reuse record (object or range) 15 OP:22.16 File Property Map Block (FPM) 22 OP:13.22 State on Level 1 bitmap block 24 OP:22.2 File Space Header Redo 29 OP:5.2 Get undo header 58 OP:5.1 Update undo block
The line that surprised me was the 14 commit/rollback OP codes – a single truncate appears to have operated as 14 separate (recursive) transactions. I did start to walk through the trace file to work out the exact order of operation but it’s a really tedious and messy task so I just did a quick scan to get the picture. I may have made a couple of mistakes in the following but I think the steps were:
- Start transaction
- Lock the extent map for the INDEX segment — no undo needed
- Lock each bitmap (space management) block — no undo needed
- Reset each bitmap block — undo needed to preserve space management information
- Reset highwater marks where relevant on bitmap and segment header block — undo needed
- Clear segment header block — undo needed
- Write all the updated space management blocks to disc (local write waits)
- Log file records “Block Written Record”.
- For each space management block in turn
- Update space management blocks with new data object_id — undo needed
- Write the updated block to disc (local write wait)
- Log file records one “Block Written Record” for each block
- Repeat all the above for the TABLE segment.
- Start a recursive transaction
- Insert a row into mon_mod$ — undo needed
- recursive commit
- Set DDL marker in redo log (possibly holding the text of the DDL statement, but it’s not visible in the dump)
- Set object reuse markers in the redo log
- update tab$ — needs undo, it’s just DML
- update ind$ — needs undo, it’s just DML
- update seg$ — needs undo, it’s just DML (twice – once for table once for index)
- update obj$ — needs undo, it’s just DML (twice – ditto)
- COMMIT — at last, with a change vector for a “Standby metadata cache invalidation” marker
The remaining 12 transactions look like things that could be delayed to tidy up things like space management blocks for the files and tablespaces and releasing “block locks”.
This first, long, transaction, is the thing that has to happen as an atomic event to truncate the table – and you can imagine that if the database crashed (or you crashed the session) in the middle of a very slow truncate then there seems to be enough information being recorded in the undo to allow the database to roll forward an incomplete truncate, and then roll back to before the truncate.
It would be possible to test whether or not this would actually work – but I wouldn’t want to do it on a database that anyone else was using.