Restore table statistics

UPDATE 230306 this post was originally written in December 2009. The examples have been verified & updated for Oracle 21.7.0, however there are most likely additional changes that have happened in the meantime that aren’t properly reflected here.

Introduction

The need to restore table statistics can be a common task for production DBAs. Let’s assume that query performance has deteriorated after gathering of statistics. You have 2 options:

  • Create another set of statistics (risky! Don’t do that)
  • Restore the previous statistics

Thankfully stats are automatically preserved and can be restored since Oracle 10g. The first option is only a theoretical option as there is no guarantee that new statistics improve the situation. It’s likely, but not certain. The old statistics on the other hand are proven to work, so let’s restore them.

By the way, statistics retention time is configurable. The current retention time can be determined using the following query:

select DBMS_STATS.GET_STATS_HISTORY_RETENTION from dual;

   GET_STATS_HISTORY_RETENTION 
______________________________ 
                            31

In 11.1 and later, it got even better: you can defer statistics publishing (see this post for more information).

Setup

Assume the following, very very basic setup of a non-partitioned table:

SQL> create table s (id number);

Table created.

Let’s gather statistics explicitly for the empty table:

SQL> exec dbms_stats.gather_table_stats(user,'S')

PL/SQL procedure successfully completed.

The view dba_optstat_operations lists the stats gathering operations against targets. As an interesting side effect, if you specify the table/index name in dbms_stats in lower case, the target column will list it in lower case as well.

The stats gathering operation is reflected in the aforementioned view:

select id, operation, target, start_time, end_time from dba_optstat_operations
where start_time > systimestamp - interval '5' minute;

    ID             OPERATION             TARGET                             START_TIME                               END_TIME 
______ _____________________ __________________ ______________________________________ ______________________________________ 
   405 gather_table_stats    "MARTINDBA"."S"    06-MAR-23 08.14.03.752519000 PM GMT    06-MAR-23 08.14.03.778287000 PM GMT    

You find statistic information in many other places as well. USER_TAB_STATS_HISTORY for example keeps the history of all statistics updates:

select table_name, stats_update_time
 from user_tab_stats_history
where table_name = 'S';

   TABLE_NAME                      STATS_UPDATE_TIME 
_____________ ______________________________________ 
S             06-MAR-23 08.14.03.769967000 PM GMT

Last but not least DBA_TABLES keeps information about the segment statistics as well:

select num_rows, blocks,empty_blocks,avg_space, avg_row_len, sample_size,
       to_char(last_analyzed, 'dd.mm.yyyy hh24:mi') last_analysed
  from USER_TABLES
 where table_name = 'S';

   NUM_ROWS    BLOCKS    EMPTY_BLOCKS    AVG_SPACE    AVG_ROW_LEN    SAMPLE_SIZE       LAST_ANALYSED 
___________ _________ _______________ ____________ ______________ ______________ ___________________ 
          0         0               0            0              0              0 06.03.2023 20:14 

Statistics Update

Let’s generate a change in data and gather statistics:

begin
  for i in 1..1000 loop
    insert into s values (i);
  end loop;
end;
/

commit;

begin dbms_stats.gather_table_stats(user,'s'); end;
/

By default new statistics are immediately published to the dictionary. If that’s not what you want, remember that you can defer statistics publishing in 11.1 and later.

select num_rows, blocks,empty_blocks,avg_space, avg_row_len, sample_size,
       to_char(last_analyzed, 'dd.mm.yyyy hh24:mi') last_analysed
  from USER_TABLES
where table_name = 'S';

   NUM_ROWS    BLOCKS    EMPTY_BLOCKS    AVG_SPACE    AVG_ROW_LEN    SAMPLE_SIZE       LAST_ANALYSED 
___________ _________ _______________ ____________ ______________ ______________ ___________________ 
       1000         5               0            0              4           1000 06.03.2023 20:18

The second statistic gathering operation is now reflected in the DBA_OPTSTAT_OPERATIONS view as expected:

select id, operation, target, start_time, end_time from dba_optstat_operations
where target = '"MARTINDBA"."S"' and start_time > systimestamp - interval '15' minute;

    ID             OPERATION             TARGET                             START_TIME                               END_TIME 
______ _____________________ __________________ ______________________________________ ______________________________________ 
   405 gather_table_stats    "MARTINDBA"."S"    06-MAR-23 08.14.03.752519000 PM GMT    06-MAR-23 08.14.03.778287000 PM GMT    
   406 gather_table_stats    "MARTINDBA"."S"    06-MAR-23 08.18.25.295581000 PM GMT    06-MAR-23 08.18.25.340237000 PM GMT 

Problems! We need to revert!

And here’s the point where we assume it all went wrong! Users calling, CPU load going through the roof because of bad execution plans etc. We need to revert back! This is easier to implement than getting approvals to do so:

begin
  dbms_stats.restore_table_stats(user,'S','06-MAR-23 08.15.03.752519000 PM GMT');
end;
/

I simply picked the timestamp a minute past the completion of the initial statistics gathering when the table was empty. The statistics restore operation is reflected in the stats history for the table so you can’t sneak that one in silently :)

SQL> select * from user_tab_stats_history;

   TABLE_NAME    PARTITION_NAME    SUBPARTITION_NAME                      STATS_UPDATE_TIME 
_____________ _________________ ____________________ ______________________________________ 
S                                                    06-MAR-23 08.14.03.769967000 PM GMT    
S                                                    06-MAR-23 08.18.25.335210000 PM GMT    
S                                                    06-MAR-23 08.27.16.956775000 PM GMT

But at least the stats are back to what they were:

select num_rows, blocks,empty_blocks,avg_space, avg_row_len, sample_size,
       to_char(last_analyzed, 'dd.mm.yyyy hh24:mi') last_analysed
  from USER_TABLES
where table_name = 'S';

   NUM_ROWS    BLOCKS    EMPTY_BLOCKS    AVG_SPACE    AVG_ROW_LEN    SAMPLE_SIZE       LAST_ANALYSED 
___________ _________ _______________ ____________ ______________ ______________ ___________________ 
          0         0               0            0              0              0 06.03.2023 20:14 

Great stuff!