This is nothing more than a basic update of a note that I wrote 8 years ago. The update was triggered by a brief comment made by Martin Widlake at the recent UKOUG annual conference “Breakthrough 2022” in Birmingham. In his presentation on “wide tables”, he mentioned row migration and the possible effects of a row having to migrate many times as it grew and the possibility (of which he was not certain as he had only a vague memory of hearing the claim at some unspecified time in the past) that it might leave a “long chain of pointers” from the header to the final location of the migrated row.
It occurred to me that the vague memory might have been due to my blog note from 2014 explaining that this doesn’t happen. If a row migrates (i.e. the whole row gets moved to a new location leaving only a header behind pointing to the new location) then at a future point in time it might migrate to a 3rd (or 4th or more) location and update the header pointer, or it might actually migrate back to the original location if space has since become available.
The following script (originally created on 10gR2, but updated for 19.11 and modified for ease of retesting) builds a table, performans a series of updates on a row, and dumps the header block after each update.
rem
rem Script: row_migration.sql
rem Author: Jonathan Lewis
rem Dated: Feb 2014 / Dec 2022
rem
rem Last tested
rem 10.2.0.5
rem 19.11.0.0
rem
create table t1 (
id number(6,0),
v1 varchar2(1200)
)
pctfree 0
;
insert into t1
select rownum - 1, rpad('x',100) i
from all_objects i
where rownum <= 75
;
commit;
prompt =========================================
prompt Get the relative file and block number of
prompt the block that hold 74 of the 75 rows,
prompt then dump the block to the trace file.
prompt =========================================
column rel_file_no new_value m_file_no
column block_no new_value m_block_no
select
dbms_rowid.rowid_relative_fno(rowid) rel_file_no,
dbms_rowid.rowid_block_number(rowid) block_no,
count(*) rows_starting_in_block
from
t1
group by
dbms_rowid.rowid_relative_fno(rowid),
dbms_rowid.rowid_block_number(rowid)
having
count(*) = 74
;
alter system flush buffer_cache;
alter session set tracefile_identifier= 'D1_start';
alter system dump datafile &m_file_no block &m_block_no;
prompt ======================================
prompt Make the first row migrate and show it
prompt ======================================
update t1 set v1 = rpad('x',400) where id = 0;
commit;
alter system flush buffer_cache;
alter session set tracefile_identifier= 'D2_migrate';
alter system dump datafile &m_file_no block &m_block_no;
prompt ======================================
prompt Fill the block the long row is now in,
promtp the make it migrate again
prompt ======================================
insert into t1
select rownum + 75, rpad('x',100)
from all_objects
where rownum <= 75
;
commit;
update t1 set v1 = rpad('x',800) where id = 0;
commit;
alter system flush buffer_cache;
alter session set tracefile_identifier= 'D3_migrate_more';
alter system dump datafile &m_file_no block &m_block_no;
prompt =======================================================
prompt Fill the block the long row is in and shrink the row
prompt to see if it returns to its original block. (No).
prompt =======================================================
insert into t1
select rownum + 150, rpad('x',100)
from all_objects
where rownum <= 75
;
commit;
-- delete from t1 where id between 1 and 20;
-- commit;
update t1 set v1 = rpad('x',50) where id = 0;
commit;
alter system flush buffer_cache;
alter session set tracefile_identifier= 'D4_shrink_row';
alter system dump datafile &m_file_no block &m_block_no;
prompt ==============================================
prompt Make a lot of space in the original block then
prompt GROW the row again to see if it migrates back.
prompt ==============================================
delete from t1 where id between 1 and 20;
commit;
update t1 set v1 = rpad('x',1200) where id = 0;
commit;
alter system flush buffer_cache;
alter session set tracefile_identifier= 'D5_forcemigrate';
alter system dump datafile &m_file_no block &m_block_no;
When the script has run there will be 5 trace files, and single “grep” command to find the row entry in the dump for the first row of the block (row 0) will give you results like the following:
[oracle@linux19c trace]$ grep -A+3 "row 0" *19012*.trc
or19_ora_19012_D1_start.trc:tab 0, row 0, @0xab
or19_ora_19012_D1_start.trc-tl: 106 fb: --H-FL-- lb: 0x1 cc: 2
or19_ora_19012_D1_start.trc-col 0: [ 1] 80
or19_ora_19012_D1_start.trc-col 1: [100]
--
or19_ora_19012_D2_migrate.trc:tab 0, row 0, @0xab
or19_ora_19012_D2_migrate.trc-tl: 9 fb: --H----- lb: 0x2 cc: 0
or19_ora_19012_D2_migrate.trc-nrid: 0x090000ac.1
or19_ora_19012_D2_migrate.trc-tab 0, row 1, @0x115
--
or19_ora_19012_D3_migrate_more.trc:tab 0, row 0, @0xab
or19_ora_19012_D3_migrate_more.trc-tl: 9 fb: --H----- lb: 0x1 cc: 0
or19_ora_19012_D3_migrate_more.trc-nrid: 0x090000b0.7
or19_ora_19012_D3_migrate_more.trc-tab 0, row 1, @0x115
--
or19_ora_19012_D4_shrink_row.trc:tab 0, row 0, @0xab
or19_ora_19012_D4_shrink_row.trc-tl: 9 fb: --H----- lb: 0x2 cc: 0
or19_ora_19012_D4_shrink_row.trc-nrid: 0x090000b0.7
or19_ora_19012_D4_shrink_row.trc-tab 0, row 1, @0x115
--
or19_ora_19012_D5_forcemigrate.trc:tab 0, row 0, @0x4b9
or19_ora_19012_D5_forcemigrate.trc-tl: 1208 fb: --H-FL-- lb: 0x2 cc: 2
or19_ora_19012_D5_forcemigrate.trc-col 0: [ 1] 80
or19_ora_19012_D5_forcemigrate.trc-col 1: [1200]
- The D1 trace shows you the row with a column count (cc) of 2, and the two column lengths.
- The D2 trace shows you a column count of zero, and a nrid (next rowid) pointing to row 1 (2nd row) of block 0x090000ac.
- The D3 trace shows you a column count of zero, and a nrid pointing to row 7 (eighth row) of block 0x090000b0, the row has moved to a new location and the header is pointing directly to the new location.
- The D4 trace shows exactly the same output – after shrinking (even to a length that is less than it started at) the row has not moved back to the original location.
- The D5 trace shows that the row has now moved back to its original location, even though it is now several hundred bytes longer than it used to be.
If you’re wondering why the row didn’t move back after shrinking at D4 (and even when I made a lot of space available in the original block the shrink didn’t cause a move), remember that Oracle tries to be “lazy” – the update can take place in situ, so Oracle doesn’t waste time and effort checking the original block.
Footnote
This note makes no claims about what might happen in a more complex case where a row is so long that it splits into multiple row pieces and the pieces end up scattering across multiple blocks. There are a couple of variations on that problem that might be worth investigating if you suspect that there is some behaviour of very wide tables or very long rows that is the source of a performance problem relating to excessive buffer gets or db file sequential reads.
[…] Desupport Note (Dec 2022): The basic content of this blog note is correct, but there is an updated version of the note, with an improved script to automate the process, available at this URL. […]
Pingback by Row Migration | Oracle Scratchpad — December 5, 2022 @ 12:11 pm GMT Dec 5,2022 |
[…] Row Migration (Dec 2022): A basic rewrite of a note published in 2014, re-tested for 19.11 […]
Pingback by Infrastructure Catalogue | Oracle Scratchpad — December 5, 2022 @ 12:14 pm GMT Dec 5,2022 |