5

Help, Oracle’s yanking my Chain (count) | The Anti-Kyte

 2 years ago
source link: https://mikesmithers.wordpress.com/2021/07/25/help-oracles-yanking-my-chain-count/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Help, Oracle’s yanking my Chain (count)

Posted on July 25, 2021

Despite all the hype, it turns out that Football was coming home only to change it’s underwear before going out again.
As the Azzuri may have said, “Veni Vidi Vici (ai penalti)”…or something like that.
Look, I’m over it, there are far more important things in life than football and it will not impinge further on this post. Well, not much.

What I’m going to explore here how you persuade Oracle to report the count of chained rows for a table in the _tables dictionary views. The examples that follow were run on Oracle 19c Enterprise Edition. I have observed the same behaviour all the way back to 11gR2.

What is a Chained Row ?

In Oracle, a chained row is one that is too big to fit into a single block.
Usually this happens because a row that fits within a single block is updated and becomes too big to fit into a block ( row migration), or the row is simply too big for a single block to start with.
Either way, reading such rows require additional blocks to be retrieved, which may impact on query performance.
There are other ways row chaining can occur but what we’re really interested in here is exactly how to identify which tables contain chained rows ( and how many).

A Tables with Chained Rows

Time for a simple example involving a table where the rows are created being too large to fit into a single block.
First, let’s check the block size of the tablespace we’re creating the table in :

select ts.tablespace_name, ts.block_size
from dba_tablespaces ts
inner join user_users usr
on usr.default_tablespace = ts.tablespace_name
/   
TABLESPACE_NAME                BLOCK_SIZE
------------------------------ ----------
USERS                                8192

Now let’s create a table which includes rows larger than 8192 bytes ( 8K) :

create table years_of_hurt as
-- Winners of all the major tournaments England haven't won since 1966
select
'ITALY' as team,
2 as euro_wins,
2 as world_cup_wins,
rpad('Forza', 4000, '.') as text1,
rpad('Italia',4000,' ') as text2,
rpad('!', 4000, ' ') as text3
from dual
union all
select 'BRAZIL', null, 3, null, null, null from dual union all
select 'GERMANY', 3, 2, rpad('Die mannschaft', 4000, '.'), rpad('Never bet against them', 4000, ' '), rpad('!', 4000, ' ') from dual union all
select 'CZECH REPUBLIC', 1, null, null, null, null from dual union all
select 'ARGENTINA', null, 3, null, null, null from dual union all
select 'FRANCE', 2, 2, rpad( 'Allez', 4000, '.'), rpad('Les', 4000, ' '), rpad('Bleus!', 4000, ' ') from dual union all
select 'DENMARK', 1, null, 'Which way to the beach ?', null, null from dual union all
select 'GREECE',  1, null, null, null, null from dual union all
select 'SPAIN', 2, 1, rpad('tiki', 4000, '.'), rpad('taka', 4000, ' '), rpad('!', 4000, ' ') from dual union all
select 'PORTUGAL', 1, null, null, null, null from dual
/   

We can see that there are some records which are larger than the tablespace block size…

select team,
nvl(length(team||to_char(euro_wins)||to_char(world_cup_wins)
||text1||text2||text3), 0) as record_length
from years_of_hurt
order by 2 desc
/
TEAM           RECORD_LENGTH
-------------- -------------
GERMANY                12009
FRANCE                 12008
ITALY                  12007
SPAIN                  12007
DENMARK                   32
CZECH REPUBLIC            15
ARGENTINA                 10
PORTUGAL                   9
GREECE                     7
BRAZIL                     7

10 rows selected. 

DBMS_STATS

I mean, this is going to be a really short post, right ? If we want a stat like the chained row count we just need to run DBMS_STATS.GATHER_TABLE_STATS, don’t we ?

I mean, there’s even a column called CHAIN_CNT in the _tables views.
If we look at the column comments, we can confirm it’s purpose :

select comments
from all_col_comments
where owner = 'SYS'
and table_name = 'USER_TABLES'
and column_name = 'CHAIN_CNT'
/
COMMENTS                                                                        
-------------------------------------------
The number of chained rows in the table

So, when we gather stats on the table…

exec dbms_stats.gather_table_stats( user, 'years_of_hurt');

…we should see this reflected in the _tables dictionary views…

select num_rows, blocks, empty_blocks, chain_cnt, avg_row_len,
sample_size
from user_tables
where table_name = 'YEARS_OF_HURT'
/
  NUM_ROWS     BLOCKS EMPTY_BLOCKS  CHAIN_CNT AVG_ROW_LEN LAST_ANALYZ SAMPLE_SIZE
---------- ---------- ------------ ---------- ----------- ----------- -----------
        10         10            0          0        4818 24-JUL-2021          10

Hmmm, it seems that DBMS_STATS is convinced that there are no chained rows at all in our table, despite sampling all 10 rows.

The ANALYZE command

Now, I’m old enough to remember a time when England were really bad at football and Italy…were still pretty good at football. This is back in the days before DBMS_STATS came along.
Then, in order to gather stats on a table, you had to run the ANALYZE command.

Looks like this command is still around and one of it’s purposes is to identify chained rows…

The CHAINED_ROWS table

We’ll come onto running ANALYZE to identify the individual chained rows in a table shortly. Before that, we need to ensure that there is a table to hold the results of this command.

Oracle provide a script for this purpose ( to be run as SYS as SYSDBA) :

$ORACLE_HOME/rdbms/admin/utlchain.sql

In order to access the table, I additionally ran :

grant all on chained_rows to mike;

You can point ANALYZE TABLE at a custom table of your own devising as long as it has the same structure as the CHAINED_ROWS table created by this script – e.g. :

create table local_chained_rows (
owner_name varchar2(128),
table_name varchar2(128),
cluster_name varchar2(128),
partition_name varchar2(128),
subpartition_name varchar2(128),
head_rowid rowid,
analyze_timestamp date)
/

Either way, you can now analyze the table and write the chained row details to whichever target table you’re using. In this case, we’ll use the default :

analyze table years_of_hurt list chained rows into sys.chained_rows;
Table YEARS_OF_HURT analyzed.

I can now see that, as expected, there are four chained rows in the table :

select head_rowid, analyze_timestamp
from sys.chained_rows
where owner_name = 'MIKE'
and table_name = 'YEARS_OF_HURT'
/
HEAD_ROWID         ANALYZE_TIM
------------------ -----------
AAATiyAAMAAAAHrAAA 24-JUL-2021
AAATiyAAMAAAAHsAAC 24-JUL-2021
AAATiyAAMAAAAHuAAD 24-JUL-2021
AAATiyAAMAAAAHwAAD 24-JUL-2021

Not that using ANALYZE in this way does not change the stats recorded in the _TABLES dictionary views :

select chain_cnt
from user_tables
where table_name = 'YEARS_OF_HURT'
/
 CHAIN_CNT
----------
         0

Old-School Analysis

“You’ll win nothing with kids !”

Also…if the continued lack of an accurate chain count in USER_TABLES has left you feeling as sick as a parrot, you can always analyze your table just like Alan Hansen used to do…

analyze table years_of_hurt estimate statistics;

Finally, we can see that the stats in USER_TABLES have been updated :

select num_rows, blocks, empty_blocks, chain_cnt, avg_row_len,
sample_size
from user_tables
where table_name = 'YEARS_OF_HURT'
/
  NUM_ROWS     BLOCKS EMPTY_BLOCKS  CHAIN_CNT AVG_ROW_LEN SAMPLE_SIZE
---------- ---------- ------------ ---------- ----------- -----------
        10         10            6          4        4829          10

It’s worth noting that, even in our restricted column selection, the EMPTY_BLOCKS and AVG_ROW_LEN values have changed in addition to CHAIN_CNT.
If you do decide you need to use analyze in this way, it may be worth re-executing DBMS_STATS afterwards to ensure the stats used by the CBO are accurate. Note that doing so will not overwrite the CHAIN_CNT value :

exec dbms_stats.gather_table_stats(user, 'years_of_hurt');
select num_rows, blocks, empty_blocks, chain_cnt, avg_row_len,
sample_size
from user_tables
where table_name = 'YEARS_OF_HURT'
/
  NUM_ROWS     BLOCKS EMPTY_BLOCKS  CHAIN_CNT AVG_ROW_LEN SAMPLE_SIZE
---------- ---------- ------------ ---------- ----------- -----------
        10         10            6          4        4818          10

For the England Mens’ team, the next major tournament ( provided they qualify) is only next year, in 2022. That’s 56 years of schadenfreude if you’re using the Scottish Calendar.

Advertisements
Report this ad

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK