The Fake Index Trap (“Nowhere Now”) May 2, 2023
Posted by Richard Foote in CBO, Drop Index, Fake Indexes, Index Internals, NOSEGMENT Option, Online DDL, Oracle, Oracle 21c, Oracle Bugs, Oracle General, Oracle Indexes, Oracle Indexing Internals Webinar, Oracle19c, Tablespace Management, Virtual Indexes.add a comment
In a recent correspondence, I was alerted to an issue in relation to the use of Virtual/Fake/Nosegment Indexes that I wasn’t aware of previously. Having a play, it appears this issue is still present in at least Oracle Database 21c, so I thought it worth a mention in case anyone else happens to fall into this trap.
I’ve discussed Virtual/Fake/Nosegment Indexes a number of times previously. These are indexes that do not exist as a physical segment (and so can be created almost immediately without consuming any storage), that can be used to determine if an index could potentially be used by the CBO if it were to be actually created.
Although such Fake Indexes don’t physically exist, they can cause issues if forgotten…
To illustrate this issue, I’ll start by creating a new tablespace:
SQL> create tablespace BOWIE_TS datafile 'C:\ORADATA\ZIGGY\ZIGGYPDB1\BOWIE_TS.DBF' size 100M; Tablespace created.
Next, I’ll create and populate a table in this BOWIE_TS tablespace:
SQL> create table bowie_test (id number, name varchar2(42)) tablespace bowie_ts; Table created. SQL> insert into bowie_test select rownum, 'DAVID BOWIE' from dual connect by level <=10000; 10000 rows created. SQL> commit; Commit complete. SQL> exec dbms_stats.gather_table_stats(ownname=>null, tabname=>'BOWIE_TEST'); PL/SQL procedure successfully completed.
I’ll next create a Virtual/Fake index, using the NOSEGMENT option:
SQL> create index bowie_test_id_i on bowie_test(id) nosegment tablespace bowie_ts; Index created.
We note this Fake Index is NOT listed in either USER_INDEXES or USER_SEGMENTS:
SQL> select index_name, tablespace_name from user_indexes where table_name='BOWIE_TEST'; no rows selected SQL> select segment_name, segment_type, tablespace_name from user_segments where segment_name='BOWIE_TEST_ID_I'; no rows selected
If we run a basic, highly selective query on this table:
SQL> select * from bowie_test where id=42; Execution Plan ---------------------------------------------------------- Plan hash value: 65548668 -------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU) | Time | -------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 16 | 11 (0) | 00:00:01 | |* 1 | TABLE ACCESS FULL | BOWIE_TEST | 1 | 16 | 11 (0) | 00:00:01 | -------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 1 - filter("ID"=42) Statistics ---------------------------------------------------------- 0 recursive calls 0 db block gets 38 consistent gets 0 physical reads 0 redo size 648 bytes sent via SQL*Net to client 52 bytes received via SQL*Net from client 2 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 1 rows processed
We notice the CBO uses a FTS. The Fake Index is NOT considered by default.
However, if we set the session as follows and re-run the query:
SQL> alter session set "_use_nosegment_indexes" = true; Session altered. SQL> select * from bowie_test where id=42; Execution Plan ---------------------------------------------------------- Plan hash value: 1280686875 ------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU) | Time | ------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 16 | 2 (0) | 00:00:01 | | 1 | TABLE ACCESS BY INDEX ROWID BATCHED | BOWIE_TEST | 1 | 16 | 2 (0) | 00:00:01 | |* 2 | INDEX RANGE SCAN | BOWIE_TEST_ID_I | 1 | | 1 (0) | 00:00:01 | ------------------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - access("ID"=42) Statistics ---------------------------------------------------------- 0 recursive calls 0 db block gets 38 consistent gets 0 physical reads 0 redo size 648 bytes sent via SQL*Net to client 52 bytes received via SQL*Net from client 2 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 1 rows processed
We can see the CBO appears to now use the Fake Index, but as it doesn’t actually physically exist, actually uses a FTS behind the scenes (the number of consistent gets is evidence of this). But at least we now know the CBO would at least consider such an index if it physically existed.
We now decide to drop the tablespace and so first try to MOVE the table to another tablespace using the ONLINE option:
SQL> alter table bowie_test move online tablespace users; alter table bowie_test move online tablespace users * ERROR at line 1: ORA-14808: table does not support ONLINE MOVE TABLE because of the presence of nosegment index
The error message clearly states we can’t move the table ONLINE if such a Fake/Nosegment Index exists. This is our official warning of the potential danger to come…
We try to move the table using the default OFFLINE method:
SQL> alter table bowie_test move tablespace users; Table altered.
We have now successfully moved the table to another tablespace.
If we check to see if we have any other segments within the tablespace yto be dropped:
SQL> select segment_name from dba_segments where tablespace_name='BOWIE_TS'; no rows selected
Oracle tells us that no, we do NOT have any current segments in this tablespace.
So it’s now safe to purge and drop this tablespace (or so we think):
SQL> purge tablespace bowie_ts; Tablespace purged. SQL> drop tablespace bowie_ts; Tablespace dropped.
The tablespace has been successfully dropped.
However, if we now re-run the query on this table:
SQL> select * from bowie_test where id=42; select * from bowie_test where id=42 * ERROR at line 1: ORA-00959: tablespace 'BOWIE_TS' does not exist
We get this unexpected error that the tablespace BOWIE_TS does not exist.
BUT, we already know the tablespace doesn’t exist, we’ve just dropped it !!!
So why are we getting this error?
It’s all due to the damn Fake Index we created previously.
Although there is no physical index segment for our Fake Index, there are still some internal Data Dictionary links between the Fake Index and the tablespace it was associated with. The tablespace is gone, but NOT the Fake Index.
The only place where fake indexes can be easily found within Oracle, is within the USER_OBJECTS view:
SQL> select o.object_name, o.object_type, o.status from user_objects o left join user_indexes i on o.object_name=i.index_name where o.object_type='INDEX' and i.index_name is null; OBJECT_NAME OBJECT_TYPE STATUS ------------------------------ ----------------------- ------- BOWIE_TEST_ID_I INDEX VALID
To eliminate this error, we have to first drop the Fake Index associated with the dropped tablespace:
SQL> drop index bowie_test_id_i; Index dropped.
We can now safely run the query without error:
SQL> select * from bowie_test where id=42; ID NAME ---------- ------------------------------------------ 42 DAVID BOWIE
So if you do ever create Fake Indexes, don’t forget to drop them once you’ve finished experimenting with them.
ESPECIALLY if you ever decide to drop the tablespace into which they were associated. This is explained in part in Oracle Support Doc ID 1361049.1.
Oracle 19c Automatic Indexing: DDL Statements With Auto Indexes (No Control) September 1, 2020
Posted by Richard Foote in 19c, 19c New Features, Automatic Indexing, Autonomous Data Warehouse, Autonomous Database, Autonomous Transaction Processing, Drop Automatic Indexing, Drop Index, Index Coalesce, Index Rebuild, Index Shrink, Invisible Indexes, Online DDL, Oracle Indexes.2 comments
I’ve had a number of questions in relation to DDL support for Automatic Indexes since my last post on how one can now drop Automatic Indexes, so decided to quickly discuss what DDL statements are supported with Automatic Indexes.
Many DDL commands are NOT supported with Automatic Indexes, such as making indexes (IN)VISIBLE and (UN)USABLE and changing storage attributes:
SQL> alter index "SYS_AI_600vgjmtqsgv3" invisible; alter index "SYS_AI_600vgjmtqsgv3" invisible * ERROR at line 1: ORA-65532: cannot alter or drop automatically created indexes SQL> alter index "SYS_AI_600vgjmtqsgv3" unusable; alter index "SYS_AI_600vgjmtqsgv3" unusable * ERROR at line 1: ORA-65532: cannot alter or drop automatically created indexes SQL> ALTER INDEX "SYS_AI_600vgjmtqsgv3" INITRANS 5; ALTER INDEX "SYS_AI_600vgjmtqsgv3" INITRANS 5 * ERROR at line 1: ORA-65532: cannot alter or drop automatically created indexes
You also can’t drop indexes with the DDL statement:
SQL> drop index "SYS_AI_600vgjmtqsgv3"; drop index "SYS_AI_600vgjmtqsgv3" * ERROR at line 1: ORA-65532: cannot alter or drop automatically created indexes
Although as discussed in my last post, you can now drop Automatic Indexes by using DBMS_AUTO_INDEX.DROP_AUTO_INDEXES.
You can however potentially improve the structure of an Automatic Index by using the REBUILD, COALESCE or SHRINK (SPACE) options:
SQL> alter index "SYS_AI_600vgjmtqsgv3" rebuild online; Index altered. SQL> alter index "SYS_AI_600vgjmtqsgv3" coalesce; Index altered. SQL> alter index "SYS_AI_600vgjmtqsgv3" shrink space; Index altered.
Interestingly, if Oracle considers an Automatic Index but decides it’s not efficient enough to be created, the Automatic Indexing process can leave a new Automatic Index in UNUSABLE / INVISIBLE state (as previously discussed), which can be subsequently rebuilt:
SQL> select index_name, status, visibility from user_indexes where index_name='SYS_AI_600vgjmtqsgv3'; INDEX_NAME STATUS VISIBILIT ------------------------------ -------- --------- SYS_AI_600vgjmtqsgv3 UNUSABLE INVISIBLE SQL> alter index "SYS_AI_600vgjmtqsgv3" rebuild online; Index altered. SQL> select index_name, status, visibility from user_indexes where index_name='SYS_AI_600vgjmtqsgv3'; INDEX_NAME STATUS VISIBILIT ------------------------------ -------- --------- SYS_AI_600vgjmtqsgv3 VALID INVISIBLE
So the index is now VALID and actually physically created. But you can’t subsequently make it VISIBLE, which means it can’t ordinarily be used by the CBO:
SQL> alter index "SYS_AI_600vgjmtqsgv3" visible; alter index "SYS_AI_600vgjmtqsgv3" visible * ERROR at line 1: ORA-65532: cannot alter or drop automatically created indexes
When you rebuild an Automatic Index, you can however change the manner in which it’s compressed:
SQL> select index_name, status, visibility, compression from user_indexes where index_name='SYS_AI_600vgjmtqsgv3'; INDEX_NAME STATUS VISIBILIT COMPRESSION ------------------------------ -------- --------- ------------- SYS_AI_600vgjmtqsgv3 VALID INVISIBLE ADVANCED LOW SQL> alter index "SYS_AI_600vgjmtqsgv3" rebuild nocompress; Index altered. SQL> select index_name, status, visibility, compression from user_indexes where index_name='SYS_AI_600vgjmtqsgv3'; INDEX_NAME STATUS VISIBILIT COMPRESSION ------------------------------ -------- --------- ------------- SYS_AI_600vgjmtqsgv3 VALID INVISIBLE DISABLED
And no, you can’t rename an Automatic Index:
SQL> alter index "SYS_AI_600vgjmtqsgv3" rename to BOWIE_INDEX; alter index "SYS_AI_600vgjmtqsgv3" rename to BOWIE_INDEX * ERROR at line 1: ORA-65532: cannot alter or drop automatically created indexes
So the answer is it depends on what one can and can’t do currently with an Automatic Index, which of course is subject to change in the future…
Improve Data Clustering on Multiple Columns Concurrently (Two Suns in the Sunset) March 12, 2018
Posted by Richard Foote in 12c, Attribute Clustering, Clustering Factor, Online DDL, Oracle Indexes.3 comments
I’ve had a couple of recent discussions around clustering and how if you attempt to improve the clustering of a table based on a column, you thereby ruin the current clustering that might exist for a different column. The common wisdom being you can only order the data one way and if you change the order, you might improve things for one column but totally stuff things up for another.
However, that’s not strictly correct. Depending on the characteristics of your data, you can potentially order (or interleave) data based on multiple columns concurrently. It’s quite possible to have good or good enough clustering on multiple columns and this is extremely important for indexes, as the efficiency of an index can be directly impacted by the clustering of data on the underlining tables.
So to illustrate, I’m going to create a table that initially has terrible clustering on two unrelated columns (code and grade) :
SQL> create table ziggy (id number, code number, grade number, name varchar2(42)); Table created. SQL> insert into ziggy select rownum, mod(rownum, 100)+1, ceil(dbms_random.value(0,100)), 'ZIGGY STARDUST' from dual connect by level commit; Commit complete. SQL> exec dbms_stats.gather_table_stats(ownname=>null, tabname=> 'ZIGGY', method_opt=>'FOR ALL COLUMNS SIZE 1'); PL/SQL procedure successfully completed. SQL> create index ziggy_code_i on ziggy(code); Index created. SQL> create index ziggy_grade_i on ziggy(grade); Index created. SQL> select index_name, clustering_factor, num_rows from user_indexes where table_name='ZIGGY'; INDEX_NAME CLUSTERING_FACTOR NUM_ROWS -------------------- ----------------- ---------- ZIGGY_CODE_I 1748800 4000000 ZIGGY_GRADE_I 1572829 4000000
So with values for both columns distributed all throughout the table, the Clustering Factor of both the CODE and GRADE indexes are both quite poor (values of 1748800 and 1572829 respectively). Even though both columns have 100 distinct values (and so a selectivity of 1%), the CBO will likely consider the indexes too inefficient to use:
SQL> select * from ziggy where code=42; 40000 rows selected. Execution Plan ---------------------------------------------------------- Plan hash value: 2421001569 --------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU) | Time | --------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 40000 | 1054K | 4985 (10) | 00:00:01| | * 1 | TABLE ACCESS FULL | ZIGGY | 40000 | 1054K | 4985 (10) | 00:00:0 | --------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 1 - filter("CODE"=42) Statistics ---------------------------------------------------------- 0 recursive calls 0 db block gets 20292 consistent gets 0 physical reads 0 redo size 1058750 bytes sent via SQL*Net to client 29934 bytes received via SQL*Net from client 2668 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 40000 rows processed SQL> select * from ziggy where grade=42; 40257 rows selected. Execution Plan ---------------------------------------------------------- Plan hash value: 2421001569 --------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | --------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 40000 | 1054K | 5021 (10) | 00:00:01 | | * 1 | TABLE ACCESS FULL | ZIGGY | 40000 | 1054K | 5021 (10) | 00:00:01 | --------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 1 - filter("GRADE"=42) Statistics ---------------------------------------------------------- 0 recursive calls 0 db block gets 20307 consistent gets 0 physical reads 0 redo size 1065641 bytes sent via SQL*Net to client 30121 bytes received via SQL*Net from client 2685 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 40257 rows processed
So even though the CBO has got the row estimates just about spot on, in both cases a Full Table Scan was chosen.
Let’s create another table based on the table above but this time order the data in CODE column order:
SQL> create table ziggy2 as select * from ziggy order by code; Table created. SQL> exec dbms_stats.gather_table_stats(ownname=>null, tabname=> 'ZIGGY2', method_opt=> 'FOR ALL COLUMNS SIZE 1'); PL/SQL procedure successfully completed. SQL> create index ziggy2_code_i on ziggy2(code); Index created. SQL> create index ziggy2_grade_i on ziggy2(grade); Index created. SQL> select index_name, clustering_factor, num_rows from user_indexes where table_name='ZIGGY2'; INDEX_NAME CLUSTERING_FACTOR NUM_ROWS -------------------- ----------------- ---------- ZIGGY2_CODE_I 17561 4000000 ZIGGY2_GRADE_I 1577809 4000000
We can see that by doing so, we have significantly reduced the Clustering Factor of the CODE index (down from 1748800 to just 17561) . The GRADE index though has changed little as there’s little co-relation between the CODE and GRADE columns.
If we now run the same query with the CODE based predicate:
SQL> select * from ziggy2 where code=42; 40000 rows selected. Execution Plan ---------------------------------------------------------- Plan hash value: 16801974 ----------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU) | Time | ----------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 40000 | 1054K | 264 (4) | 00:00:01 | | 1 | TABLE ACCESS BY INDEX ROWID BATCHED | ZIGGY2 | 40000 | 1054K | 264 (4) | 00:00:01 | |* 2 | INDEX RANGE SCAN | ZIGGY2_CODE_I | 40000 | | 84 (5) | 00:00:01 | ----------------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - access("CODE"=42) Statistics ---------------------------------------------------------- 0 recursive calls 0 db block gets 273 consistent gets 0 physical reads 0 redo size 1272038 bytes sent via SQL*Net to client 685 bytes received via SQL*Net from client 9 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 40000 rows processed
The CBO has not only used the index, but the query is much more efficient as a result, with just 273 consistent gets required to retrieve 40000 rows.
However the query based on the GRADE predicate still uses a FTS:
SQL> select * from ziggy2 where grade=42; 40257 rows selected. Execution Plan ---------------------------------------------------------- Plan hash value: 1810052534 ---------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU) | Time | ---------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 40000 | 1054K | 4920 (10) | 00:00:01 | |* 1 | TABLE ACCESS FULL | ZIGGY2 | 40000 | 1054K | 4920 (10) | 00:00:01 | ---------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 1 - filter("GRADE"=42) Statistics ---------------------------------------------------------- 0 recursive calls 11 db block gets 17602 consistent gets 0 physical reads 0 redo size 434947 bytes sent via SQL*Net to client 696 bytes received via SQL*Net from client 10 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 40257 rows processed
Now if we decide that actually the query based on GRADE is far more important to the business, we could of course reorder the data again. The following is yet another table, this time based on the CODE sorted ZIGGY2 table, but inserted in GRADE column order:
SQL> create table ziggy3 as select * from ziggy2 order by grade; Table created. SQL> exec dbms_stats.gather_table_stats(ownname=>null, tabname=> 'ZIGGY3', method_opt=> 'FOR ALL COLUMNS SIZE 1'); PL/SQL procedure successfully completed. SQL> create index ziggy3_code_i on ziggy3(code); Index created. SQL> create index ziggy3_grade_i on ziggy3(grade); Index created. SQL> select index_name, clustering_factor, num_rows from user_indexes where table_name='ZIGGY3'; INDEX_NAME CLUSTERING_FACTOR NUM_ROWS -------------------- ----------------- ---------- ZIGGY3_CODE_I 30231 4000000 ZIGGY3_GRADE_I 17582 4000000
We notice we now have an excellent, very low Clustering Factor for the GRADE index (down to just 17582). But notice also the Clustering Factor for CODE. Although it has increased from 17561 to 30231, it’s nowhere near as bad as it was initially when is was a massive 1748800.
The point being that with the data already ordered on CODE, Oracle inserting the data in GRADE order effectively had the data already sub-ordered on CODE. So we end up with perfect clustering on the GRADE column and “good enough” clustering on CODE as well.
If we now run the same queries again:
SQL> select * from ziggy3 where code=42; 40000 rows selected. Execution Plan ---------------------------------------------------------- Plan hash value: 1004048030 ----------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU) | Time | ----------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 40000 | 1054K | 392 (3) | 00:00:01 | | 1 | TABLE ACCESS BY INDEX ROWID BATCHED | ZIGGY3 | 40000 | 1054K | 392 (3) | 00:00:01 | |* 2 | INDEX RANGE SCAN | ZIGGY3_CODE_I | 40000 | | 84 (5) | 00:00:01 | ----------------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - access("CODE"=42) Statistics ---------------------------------------------------------- 0 recursive calls 0 db block gets 401 consistent gets 0 physical reads 0 redo size 1272038 bytes sent via SQL*Net to client 685 bytes received via SQL*Net from client 9 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 40000 rows processed
With the CODE based query, the CBO still uses the index and performance is still quite good with consistent gets having gone up a tad (401 up from 273). However, we now have the scenario where the GRADE based query is also efficient with the index access also selected by the CBO:
SQL> select * from ziggy3 where grade=42; 40257 rows selected. Execution Plan ---------------------------------------------------------- Plan hash value: 844233985 ------------------------------------------------------------------------------------------------------ | Id | Operation | Name | Rows | Bytes | Cost (%CPU) | Time | ------------------------------------------------------------------------------------------------------ | 0 | SELECT STATEMENT | | 40000 | 1054K | 264 (4) | 00:00:01 | | 1 | TABLE ACCESS BY INDEX ROWID BATCHED | ZIGGY3 | 40000 | 1054K | 264 (4) | 00:00:01 | |* 2 | INDEX RANGE SCAN | ZIGGY3_GRADE_I | 40000 | | 84 (5) | 00:00:01 | ------------------------------------------------------------------------------------------------------ Predicate Information (identified by operation id): --------------------------------------------------- 2 - access("GRADE"=42) Statistics ---------------------------------------------------------- 0 recursive calls 0 db block gets 278 consistent gets 0 physical reads 0 redo size 1280037 bytes sent via SQL*Net to client 696 bytes received via SQL*Net from client 10 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 40257 rows processed
We are relying here however on how Oracle actually loads the data on the non-sorted columns, so we can guarantee good clustering on both these columns by simply ordering the data on both columns. Here’s table number 4 with data explicitly sorted on both columns (the values of CODE sub-sorted within the ordering of GRADE):
SQL> create table ziggy4 as select * from ziggy3 order by grade, code; Table created. SQL> exec dbms_stats.gather_table_stats(ownname=>null, tabname=> 'ZIGGY4', method_opt=> 'FOR ALL COLUMNS SIZE 1'); PL/SQL procedure successfully completed. SQL> create index ziggy4_code_i on ziggy4(code); Index created. SQL> create index ziggy4_grade_i on ziggy4(grade); Index created. SQL> select index_name, clustering_factor, num_rows from user_indexes where table_name='ZIGGY4'; INDEX_NAME CLUSTERING_FACTOR NUM_ROWS -------------------- ----------------- ---------- ZIGGY4_CODE_I 27540 4000000 ZIGGY4_GRADE_I 17583 4000000
We notice we have a near perfect Clustering Factor on the GRADE column (just 17583) and a “good enough” Clustering Factor on the CODE column (27540).
With 12c Rel 2, we can effectively “fix” the original poorly clustered table online on both columns by adding an appropriate Clustering Attribute to the table (new in 12.1) and performing a subsequent Online table reorg (new in 12.2):
SQL> alter table ziggy add clustering by linear order (grade, code); Table altered. SQL> alter table ziggy move online; Table altered. SQL> select index_name, clustering_factor, num_rows from user_indexes where table_name='ZIGGY'; INDEX_NAME CLUSTERING_FACTOR NUM_ROWS -------------------- ----------------- ---------- ZIGGY_CODE_I 27525 4000000 ZIGGY_GRADE_I 17578 4000000
We now have the same excellent Clustering Factor values as we had in the previous example.
Depending on data characteristics, you could potentially use the Interleave Clustering Attribute for good enough Clustering Factor values on your multiple columns, rather than perfect clustering on specific columns.
So it is entirely possible to have the necessary data ordering you need for effective data accesses on multiple columns concurrently.
12.2 Online Conversion of a Non-Partitioned Table to a Partitioned Table (A Small Plot Of Land) March 27, 2017
Posted by Richard Foote in 12c Release 2 New Features, Attribute Clustering, Clustering Factor, Online DDL, Oracle, Oracle Indexes, Partitioning.4 comments
In my previous post, I discussed how you can now move heap tables online with Oracle Database 12.2 and how this can be very beneficial in helping to address issues with the Clustering Factor of key indexes.
A problem with this technique is that is requires the entire table to be effectively reorganised when most of the data might already be well clustered. It would be much more efficient if we could somehow only move and reorganise just the portion of a table that has poorly clustered data introduced to the table since the last reorg.
Partitioning the table appropriately would help to address this disadvantage but converting a non-partitioned table to be partitioned can be a pain. To do this online with as little complication as possible one could use the dbms_redefintion package which has improved with latter releases.
However, with Oracle Database 12.2, there is now an even easier, more flexible method of performing such a conversion.
Using the same table definition and data as from my previous post, I’m going to first create a couple of additional indexes (on the ID column and on the DATE_CREATED column) :
SQL> create unique index ziggy_id_i on ziggy(id); Index created. SQL> create index ziggy_date_created_i on ziggy(date_created); Index created.
To convert a non-partitioned table to a partitioned table online, we can now use this new extension to the ALTER TABLE syntax:
SQL> alter table ziggy 2 modify partition by range (date_created) 3 (partition p1 values less than (TO_DATE('01-JAN-2015', 'DD-MON-YYYY')), 4 partition p2 values less than (TO_DATE('01-JAN-2016', 'DD-MON-YYYY')), 5 partition p3 values less than (maxvalue)) online; Table altered.
How simple is that !! We now have a table that is range partitioned based on the DATE_CREATED column and this conversion was performed online.
We notice not only is the table now partitioned with all the indexes remaining Valid, but the index based on the partitioning key (DATE_CREATED) has also been implicitly converted to be a Local partitioned index:
SQL> select table_name, status, partitioned from dba_tables where table_name='ZIGGY'; TABLE_NAME STATUS PAR ------------ -------- --- ZIGGY VALID YES SQL> select index_name, status, partitioned, num_rows from dba_indexes where table_name='ZIGGY'; INDEX_NAME STATUS PAR NUM_ROWS -------------------- -------- --- ---------- ZIGGY_DATE_CREATED_I N/A YES 2000000 ZIGGY_CODE_I VALID NO 2000000 ZIGGY_ID_I VALID NO 2000000 SQL> select index_name, partition_name, status, leaf_blocks from dba_ind_partitions where index_name like 'ZIGGY%'; INDEX_NAME PARTITION_NAME STATUS LEAF_BLOCKS -------------------- --------------- -------- ----------- ZIGGY_DATE_CREATED_I P1 USABLE 865 ZIGGY_DATE_CREATED_I P2 USABLE 1123 ZIGGY_DATE_CREATED_I P3 USABLE 1089 SQL> select index_name, partitioning_type, partition_count, locality from dba_part_indexes where table_name='ZIGGY'; INDEX_NAME PARTITION PARTITION_COUNT LOCALI -------------------- --------- --------------- ------ ZIGGY_DATE_CREATED_I RANGE 3 LOCAL
As part of the table conversion syntax, we have the option to also update all the associated indexes and partition them in any manner we may want. For example:
SQL> alter table ziggy 2 modify partition by range (date_created) 3 (partition p1 values less than (TO_DATE('01-JAN-2015', 'DD-MON-YYYY')), 4 partition p2 values less than (TO_DATE('01-JAN-2016', 'DD-MON-YYYY')), 5 partition p3 values less than (maxvalue)) online 6 update indexes 7 (ziggy_code_i local, 8 ziggy_id_i global partition by range (id) 9 (partition ip1 values less than (maxvalue))); Table altered.
In this example, not only are we converting the non-partitioned table to be partitioned, but we’re also explicitly converting the index on the CODE column to be a Locally partitioned index and the index on the ID column to be Globally partitioned in its own manner.
If we look at the definition of these indexes, we see that they also have all been converted to partitioned indexes online along with the table:
SQL> select table_name, status, partitioned from dba_tables where table_name='ZIGGY'; TABLE_NAME STATUS PAR ------------ -------- --- ZIGGY VALID YES SQL> select index_name, status, partitioned from dba_indexes where table_name = 'ZIGGY'; INDEX_NAME STATUS PAR -------------------- -------- --- ZIGGY_CODE_I N/A YES ZIGGY_ID_I N/A YES ZIGGY_DATE_CREATED_I N/A YES SQL> select index_name, partitioning_type, partition_count, locality from dba_part_indexes where table_name='ZIGGY'; INDEX_NAME PARTITION PARTITION_COUNT LOCALI -------------------- --------- --------------- ------ ZIGGY_CODE_I RANGE 3 LOCAL ZIGGY_ID_I RANGE 1 GLOBAL ZIGGY_DATE_CREATED_I RANGE 3 LOCAL
If we look at the Clustering Factor of the important CODE column index, we see that all partitions have an excellent Clustering Factor as all partitions have just been created.
SQL> select partition_name, num_rows, clustering_factor from dba_ind_partitions where index_name='ZIGGY_CODE_I'; PARTITION_NAME NUM_ROWS CLUSTERING_FACTOR -------------------- ---------- ----------------- P1 490000 2275 P2 730000 3388 P3 780000 3620
However, if we now add new rows to the table as would occur with a real application, the data from the “current” partition results in the Clustering Factor “eroding” over time for this partition.
SQL> insert into ziggy select 2000000+rownum, mod(rownum,100), sysdate, 'DAVID BOWIE' from dual connect by level <= 500000; 500000 rows created. SQL> commit; Commit complete. SQL> exec dbms_stats.gather_index_stats(ownname=>null,indname=>'ZIGGY_CODE_I'); PL/SQL procedure successfully completed. SQL> select partition_name, num_rows, clustering_factor from dba_ind_partitions where index_name='ZIGGY_CODE_I'; PARTITION_NAME NUM_ROWS CLUSTERING_FACTOR -------------------- ---------- ----------------- P1 490000 2275 P2 730000 3388 P3 1280000 238505
As discussed previously, the Clustering Attribute has no effect with standard DML operations. Therefore, the efficiency of the CODE index reduces over time in the partition where new data is being introduced. The Clustering Factor has now substantially increased from 3620 to 238505. Note for all the other partitions where there are no modifications to the data, the Clustering Factor remains excellent.
Having the table/index partitioned means we can therefore periodically reorg just the problematic partition:
SQL> alter table ziggy move partition p3 update indexes online; Table altered. SQL> select partition_name, num_rows, clustering_factor from dba_ind_partitions where index_name='ZIGGY_CODE_I'; PARTITION_NAME NUM_ROWS CLUSTERING_FACTOR -------------------- ---------- ----------------- P1 490000 2275 P2 730000 3388 P3 1280000 5978
The Clustering Factor for this partition has now reduced substantially from 238505 to just 5978.
For those of you with the Partitioning database option, the ability in 12.2 to now so easily convert a non-partitioned table to be partitioned, along with its associated indexes is just brilliant 🙂
12c Enhanced Online Index DDL Operations (Lady Godiva’s Operation) February 17, 2014
Posted by Richard Foote in 12c, Drop Index, Invisible Indexes, Online DDL, Oracle Indexes, Unusable Indexes.6 comments
In my last couple of posts, I discussed how table partitions can be moved online since 12c, keeping all indexes in sync as part of the process.
12c also introduced enhancements to a number of index related DDL statements, removing blocking locks and making their use online and far less intrusive. The following commands now have a new ONLINE option:
DROP INDEX ONLINE
ALTER INDEX UNUSABLE ONLINE
So if we look at a little example (initially on 11g R2), where we create a table and associated index on the CODE column:
SQL> create table radiohead (id number, code number, name varchar2(30)); Table created. SQL> insert into radiohead select rownum, mod(rownum,1000), 'DAVID BOWIE' from dual connect by level <= 1000000; 1000000 rows created. SQL> commit; Commit complete. SQL> create index radiohead_code_i on radiohead(code); Index created.
If we now insert a new row in one session but not commit:
SQL> insert into radiohead values (1000001, 42, 'ZIGGY STARDUST'); 1 row created.
And then attempt any of the following DDL commands in another session:
SQL> drop index radiohead_code_i; drop index radiohead_code_i * ERROR at line 1: ORA-00054: resource busy and acquire with NOWAIT specified or timeout expired
SQL> alter index radiohead_code_i invisible; alter index radiohead_code_i invisible * ERROR at line 1: ORA-00054: resource busy and acquire with NOWAIT specified or timeout expired
SQL> alter index radiohead_code_i unusable; alter index radiohead_code_i unusable * ERROR at line 1: ORA-00054: resource busy and acquire with NOWAIT specified or timeout expired
They all get the well-known “ORA-00054: resource busy” error.
If on the other hand, one of these DDL statements is already running in a session:
SQL> alter index radiohead_code_i unusable;
All DML statements in other sessions will hang until the DDL completes:
SQL> insert into radiohead values (1000002, 42, 'THIN WHITE DUKE');
Once the index is finally made unusable:
SQL> alter index radiohead_code_i unusable; Index altered. SQL> select index_name, status from dba_indexes where index_name = 'RADIOHEAD_CODE_I'; INDEX_NAME STATUS ------------------------------ -------- RADIOHEAD_CODE_I UNUSABLE SQL> select segment_name, blocks, extents from dba_segments where segment_name = 'RADIOHEAD_CODE_I'; no rows selected
We can see not only is the index now in an unusable state but the index segment has been dropped (in 11g r2) as the storage associated with the unusable index is of no further use.
So these commands prior to the Oracle 12c Database previously had locking related issues.
If we now perform the same setup in 12c and again have an outstanding transaction in a session:
SQL> drop index radiohead_code_i online;
The Drop Index command doesn’t now get the Ora-00054: resource busy, but rather hangs until all prior transactions complete.
However, while the Drop Index command hangs, it doesn’t in turn lock out transactions within other sessions. In another session:
SQL> insert into radiohead values (1000002, 42, 'THIN WHITE DUKE'); 1 row created.
And in yet other session:
SQL> delete radiohead where id = 42; 1 row deleted. SQL> commit; Commit complete.
These all complete successfully. The Drop Index command itself will eventually complete successfully once all prior transaction have finished.
SQL> drop index radiohead_code_i online; Index dropped.
Another more subtle difference in behaviour with 12c. If there’s an existing transaction when you decide to make an index unusable:
SQL> insert into radiohead values (1000001, 42, 'ZIGGY STARDUST'); 1 row created.
SQL> alter index radiohead_code_i unusable online;
As in the previous demo, the alter index command will hang indefinitely until the previous transaction commits:
SQL> commit; Commit complete. SQL> alter index radiohead_code_i unusable online; Index altered. SQL> select index_name, status from dba_indexes where index_name = 'RADIOHEAD_CODE_I'; INDEX_NAME STATUS ------------------------- -------- RADIOHEAD_CODE_I UNUSABLE SQL> select segment_name, blocks, extents from dba_segments where segment_name = 'RADIOHEAD_CODE_I'; SEGMENT_NAME BLOCKS EXTENTS ---------------- ---------- ---------- RADIOHEAD_CODE_I 2176 32
We note the index has eventually been made Unusable, however the segment has not now been dropped (as it was in the 11g R2 demo) due to the use of the ONLINE clause.
With the Oracle 12c Database, the locking implications and concurrency issues associated these index related DDL commands have been reduced with these new ONLINE options.