Whenever I am sent a SQL statement that's not performing very well, my first question is always, "are stats fresh?". If statistics are stale, the optimizer will still use them because it's the best information it can get (well, in theory, and assuming you're not using dynamic sampling). If the statistics don't match the reality, the optimizer may choose a sub-optimal plan.
You can check every table for stale or missing statistics like this:
select table_name,owner
from dba_tab_statistics
where stale_stats = 'YES' or stale_stats is null;
However, wouldn't it be nice if you could check your 'problem' query?
If the query has been executed and it's still in the cursor cache (and you have the SQL ID), you can do something like this. Note that I don't claim this is a perfect solution (see the caveat below), but it's pretty cool IMO:
set pagesize 100
set linesize 150
set trims off
set tab off
set verify off
column table_name format a50
column index_name format a50
column object_type format a40
column owner format a40
accept sql_id prompt 'Enter the SQL ID: '
PROMPT ==========
PROMPT Tables
PROMPT ==========
with plan_tables as (
select distinct object_name,object_owner, object_type
from v$sql_plan
where object_type like 'TABLE%'
and sql_id = '&sql_id')
select t.object_owner owner,
t.object_name table_name,
t.object_type object_type,
decode(stale_stats,'NO','OK',NULL, 'NO STATS!', 'STALE!') staleness
from dba_tab_statistics s,
plan_tables t
where s.table_name = t.object_name
and s.owner = t.object_owner
and s.partition_name is null
and s.subpartition_name is null
order by t.object_owner, t.object_name;
PROMPT ==========
PROMPT Indexes
PROMPT ==========
with plan_indexes as (
select distinct object_name,object_owner, object_type
from v$sql_plan
where object_type like 'INDEX%'
and sql_id = '&sql_id')
select i.object_owner owner,
i.object_name index_name,
i.object_type object_type,
decode(stale_stats,'NO','OK',NULL, 'NO STATS!', 'STALE!') staleness
from dba_ind_statistics s,
plan_indexes i
where s.index_name = i.object_name
and s.owner = i.object_owner
and s.partition_name is null
and s.subpartition_name is null
order by i.object_owner, i.object_name;
This will list the staleness status of tables and indexes accessed by the relevant SQL statement. You can also retrieve plans from AWR and SQL tuning sets if the cursor cache doesn't contain the statement you want. Alternatively, you can join the plan data with DBA_TAB_MODIFICATIONS, and see exactly how much DML activity has been going on with the tables accessed by the SQL statement.
I have a caveat! Use this information as a guide. A SQL statement in the cursor cache may have been parsed hours or even days ago, so it could have been parsed under completely different circumstances (maybe all stats were fresh or even wholly stale). In addition, if you have gathered statistics recently (with default parameters), the SQL statement in the cursor cache might not have been re-parsed to see the updated statistics. The re-parse does not happen immediately, so the SQL statement could be using a plan based on stale statistics even if everything looks fresh. Eventually, cursor invalidation will initiate a re-parse and the SQL statement will see the new statistics. Be aware of these possibilities, but at least you can tell whether or not a freshly-parsed SQL statement will see up-to-date statistics.
Nigel is the product manager for the Oracle Optimizer. He's worked with Oracle technology since 1988 and joined Oracle in 1996. He's been involved in a wide variety of roles including development, benchmarking, consulting and pre-sales.