Detecting and Curing Row Migration
Administration Tips
Detecting and Curing Row Migration Row migration occurs when an existing row, happily contained within an Oracle Block, is updated to contain extra information, and finds that there is insufficient free space within the block to house the newly-grown record. The primary cause for this is that PCTFREE for a segment has been set too low (PCTFREE is there to stop new inserts taking place in a block before the block is actually full -thus leaving some wasted space at the top of the block, expressly for the purpose of allowing existing rows to slosh around within the block as they are updated). Finding no room to expand within its original block, most of the row is moved off into a completely new block. All that is left behind in the original block is a small row stub (a row header, if you like), which is used to point to where the real row data is now loacted. This seriously degrades performance, since the index on the segment still points to the original block. That means all access to that row via the index is first directed to the original block -where it discovers the row stub pointing to the new block. That's two blocks that have to be read before we can access the row data, rather than the original one. Doubling your I/O for row access is not exactly what one would recommend as a performance enhancing tip!! Detecting whether rows have already migrated is therefore important. There are a number of ways to do it, but the easiest is to issue the following command: ALTER TABLE BLAH COMPUTE STATISTICS;
That causes a full table scan to take place (so it's not something you'd do in the middle of a busy day), and each row to be inspected for pointers to other blocks. (It also calculates other important statistics for the table which the Cost-based Optimizer uses to generate execution plans whenever a new SQL statement is thrown at the database). The DBA_TABLES view is then populated with the statistics gathered during the scan, and for our purposes the killer column is one called CHAIN_CNT. If that contains a non-zero number, then you have row migration -or, possibly, row chaining -the column doesn't distinguish between them. To distinguish row migration (which can be fixed) from row chaining (which can't), you need to know your table column definitions. If the definition included things like 'CLOB' or "BLOB' or 'VARCHAR2(3000)', then the number probably reflects row chaining (which is where a single row is simply too long to fit into a single Oracle Block, and has to be broken up and distributed amongst several). If the definition is full of "normal" stuff, like 'VARCHAR2(30)', 'NUMBER(8,2) and so on, then it is extremely unlikely that the row simply can't be fitted into a block -in which case, the CHAIN_CNT must represent migrated rows.
Copyright Š Howard Rogers 2001
10/18/2001
Page 1 of 4
Detecting and Curing Row Migration
Administration Tips
If you determine that row migration has taken place, what can you do to fix it up? Firstly, of course, you probably want to adjust PCTFREE for the segment: ALTER TABLE BLAH
PCTFREE 30;
(or some other percentage that is appropriate). That helps prevent future migration, but it doesn't cure any that's already taken place. For that, you have to recall that the migration happened because rows got updated. Migration does not happen when rows are freshly inserted. So what we need is something that will get all the rows of a table freshly inserted -and that will fix the problem. The easiest way to achieve that (in 8i only, alas) is to issue the following command: ALTER TABLE BLAH MOVE TABLESPACE
X;
The 'move tablespace' command does not have to reference a completely new tablespace you can type in the name of the same tablespace in which the table is already housed. But it does cause every record to be read, and freshly inserted into new blocks, and the fresh insertion means that the row migration is cured. There's only one slight drawback with this approach: you need to rebuild all indexes on the table, since they are left pointing at the blocks where the table used to be. In earlier versions of Oracle (7.x and 8.0) the move command is not available to you, so the only real option you have is to perform an export of the table, truncate it, and then run import for it. Import needs to be run with the 'IGNORE=Y' parameter (otherwise, the fact that the table still exists will cause it to abort with an error). The import will then proceed to insert all the rows back into the table -and, since these are fresh inserts, row migration will be cured. The main drawback to this approach is that until the import is finished, your Users can have no access to the table data (whereas a 'move tablespace' still allows query access to the original data whilst the move is taking place). On the plus side, import re-creates all indexes automatically, so they don't need to be rebuilt manually. Both approaches are fairly expensive ways of fixing up row migration: for a large table, they'd both involve massive reads and writes. If the migrated rows are only a relatively small proportion of the total number of records (say, 10-20%), there is a more subtle way of fixing the problem. In principle, it involves copying the rowid's of the migrated rows out into a holding table, and using them as a 'hook' by which to delete the rows, and then as a source for reinserting them (and, being fresh inserts, they fix up the migration problem). The steps are as follows:
Copyright Š Howard Rogers 2001
10/18/2001
Page 2 of 4
Detecting and Curing Row Migration
Administration Tips
First, run the Oracle-supplied script utlchain.sql (it's found in the ORACLE_HOME/rdbms/admin directory). It creates a special table called "chained_rows" into which the rowid of the rows suffering from migration are copied when you run the command: ANALYZE TABLE
ORDER_HIST
LIST CHAINED ROWS;
You can query that table directly, like this: SELECT * FROM
CHAINED_ROWS;
OWNER_NAME TABLE_NAME ... HEAD_ROWID TIMESTAMP ---------- ---------- ... ------------------ --------SCOTT EMP ... AAAALUAAHAAAAA1AAA 04-OCT-01 SCOTT EMP ... AAAALUAAHAAAAA1AAB 04-OCT-01 SCOTT EMP ... AAAALUAAHAAAAA1AAC 04-OCT-01 However, the more meaningful approach is to create a holding table to hold the entire row data for these rows, using the rowid stored in the chained_rows table as the hook by which to select them. You do that by running a command like this: CREATE TABLE HOLD_EMP AS SELECT * FROM EMP WHERE ROWID IN (SELECT HEAD_ROWID FROM CHAINED_ROWS WHERE TABLE_NAME = 'EMP'); Now you use the same sort of sub-select technique to delete the rows suffering migration out of the original table, like this: DELETE FROM EMP WHERE ROWID IN (SELECT HEAD_ROWID FROM CHAINED_ROWS WHERE TABLE_NAME = 'EMP'); And finally, you insert the same row data back into the original table, drawing on the holding table contents we created earlier: INSERT INTO EMP SELECT * FROM HOLD_EMP;
Copyright Š Howard Rogers 2001
10/18/2001
Page 3 of 4
Detecting and Curing Row Migration
Administration Tips
The freshly inserted rows will now no longer be migrated rows (you could check that by truncating the chained_rows table, and doing a fresh 'analyze table emp list chained rows' command -this time, a select * from chained_rows should yield zero records). To tidy up, drop the holding table: DROP TABLE
INT_ORDER_HISTORY;
And finally (if you haven't already done so) clear out the chained_rows table: DELETE FROM
CHAINED_ROWS;
(or you could truncate it).
This technique is rather more fiddly than the 'move tablespace' or 'export-truncate-import' ones, but it has the distinct advantage that the bulk of the table remains unaffected by your repair efforts. On a big table, that's a huge benefit. Just in case it isn't clear, in all three cases, you need to change PCTFREE first, otherwise the fix will be purely temporary: unless the underlying cause of row migration is addressed (i.e., a lack of space in the block to accommodate updates), rows will start migrating again the first time you do some updates to the table.
Copyright Š Howard Rogers 2001
10/18/2001
Page 4 of 4