Troubleshoot: a non-regular solution for “ORA-01502 index is in unusable state”

The upgrade of a database from Oracle 11.2 to 19.24 was completed, and the applications were restarted. Minutes later, the first incident is reported: a batch process aborts after a few moments.

java.sql.SQLException: ORA-01502: index 'SCOTT.EMP_IX' or partition of such index is in unusable state ORA-06512: at "SCOTT.EMP_LOAD", line 1714 ORA-06512: at line 1 

As this routine is executed daily, this failure is automatically attributed to some problem with the new version of Oracle, so let’s get to work and see what it is.

Join me on this interesting adventure!

The problem

Reviewing the code of the stored procedure EMP_LOAD, line 1714 that triggers the error is one in which the command ANALYZE is executed:
INSERT INTO SCOTT.EMP SELECT . . .

EXECUTE IMMEDIATE 'ANALYZE TABLE SCOTT.EMP ESTIMATE STATISTICS';

I try to run it directly and succeed in reproducing the error:

SQL> ANALYZE TABLE SCOTT.EMP ESTIMATE STATISTICS;
ANALYZE TABLE SCOTT.EMP ESTIMATE STATISTICS
*
ERROR at line 1:
ORA-01502: index 'SCOTT.EMP_IX' or partition of such
index is in unusable state

The question is: why is the EMP_IX index in UNUSABLE state?

It turns out that this routine is invoked by another one (EMP_BATCH), which contains the following code:

EXECUTE IMMEDIATE 'TRUNCATE TABLE SCOTT.EMP REUSE STORAGE';
EXECUTE IMMEDIATE 'ALTER TABLE SCOTT.EMP DEALLOCATE UNUSED';
EXECUTE IMMEDIATE 'ALTER INDEX SCOTT.EMP_IX UNUSABLE';

INSERT /*+APPEND*/ INTO SCOTT.EMP SELECT . . .

EMP_LOAD;
  
EXECUTE IMMEDIATE 'ALTER INDEX SCOTT.EMP_IX REBUILD';
EXECUTE IMMEDIATE 'ANALYZE TABLE SCOTT.EMP ESTIMATE STATISTICS';

It is observed that in line 7 the routine that executes the ANALYZE that aborts is invoked, and in line 10 ANALYZE is invoked again.

It turns out that since they want to do a massive data load, the first thing they do is truncate the table and, to reduce the work, they put the indexes in UNUSABLE state.

After that they fill the table from both EMP_BATCH and EMP_LOAD, to finally rebuild the indexes and update the statistics.

Everything would be fine if it were not for the fact that in EMP_LOAD the statistics are also updated and—unlike in previous versions—it is evident that in Oracle 19c it is not allowing to calculate them if there is any index in UNUSABLE state.

The solution

To begin with, the use of ANALYZE should be avoided. Since Oracle 8i the DBMS_STATS package appeared as a replacement and the documentation has been warning us about it.

The use of ANALYZE for the collection of optimizer statistics is obsolete.
If you want to collect optimizer statistics, use the DBMS_STATS package

After verifying that the execution of DBMS_STATS has no problems if there is any index in UNUSABLE state, the immediate measure is to replace all occurrences of ANALYZE by DBMS_STATS.

The problem now is that there are 67 packages containing 1,215 calls to ANALYZE, so a manual change will be a herculean task. It is not a matter of simply opening SQL Developer, loading the package code and running a Search & Replace.

Regular expressions to the rescue

A visual inspection of the code allows us to identify that the calls to ANALYZE have the following pattern:

EXECUTE IMMEDIATE 'ANALYZE TABLE OWNER.TABLE_NAME ESTIMATE STATISTICS';
EXECUTE IMMEDIATE 'ANALYZE TABLE OWNER.TABLE_NAME ESTIMATE STATISTICS SAMPLE 40 PERCENT';
EXECUTE IMMEDIATE 'ANALYZE TABLE OWNER.TABLE_NAME COMPUTE STATISTICS';
EXECUTE IMMEDIATE 'ANALYZE INDEX OWNER.INDEX_NAME ESTIMATE STATISTICS';
EXECUTE IMMEDIATE 'ANALYZE INDEX OWNER.INDEX_NAME COMPUTE STATISTICS';

That is: it can be invoked for a table or for an index, and in some cases the SAMPLE keyword is added.

Everything points to a scenario where we can make use of regular expressions to implement the massive change from ANALYZE to DBMS_STATS.

Although I had never used regular expressions before, the rush to solve the incident and avoid others that may arise later, makes it inevitable to investigate on the subject, and fortunately it turns out that it is not so complicated.

We start by defining that the pattern to identify and transform is the following:

EXECUTE IMMEDIATE 'ANALYZE TABLE|INDEX [[:alnum:]_\$#]+\.[[:alnum:]_\$#]+ ESTIMATE|COMPUTE .+';

Yes, it is a hieroglyphic, but if you pay attention for a few moments I will try to help you decipher it:

  1. The text to be searched starts with EXECUTE IMMEDIATE ‘ANALYZE, that is invariable.
  2. After that you may want to process a table or an index, that “o” translates to ‘|’, hence ‘TABLE|INDEX’ is used.
  3. Next comes the schema name and the table or index name, separated by a period. As these names are limited to the use of letters, numbers, ‘_’ and ‘#’, we use ‘[[:alnum:]_\$#]+’ as the pattern for each name.
  4. As the characters ‘$’ and ‘.’ have a special meaning in regular expressions, we must precede them with ‘\’ so that they are interpreted literally.
  5. Since we do not know how many characters the schema and table or index names have, we use ‘+’, which means “one or more occurrences”.
  6. Now comes ESTIMATE or COMPUTE, which we translate by ‘ESTIMATE|COMPUTE’.
  7. What comes next does not interest us, because with what we have identified we are already sure that it is a statistical calculation with ANALYZE and we have all the data to rewrite it. We express this with ‘.+’ which means ‘one or more characters’.
  8. Finally we have ‘;’, to close the text we are interested in processing.

With this we are now ready to transform the code using a PL/SQL program:

  1. We will start by creating a table in which we will obtain a backup of the code to be processed.
CREATE TABLE system.ddl_analyze_objects (
object_id       NUMBER,
data_object_id  NUMBER,
owner           VARCHAR2(128),
object_name     VARCHAR2(128),
object_type     VARCHAR2(23),
ddl_object      CLOB,
CONSTRAINT ddl_analyze_objects_pk PRIMARY KEY ( object_id ) );
  1. Now we will identify the stored source code (packages, procedures and functions) that contains lines that follow the pattern we have established, and using DBMS_METADATA we will dump the code into the previously created table. Note: we must ignore the objects that are managed by Oracle.
DECLARE
  ddl_tmp CLOB;
  h NUMBER;
  th NUMBER;
BEGIN
   FOR i IN ( SELECT DISTINCT obj.object_id, obj.data_object_id, 
                     obj.owner, obj.object_name, 
                     DECODE( obj.object_type, 'PACKAGE BODY', 'PACKAGE_BODY', object_type) AS object_type
                FROM dba_objects obj, 
                     dba_source src
               WHERE obj.owner = src.owner
                 AND obj.object_name = src.name
                 AND obj.object_type = src.type
                 AND obj.oracle_maintained = 'N'
                 AND REGEXP_LIKE( src.text, 'EXECUTE IMMEDIATE ''ANALYZE TABLE|INDEX [[:alnum:]_\$#]+\.[[:alnum:]_\$#]+ ESTIMATE|COMPUTE .+'';', 'i' )
               ORDER BY obj.object_id, obj.data_object_id, obj.owner, obj.object_name, object_type
             ) 
   LOOP
     h := DBMS_METADATA.open( object_type => i.object_type );
     DBMS_METADATA.set_filter( handle => h, name => 'SCHEMA', value => i.owner) ;
     DBMS_METADATA.set_filter( handle => h, name => 'NAME', value => i.object_name );
     th := DBMS_METADATA.add_transform ( handle => h, name => 'DDL' );
     ddl_tmp := DBMS_METADATA.fetch_clob( handle => h );
     INSERT INTO system.ddl_analyze_objects (
       object_id, data_object_id, owner, object_name, object_type, ddl_object )
     VALUES (
       i.object_id, i.data_object_id, i.owner, i.object_name, i.object_type, ddl_tmp
     );
     DBMS_METADATA.close( handle => h );
   END LOOP;
   COMMIT;
END;
/
  1. Explaining all the code is beyond the scope of this post, so we will focus on the use of REGEXP_LIKE in the predicate of the main query. The first parameter required is the column to evaluate, in this case it is DBA_SOURCE.TEXT. The second parameter is the pattern to search for, which we explained in detail above. The final parameter is ‘i’, which means “ignore case sensitivity”.
  2. We will now process the rows of our DDL_ANALYZE_OBJECTS table, using REGEXP_REPLACE to replace the original code using ANALYZE by its equivalent with DBMS_STATS.
BEGIN
   FOR i IN ( SELECT owner, object_name, object_type,
                     REGEXP_REPLACE( 
                       ddl_object,
                       'EXECUTE IMMEDIATE ''ANALYZE (TABLE|INDEX) ([[:alnum:]_\$#]+)\.([[:alnum:]_\$#]+) (ESTIMATE|COMPUTE) .+'';',
                       UPPER('DBMS_STATS.GATHER_\1_STATS(''\2'',''\3'');'),
                       1, 0, 'i'
                     ) as ddl_object
                FROM system.ddl_analyze_objects
             ) 
   LOOP          
     EXECUTE IMMEDIATE i.ddl_object;
   END LOOP;
END;
/
Once again, it is time to concentrate in order to understand what is going on:
  1. The first parameter required by REGEXP_REPLACE is the column to transform, in this case it is DDL_OBJECT.
  2. The second parameter is the pattern to search for, but you will notice that there is a small difference: the data we are interested in are now surrounded by ‘(‘ and ‘)’, this transforms them into variables. In other words ‘(TABLE|INDEX)’ is transformed into variable ‘1’, the first ‘([[:alnum:]_\$#]+)’ is transformed into variable ‘2’ (i.e. the name of the schema), and the second ‘([[:alnum:]_\$#]+)’ is transformed into variable ‘3’ (i.e. the name of the table or index).
  3. The third parameter is the replace string. In order to be able to reference the variables mentioned in the previous step, they must be prefixed with ‘\’.
  4. The fourth parameter indicates that the evaluation must be done from the first character of the column we are processing.
  5. The fifth parameter indicates that we want to process all occurrences within the column we are processing.
  6. The final parameter is ‘i’, which means “case-insensitive matching”.

End result

Reviewing again the code of the stored procedure EMP_LOAD, line 1714 that previously executed the ANALYZE command now calls DBMS_STATS:

INSERT INTO SCOTT.EMP SELECT . . .

DBMS_STATS.GATHER_TABLE_STATS('SCOTT','EMP');
Something similar happens with EMP_BATCH, which now contains the following code (see line 10):
EXECUTE IMMEDIATE 'TRUNCATE TABLE SCOTT.EMP REUSE STORAGE';
EXECUTE IMMEDIATE 'ALTER TABLE SCOTT.EMP DEALLOCATE UNUSED';
EXECUTE IMMEDIATE 'ALTER INDEX SCOTT.EMP_IX UNUSABLE';

INSERT /*+APPEND*/ INTO SCOTT.EMP SELECT . . .

EMP_LOAD;
  
EXECUTE IMMEDIATE 'ALTER INDEX SCOTT.EMP_IX REBUILD';
DBMS_STATS.GATHER_TABLE_STATS('SCOTT','EMP');

With the change already implemented, we tried to execute the routine that gave life to this incident and now it finishes without errors.

With this we can now consider the work completed and the incident resolved. What would have been a time-consuming and error-prone manual task of several days now takes only a few seconds!

To complement the above, especially on the use of DBMS_METADATA, I recommend reading the notes:

237293.1How to Move from ANALYZE to DBMS_STATS - Introduction
263140.1How to Use Regular Expressions from Oracle 10G Onwards
556823.1How To Use The Package DBMS_METADATA With SCHEMA_EXPORT To Extract DDLs Owned By Another User

Did you find this article interesting, did you have any doubts, do you want to suggest a topic to cover, leave me your comments or contact me me right now!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Related Posts

Are you about to add, drop or replace disks in ASM? You may not be doing it in the most efficient way. Learn how.
Did you know that AutoUpgrade fails if the server names are not in lowercase and you are working with a RAC One Node database.
What to do when Oracle reports an incorrect value for the space used in Fast Recovery Area.

Need Help?

Fill in these details and I will be in touch as soon as possible.