Resolving one of performance issues I figure out that primary key index is much bigger that it should be. Most of database activity is inserting data into partitioning table using some date field as a partitioning key. Rows can be inserted to every partition not only to most recent one. Daily job is dropping one oldest partition per day. Unfortunately primary key index is not partitioned and is maintained during partition drop as a global index. Correlation of these two things is a root cause of index size explosion.
Short test case below:
- script test.sql has been used to check index size, number of rows in table and partition drop
- in second session simple script inserting data into random partition was running continuously
Table definition:
CREATE TABLE LOGS
( "LOG_ID" NUMBER NOT NULL ENABLE,
"DELDATE" DATE NOT NULL ENABLE,
"FILL" VARCHAR2(100),
CONSTRAINT "LOGS_PK" PRIMARY KEY ("LOG_ID")
)
TABLESPACE "USERS"
PARTITION BY RANGE ("DELDATE")
(PARTITION "PARTITION1" VALUES LESS THAN (TO_DATE(' 2011-02-28 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIAN')),
PARTITION "PARTITION2" VALUES LESS THAN (TO_DATE(' 2011-03-28 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIAN')),
PARTITION "PARTITION3" VALUES LESS THAN (TO_DATE(' 2011-04-28 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIAN')),
PARTITION "PARTITION4" VALUES LESS THAN (TO_DATE(' 2011-05-28 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIAN')),
PARTITION "PARTITION5" VALUES LESS THAN (TO_DATE(' 2011-06-28 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIAN')),
PARTITION "PARTITION6" VALUES LESS THAN (TO_DATE(' 2011-07-28 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIAN')),
PARTITION "PARTITION7" VALUES LESS THAN (TO_DATE(' 2011-08-28 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIAN')),
PARTITION "PARTITION8" VALUES LESS THAN (TO_DATE(' 2011-09-28 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIAN')),
PARTITION "PARTITION9" VALUES LESS THAN (TO_DATE(' 2011-10-28 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIAN')),
PARTITION "PARTITION10" VALUES LESS THAN (TO_DATE(' 2011-11-28 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIAN')),
PARTITION "PARTITION11" VALUES LESS THAN (TO_DATE(' 2011-12-28 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIAN'))
);
Source of test.sql script
prompt number of rows in table
select count(*) from pioro.logs;
prompt LOGS_PK index size
select sum(bytes)/1024/1024 from dba_extents where segment_name = 'LOGS_PK';
prompt calculating stats
exec dbms_stats.gather_table_stats('PIORO','LOGS',cascade=>true, estimate_percent=>null);
prompt rows/block without update global indexes
select blevel, leaf_blocks, distinct_keys, NUM_ROWS, NUM_ROWS/leaf_blocks "rows/block" from dba_indexes
where owner='PIORO' and table_name='LOGS' and leaf_blocks <> 0;
prompt number of rows in table partition
select count(*) from pioro.logs partition(&partname);
set timing on
alter table pioro.logs drop partition &partname update global indexes;
Inserts script running in loop looks like
insert into logs select seq_log_id.nextval, to_date('2011-'|| trunc(dbms_random.value(2,9)) ||'-15','yyyy-mm-dd'),
lpad('x',70,'x') from all_source a, all_source b where rownum <= 3000000;
commit;
Here is a output from test.sql for first partition drop
SQL> @test
number of rows in table COUNT(*)
----------
12000000
Elapsed: 00:00:13.74
LOGS_PK index size
SUM(BYTES)/1024/1024
--------------------
242
Elapsed: 00:00:00.51
calculating stats
PL/SQL procedure successfully completed.
Elapsed: 00:01:11.28
rows/block without update global indexes
BLEVEL LEAF_BLOCKS DISTINCT_KEYS NUM_ROWS rows/block
---------- ----------- ------------- ---------- ----------
2 30000 12000000 12000000 400
Elapsed: 00:00:00.03
number of rows in table partition
Enter value for partname: partition1
old 1: select count(*) from pioro.logs partition(&partname)
new 1: select count(*) from pioro.logs partition(partition1)
COUNT(*)
----------
3001361
Elapsed: 00:00:00.35
Enter value for partname: partition1
old 1: alter table pioro.logs drop partition &partname update global indexes
new 1: alter table pioro.logs drop partition partition1 update global indexes
Table altered.
Elapsed: 00:00:55.37
As you can see before first drop average number of rows in one index leaf for primary key was 400. ( 8 kB database block )
Output from next run looks like:
SQL> @test
number of rows in table
COUNT(*)
----------
11998639
Elapsed: 00:00:09.59
LOGS_PK index size
SUM(BYTES)/1024/1024
--------------------
296
Elapsed: 00:00:01.12
calculating stats
PL/SQL procedure successfully completed.
Elapsed: 00:01:13.16
rows/block without update global indexes
BLEVEL LEAF_BLOCKS DISTINCT_KEYS NUM_ROWS rows/block
---------- ----------- ------------- ---------- ----------
2 37500 11998639 11998639 319.963707
Elapsed: 00:00:00.05
number of rows in table partition
Enter value for partname: partition2
old 1: select count(*) from pioro.logs partition(&partname)
new 1: select count(*) from pioro.logs partition(partition2)
COUNT(*)
----------
3749280
Elapsed: 00:00:00.43
Enter value for partname: partition2
old 1: alter table pioro.logs drop partition &partname update global indexes
new 1: alter table pioro.logs drop partition partition2 update global indexes
Table altered.
Elapsed: 00:01:22.85
Numbers of rows in one leaf block drop from 400 to 320 rows per block.
After a few next runs it looks like:
SQL> @test
number of rows in table
COUNT(*)
----------
9748583
Elapsed: 00:00:17.67
LOGS_PK index size
SUM(BYTES)/1024/1024
--------------------
414
Elapsed: 00:00:00.85
calculating stats
PL/SQL procedure successfully completed.
Elapsed: 00:01:11.78
rows/block without update global indexes
BLEVEL LEAF_BLOCKS DISTINCT_KEYS NUM_ROWS rows/block
---------- ----------- ------------- ---------- ----------
2 52500 9748583 9748583 185.687295
Elapsed: 00:00:00.01
Now is really bad - only 186 rows in one leaf block and size of index segment is now 414 MB.
It will have huge impact on database performance as much more index blocks have to be read during index range scans.
Solution is quite simple and can be done online without huge overhead.
It is a index coalesce - this command will consolidate a free space in existing index and provide more free blocks for particular index segment. See example:
SQL> alter index pioro.LOGS_PK coalesce;
Index altered.
Elapsed: 00:03:57.12
SQL> prompt number of rows in table partition
number of rows in table partition
SQL> select count(*) from pioro.logs partition(&partname);
Enter value for partname: partition4
old 1: select count(*) from pioro.logs partition(&partname)
new 1: select count(*) from pioro.logs partition(partition4)
COUNT(*)
----------
5248055
Elapsed: 00:00:10.47
SQL> alter table pioro.logs drop partition &partname update global indexes;
Enter value for partname: partition4
old 1: alter table pioro.logs drop partition &partname update global indexes
new 1: alter table pioro.logs drop partition partition4 update global indexes
Table altered.
Elapsed: 00:02:09.65
SQL> @test
number of rows in table
COUNT(*)
----------
7500528
Elapsed: 00:00:15.78
LOGS_PK index size
SUM(BYTES)/1024/1024
--------------------
414
Elapsed: 00:00:01.47
calculating stats
PL/SQL procedure successfully completed.
Elapsed: 00:00:46.30
rows/block without update global indexes
BLEVEL LEAF_BLOCKS DISTINCT_KEYS NUM_ROWS rows/block
---------- ----------- ------------- ---------- ----------
2 26682 7500528 7500528 281.108163
Elapsed: 00:00:00.08
After index coalesce we can see that Oracle is able to allocate space from free block and size of index remain stable and number of rows in one leaf block is now increasing.
This post has been created based on test data and test scenario but after sucesfull implementation in life systems I will blog about real numbers from production databases.