Showing posts with label Performance Tuning. Show all posts
Showing posts with label Performance Tuning. Show all posts

Monday, August 17, 2026

Gather stats in Oracle - most commonly used DBMS_STATS.GATHER_TABLE_STATS parameters

Below is a comprehensive explanation of the most commonly used DBMS_STATS.GATHER_TABLE_STATS parameters and when you should use them.

Syntax

DBMS_STATS.GATHER_TABLE_STATS(
ownname => 'SCOTT',
tabname => 'EMP',
partname => NULL,
estimate_percent => DBMS_STATS.AUTO_SAMPLE_SIZE,
block_sample => FALSE,
method_opt => 'FOR ALL COLUMNS SIZE AUTO',
degree => DBMS_STATS.AUTO_DEGREE,
granularity => 'AUTO',
cascade => TRUE,
no_invalidate => DBMS_STATS.AUTO_INVALIDATE,
options => 'GATHER',
force => FALSE
);


Parameter Explanation Table

ParameterPurposeTypical ValueWhy NeededRecommendation
OWNNAMESchema owner'HR'Identifies schema containing tableMandatory
TABNAMETable name'EMPLOYEES'Table for stats collectionMandatory
PARTNAMESpecific partition'P202501'Gather stats for only one partitionUse only for partitioned tables
ESTIMATE_PERCENTSample size percentageAUTO_SAMPLE_SIZEDetermines how much data Oracle samplesUse AUTO_SAMPLE_SIZE
BLOCK_SAMPLEBlock sampling methodTRUE/FALSESamples blocks instead of rowsUsually FALSE
METHOD_OPTHistogram collection method'FOR ALL COLUMNS SIZE AUTO'Controls histogram creationMost important parameter
DEGREEParallelismAUTO_DEGREESpeeds up large-table stats collectionUse AUTO
GRANULARITYPartition/global stats level'AUTO'Determines partition-level stats collectionUse AUTO
CASCADEGather index stats alsoTRUEUpdates index statisticsAlways TRUE
NO_INVALIDATECursor invalidation controlAUTO_INVALIDATEDetermines when execution plans become obsoleteUse AUTO
OPTIONSCollection modeGATHER AUTO, GATHER STALEControls which objects get analyzedUsually GATHER STALE
FORCEIgnore lock statusTRUE/FALSEGather stats on locked objectsUsually FALSE

Important Parameters Deep Dive

1. ESTIMATE_PERCENT

AUTO_SAMPLE_SIZE

estimate_percent => DBMS_STATS.AUTO_SAMPLE_SIZE

Why Use It?

Oracle decides the optimal sample size.

Benefits:

  • More accurate cardinality estimates
  • Reduced runtime
  • Recommended by Oracle

Avoid

estimate_percent => 100

unless doing optimizer troubleshooting.


2. METHOD_OPT

This parameter has the most impact on optimizer plans.

Recommended

method_opt => 'FOR ALL COLUMNS SIZE AUTO'

What It Does

Oracle creates histograms only when useful.

Example:

Column:

STATUS
-------
ACTIVE 99%
INACTIVE 1%

Without histogram:

Optimizer assumes even distribution.

With histogram:

Optimizer understands skew.

Result:

  • Better index selection
  • Better join ordering
  • Better cardinality estimates

Other Options

ValuePurpose
FOR ALL COLUMNS SIZE AUTOOracle chooses histograms
FOR ALL COLUMNS SIZE 1No histograms
FOR ALL INDEXED COLUMNS SIZE AUTOHistograms only on indexed columns
FOR COLUMNS SIZE 254 col1,col2Maximum histogram detail

3. CASCADE

Example

cascade => TRUE

What It Does

Gathers statistics on:

  • Table
  • Indexes

Without it:

Table stats updated
Index stats stale

This can lead to poor plans.

Recommended:

cascade => TRUE


4. NO_INVALIDATE

AUTO_INVALIDATE

no_invalidate => DBMS_STATS.AUTO_INVALIDATE

Purpose

When statistics change Oracle may invalidate cached SQL execution plans.

Options:

ValueEffect
TRUEKeep existing plans
FALSEImmediately invalidate plans
AUTO_INVALIDATEOracle decides

Recommended

AUTO_INVALIDATE

for production systems.


5. DEGREE

Controls parallel workers.

Examples

degree => 8

degree => DBMS_STATS.AUTO_DEGREE

Usage

Table SizeRecommendation
< 10 GBAUTO
10-100 GBAUTO
> 100 GB4-16

6. GRANULARITY

Used for partitioned tables.

Example

granularity => 'AUTO'

Options:

ValueDescription
AUTOOracle decides
GLOBALGlobal stats only
PARTITIONPartition stats only
ALLGlobal + Partition
SUBPARTITIONSubpartition stats

Recommendation

AUTO


7. OPTIONS

Very important for maintenance windows.

GATHER

options => 'GATHER'

Gather regardless of staleness.


GATHER STALE

options => 'GATHER STALE'

Only tables with stale statistics.

Recommended for production.


GATHER AUTO

options => 'GATHER AUTO'

Gathers:

  • Missing stats
  • Stale stats
  • Oracle-selected objects

Recommended when running manually.


LIST STALE

options => 'LIST STALE'

Report only.

No gathering performed.


8. FORCE

Example

force => TRUE

Normally locked statistics are skipped.

force => TRUE

forces collection anyway.

Mostly used by DBAs during troubleshooting.


Best Production Script

For Oracle 12c/19c/21c:

BEGIN
DBMS_STATS.GATHER_TABLE_STATS(
ownname => 'SCHEMA_NAME',
tabname => 'TABLE_NAME',
estimate_percent => DBMS_STATS.AUTO_SAMPLE_SIZE,
method_opt => 'FOR ALL COLUMNS SIZE AUTO',
cascade => TRUE,
degree => DBMS_STATS.AUTO_DEGREE,
granularity => 'AUTO',
no_invalidate => DBMS_STATS.AUTO_INVALIDATE
);
END;
/

Best Production Schema Script

BEGIN
DBMS_STATS.GATHER_SCHEMA_STATS(
ownname => 'SCHEMA_NAME',
options => 'GATHER STALE',
estimate_percent => DBMS_STATS.AUTO_SAMPLE_SIZE,
method_opt => 'FOR ALL COLUMNS SIZE AUTO',
cascade => TRUE,
degree => DBMS_STATS.AUTO_DEGREE
);
END;
/

DBA Recommendation

For Oracle 19c/21c production systems, the combination below provides the best balance between optimizer accuracy, maintenance window duration, and plan stability:

ParameterRecommended Value
ESTIMATE_PERCENTAUTO_SAMPLE_SIZE
METHOD_OPTFOR ALL COLUMNS SIZE AUTO
CASCADETRUE
DEGREEAUTO_DEGREE
GRANULARITYAUTO
NO_INVALIDATEAUTO_INVALIDATE
OPTIONSGATHER STALE (schema) / default GATHER (table)
FORCEFALSE


=> Dynamic SQL query that generates DBMS_STATS.GATHER_TABLE_STATS statements for all tables in a schema, use one of the following.

Generate Gather Stats Commands for All Tables in a Schema

SELECT 'EXEC DBMS_STATS.GATHER_TABLE_STATS('||
'ownname=>'''||owner||''','||
'tabname=>'''||table_name||''','||
'estimate_percent=>DBMS_STATS.AUTO_SAMPLE_SIZE,'||
'method_opt=>''FOR ALL COLUMNS SIZE AUTO'','||
'cascade=>TRUE,'||
'degree=>DBMS_STATS.AUTO_DEGREE);'
FROM dba_tables
WHERE owner = UPPER('&SCHEMA_NAME')
ORDER BY table_name;

Generate Gather Stats Only for Stale Tables

SELECT 'EXEC DBMS_STATS.GATHER_TABLE_STATS('||
'ownname=>'''||owner||''','||
'tabname=>'''||table_name||''','||
'estimate_percent=>DBMS_STATS.AUTO_SAMPLE_SIZE,'||
'method_opt=>''FOR ALL COLUMNS SIZE AUTO'','||
'cascade=>TRUE,'||
'degree=>DBMS_STATS.AUTO_DEGREE);'
FROM dba_tab_statistics
WHERE owner = UPPER('&SCHEMA_NAME')
AND stale_stats = 'YES'
ORDER BY table_name;

Generate Gather Stats for Tables with Missing Statistics

SELECT 'EXEC DBMS_STATS.GATHER_TABLE_STATS('||
'ownname=>'''||owner||''','||
'tabname=>'''||table_name||''','||
'estimate_percent=>DBMS_STATS.AUTO_SAMPLE_SIZE,'||
'method_opt=>''FOR ALL COLUMNS SIZE AUTO'','||
'cascade=>TRUE);'
FROM dba_tables
WHERE owner = UPPER('&SCHEMA_NAME')
AND last_analyzed IS NULL;

Execute Automatically Using Dynamic PL/SQL

BEGIN
FOR r IN (
SELECT owner, table_name
FROM dba_tab_statistics
WHERE owner = 'SCHEMA_NAME'
AND stale_stats = 'YES'
)
LOOP
DBMS_STATS.GATHER_TABLE_STATS(
ownname => r.owner,
tabname => r.table_name,
estimate_percent => DBMS_STATS.AUTO_SAMPLE_SIZE,
method_opt => 'FOR ALL COLUMNS SIZE AUTO',
cascade => TRUE,
degree => DBMS_STATS.AUTO_DEGREE
);
END LOOP;
END;
/

Parallel Execution Script Generator (Useful for Large Schemas)

SELECT 'EXEC DBMS_STATS.GATHER_TABLE_STATS('||
'ownname=>'''||owner||''','||
'tabname=>'''||table_name||''','||
'estimate_percent=>DBMS_STATS.AUTO_SAMPLE_SIZE,'||
'method_opt=>''FOR ALL COLUMNS SIZE AUTO'','||
'cascade=>TRUE,'||
'degree=>8);'
FROM dba_tables
WHERE owner='SCHEMA_NAME';

For Oracle 19c/21c production environments, generally recommend gathering only stale statistics using AUTO_SAMPLE_SIZE, FOR ALL COLUMNS SIZE AUTO, and CASCADE=>TRUE rather than forcing stats collection on every table. This reduces maintenance time while keeping optimizer plans accurate.

Monday, August 3, 2026

How to verify whether TIMED_STATISTICS changed between the oracle AWR database snapshots ?

To verify whether TIMED_STATISTICS changed between the snapshots used in your AWR Diff report, you can check the historical parameter values stored in AWR.

Method 1: Check Parameter History from AWR

SELECT s.snap_id,
s.instance_number,
TO_CHAR(s.begin_interval_time,'DD-MON-YYYY HH24:MI') begin_time,
p.value
FROM dba_hist_parameter p,
dba_hist_snapshot s
WHERE p.snap_id = s.snap_id
AND p.dbid = s.dbid
AND p.instance_number = s.instance_number
AND p.parameter_name = 'timed_statistics'
ORDER BY s.snap_id;

Look for changes in the VALUE column (TRUE/FALSE).


Method 2: Check Only the Snapshots Used in the Report

If your report compared snapshots 100-101 with 200-201:

SELECT snap_id,
value
FROM dba_hist_parameter
WHERE parameter_name = 'timed_statistics'
AND snap_id IN (100,101,200,201)
ORDER BY snap_id;

Example output:

SNAP_ID VALUE
------- -----
100 FALSE
101 FALSE
200 TRUE
201 TRUE

This would explain the warning.


Method 3: Find Exactly When the Value Changed

SELECT snap_id,
value,
LAG(value) OVER (ORDER BY snap_id) prev_value
FROM dba_hist_parameter
WHERE parameter_name = 'timed_statistics'
ORDER BY snap_id;

Or show only the change point:

WITH p AS
(
SELECT snap_id,
value,
LAG(value) OVER (ORDER BY snap_id) prev_value
FROM dba_hist_parameter
WHERE parameter_name = 'timed_statistics'
)
SELECT *
FROM p
WHERE value <> prev_value;


Method 4: Check Database Restart Between Snapshots

Sometimes the warning appears because the database was restarted and initialization parameters changed.

SELECT snap_id,
startup_time,
begin_interval_time
FROM dba_hist_snapshot
ORDER BY snap_id;

Look for a change in STARTUP_TIME between your snapshot ranges.


RAC Environment

If this is RAC, check all instances:

SELECT snap_id,
instance_number,
value
FROM dba_hist_parameter
WHERE parameter_name = 'timed_statistics'
ORDER BY instance_number, snap_id;

A change on even one instance can trigger the warning.


How to generate Oracle AWR Difference Report (AWR Diff Report) ?

 You can generate an AWR Difference Report (AWR Diff Report) using Oracle's standard script awrddrpt.sql available under the Oracle Home directory.

1. Connect as SYSDBA

sqlplus / as sysdba

or

sqlplus sys/password as sysdba


2. Run the AWR Difference Report Script

The script is located at:

$ORACLE_HOME/rdbms/admin/awrddrpt.sql

Execute it from SQL*Plus:

SQL> @?/rdbms/admin/awrddrpt.sql

(? automatically resolves to ORACLE_HOME)


3. Provide Required Inputs

The script will prompt for:

Source Database

Select:

  • DBID
  • Instance Number
  • Begin Snapshot ID
  • End Snapshot ID

Example:

Enter value for dbid : 123456789
Enter value for inst_num : 1
Enter value for begin_snap : 100
Enter value for end_snap : 101

Target Database / Comparison Period

Provide another snapshot range:

Enter value for dbid : 123456789
Enter value for inst_num : 1
Enter value for begin_snap : 200
Enter value for end_snap : 201

This can be:

  • Same database, different time period
  • Different RAC instance
  • Different database (if AWR data exists)

4. Choose Report Format

The script will ask:

Specify the Report Type

Enter 'html' for an HTML report
Enter 'text' for plain text report

Example:

html


5. Specify Output File

Example:

Enter value for report_name: awr_diff_peak_vs_normal.html

The report gets generated in the current SQL*Plus working directory.


Finding Snapshot IDs

Before running the report, identify snapshot IDs:

SELECT
snap_id,
begin_interval_time,
end_interval_time
FROM dba_hist_snapshot
ORDER BY snap_id;

For RAC:

SELECT
instance_number,
snap_id,
begin_interval_time
FROM dba_hist_snapshot
ORDER BY instance_number, snap_id;


Useful Report Types

Compare Good vs Bad Performance

Example:

PeriodSnap Range
Good Performance100-101
Slow Performance200-201

Generate AWR Diff Report to identify:

  • SQL elapsed time differences
  • Wait event changes
  • CPU usage increases
  • I/O bottlenecks
  • Execution plan regressions

Non-Interactive Generation

You can also call the package directly:

SELECT *
FROM TABLE(
dbms_workload_repository.awr_diff_report_html(
123456789,
1,
100,
101,
123456789,
1,
200,
201
));

For text format:

SELECT *
FROM TABLE(
dbms_workload_repository.awr_diff_report_text(
123456789,
1,
100,
101,
123456789,
1,
200,
201
));

Quick Check of Available AWR Scripts in Oracle Home

cd $ORACLE_HOME/rdbms/admin

ls awr*.sql

Common scripts:

awrrpt.sql -- Single AWR report
awrgrpt.sql -- RAC Global AWR report
awrddrpt.sql -- AWR Difference report
awrgdrpt.sql -- Global AWR Difference report (RAC)
ashrpt.sql -- ASH report
ashrpti.sql -- ASH report for specific instance

For a RAC environment, use awrgdrpt.sql instead of awrddrpt.sql when you want to compare cluster-wide performance across snapshot ranges.

Thursday, July 30, 2026

How to Check SQL_IDs Having Multiple PLAN_HASH_VALUE ?

identify SQL_IDs having multiple PLAN_HASH_VALUEs by grouping on SQL_ID and counting distinct PLAN_HASH_VALUE. This is very useful for finding SQLs with plan instability or plan changes.

Oracle execution plans can be checked from the cursor cache using views like V$SQL, and detailed plans can be displayed using DBMS_XPLAN.DISPLAY_CURSOR .

Oracle documents that DBMS_XPLAN can display plans from cached cursors, AWR, SQL tuning sets, and SQL plan baseline


1. Check SQL_IDs Having Multiple PLAN_HASH_VALUEs in Current Memory

Use this query on V$SQL.

SELECT sql_id,
COUNT(DISTINCT plan_hash_value) AS plan_count,
LISTAGG(DISTINCT plan_hash_value, ', ')
WITHIN GROUP (ORDER BY plan_hash_value) AS plan_hash_values,
SUM(executions) AS total_executions
FROM v$sql
WHERE sql_id IS NOT NULL
AND plan_hash_value <> 0
GROUP BY sql_id
HAVING COUNT(DISTINCT plan_hash_value) > 1
ORDER BY plan_count DESC, total_executions DESC;

Meaning

  • SQL_ID: SQL statement identifier
  • PLAN_COUNT: number of different plans used by the SQL
  • PLAN_HASH_VALUES: list of plan hash values
  • TOTAL_EXECUTIONS: total executions from cursor cache

If one SQL_ID has more than one PLAN_HASH_VALUE, it means Oracle has generated multiple execution plans for the same SQL.


2. Detailed Child Cursor Level Query

This gives more detail for each SQL_ID, child cursor, and plan hash value.

SELECT sql_id,
child_number,
plan_hash_value,
executions,
parsing_schema_name,
optimizer_mode,
loaded_versions,
invalidations,
first_load_time,
last_active_time
FROM v$sql
WHERE sql_id IN (
SELECT sql_id
FROM v$sql
WHERE sql_id IS NOT NULL
AND plan_hash_value <> 0
GROUP BY sql_id
HAVING COUNT(DISTINCT plan_hash_value) > 1
)
ORDER BY sql_id, plan_hash_value, child_number;

This is useful when you want to see which child cursor used which plan.


3. Group PLAN_HASH_VALUE Against Each SQL_ID

If you want a grouped summary of all SQL_IDs and their plan hash values:

SELECT sql_id,
plan_hash_value,
COUNT(*) AS child_cursor_count,
SUM(executions) AS executions,
MIN(first_load_time) AS first_load_time,
MAX(last_active_time) AS last_active_time
FROM v$sql
WHERE sql_id IS NOT NULL
AND plan_hash_value <> 0
GROUP BY sql_id, plan_hash_value
ORDER BY sql_id, executions DESC;

This output shows one row per:

SQL_ID + PLAN_HASH_VALUE

Example output:

SQL_ID PLAN_HASH_VALUE CHILD_CURSOR_COUNT EXECUTIONS
------------- --------------- ------------------ ----------
abc123xyz789 1234567890 2 500
abc123xyz789 9876543210 1 20
def456pqr111 5555555555 1 100


4. Check Only One Specific SQL_ID

If you want to check plan hash values for one SQL ID:

SELECT sql_id,
plan_hash_value,
COUNT(*) AS child_cursor_count,
SUM(executions) AS total_executions,
MIN(child_number) AS min_child_number,
MAX(child_number) AS max_child_number
FROM v$sql
WHERE sql_id = '&sql_id'
GROUP BY sql_id, plan_hash_value
ORDER BY total_executions DESC;

Example:

SELECT sql_id,
plan_hash_value,
COUNT() AS child_cursor_count,
SU(executions) AS total_executions,
* MIN(child_number) AS min_chi*d_number,
MAX(child_number)AS max_child_number
FROM v$sql
WERE sql_id = '5g8t1m9n2abc3'
GROUP BY sql_id, plan_hash_value
ORDER BY total_executions DESC;


5. Historical Check from AWR

If you want to check SQL_IDs with multiple plans historically, use DBA_HIST_SQLSTAT.

SELECT sql_id,
COUNT(DISTINCT plan_hash_value) AS plan_count,
LISTAGG(DISTINCT plan_hash_value, ', ')
WITHIN GROUP (ORDER BY plan_hash_value) AS plan_hash_values,
SUM(executions_delta) AS total_executions
FROM dba_hist_sqlstat
WHERE sql_id IS NOT NULL
AND plan_hash_value <> 0
GROUP BY sql_id
HAVING COUNT(DISTINCT plan_hash_value) > 1
ORDER BY plan_count DESC, total_executions DESC;

Use this when the SQL is not present in V$SQL, or you want to analyze old plan changes. Oracle documents that DBMS_XPLAN.DISPLAY_AWR can display execution plans stored in AWR. [docs.oracle.com]


6. Historical Grouping by SQL_ID and PLAN_HASH_VALUE

SELECT sql_id,
plan_hash_value,
SUM(executions_delta) AS executions,
ROUND(SUM(elapsed_time_delta) / 1000000, 2) AS elapsed_seconds,
ROUND(SUM(cpu_time_delta) / 1000000, 2) AS cpu_seconds,
SUM(buffer_gets_delta) AS buffer_gets,
SUM(disk_reads_delta) AS disk_reads,
MIN(snap_id) AS first_snap_id,
MAX(snap_id) AS last_snap_id
FROM dba_hist_sqlstat
WHERE sql_id IS NOT NULL
AND plan_hash_value <> 0
GROUP BY sql_id, plan_hash_value
ORDER BY sql_id, executions DESC;

This is good for performance comparison between plans.


7. Best Query for Plan Instability Report

For DBA troubleshooting, this is usually the most useful query:

SELECT sql_id,
COUNT(DISTINCT plan_hash_value) AS plan_count,
SUM(executions_delta) AS total_executions,
ROUND(SUM(elapsed_time_delta) / 1000000, 2) AS total_elapsed_sec,
ROUND(SUM(cpu_time_delta) / 1000000, 2) AS total_cpu_sec,
SUM(buffer_gets_delta) AS total_buffer_gets,
SUM(disk_reads_delta) AS total_disk_reads
FROM dba_hist_sqlstat
WHERE sql_id IS NOT NULL
AND plan_hash_value <> 0
GROUP BY sql_id
HAVING COUNT(DISTINCT plan_hash_value) > 1
ORDER BY total_elapsed_sec DESC;

This shows SQLs that had multiple plans and consumed the most elapsed time.


8. RAC-Aware Query

If your database is RAC, include INST_ID from GV$SQL.

SELECT inst_id,
sql_id,
plan_hash_value,
COUNT(*) AS child_cursor_count,
SUM(executions) AS executions,
MAX(last_active_time) AS last_active_time
FROM gv$sql
WHERE sql_id IS NOT NULL
AND plan_hash_value <> 0
GROUP BY inst_id, sql_id, plan_hash_value
ORDER BY sql_id, inst_id, executions DESC;

To find SQL_IDs with multiple plans across RAC:

SELECT sql_id,
COUNT(DISTINCT plan_hash_value) AS plan_count,
LISTAGG(DISTINCT plan_hash_value, ', ')
WITHIN GROUP (ORDER BY plan_hash_value) AS plan_hash_values,
SUM(executions) AS total_executions
FROM gv$sql
WHERE sql_id IS NOT NULL
AND plan_hash_value <> 0
GROUP BY sql_id
HAVING COUNT(DISTINCT plan_hash_value) > 1
ORDER BY plan_count DESC, total_executions DESC;


9. Display Execution Plan for Each PLAN_HASH_VALUE

After finding multiple plan hash values, display the plan using:

SELECT *
FROM TABLE(
DBMS_XPLAN.DISPLAY_CURSOR(
sql_id => '&sql_id',
cursor_child_no => NULL,
format => 'ALLSTATS LAST +PEEKED_BINDS +OUTLINE'
)
);

For historical AWR plan:

SELECT *
FROM TABLE(
DBMS_XPLAN.DISPLAY_AWR(
sql_id => '&sql_id',
plan_hash_value => &plan_hash_value,
format => 'TYPICAL'
)
);


Recommended Quick Script

SET LINESIZE 220
SET PAGESIZE 100
COLUMN sql_id FORMAT A15
COLUMN plan_hash_values FORMAT A80

PROMPT === SQL_IDs with Multiple Plan Hash Values from V$SQL ===

SELECT sql_id,
COUNT(DISTINCT plan_hash_value) AS plan_count,
LISTAGG(DISTINCT plan_hash_value, ', ')
WITHIN GROUP (ORDER BY plan_hash_value) AS plan_hash_values,
SUM(executions) AS total_executions
FROM v$sql
WHERE sql_id IS NOT NULL
AND plan_hash_value <> 0
GROUP BY sql_id
HAVING COUNT(DISTINCT plan_hash_value) > 1
ORDER BY plan_count DESC, total_executions DESC;


Plan Hash Grouping Summary

Use this if your goal is simply:

“Group plan hash value against SQL ID.”

SELECT sql_id,
plan_hash_value,
COUNT(*) AS cursor_count,
SUM(executions) AS executions
FROM v$sql
WHERE sql_id IS NOT NULL
AND plan_hash_value <> 0
GROUP BY sql_id, plan_hash_value
ORDER BY sql_id, executions DESC;


SQL_IDs having multiple plan hash values and executions more than 5


SELECT sql_id,
       plan_hash_value,
       COUNT(*) AS child_cursor_count,
       SUM(executions) AS executions,
       MIN(first_load_time) AS first_load_time,
       MAX(last_active_time) AS last_active_time
FROM   v$sql
WHERE  sql_id IS NOT NULL
AND    plan_hash_value <> 0
GROUP BY sql_id, plan_hash_value
HAVING SUM(executions) > 5
ORDER BY sql_id, executions DESC;

SQL_IDs Having More Than One PLAN_HASH_VALUE


SELECT sql_id,
COUNT(DISTINCT plan_hash_value) AS plan_hash_count,
LISTAGG(DISTINCT plan_hash_value, ', ')
WITHIN GROUP (ORDER BY plan_hash_value) AS plan_hash_values,
SUM(executions) AS total_executions
FROM v$sql
WHERE sql_id IS NOT NULL
AND plan_hash_value <> 0
GROUP BY sql_id
HAVING COUNT(DISTINCT plan_hash_value) > 1
ORDER BY plan_hash_count DESC, total_executions DESC;


Yes, to find SQL_IDs where PLAN_HASH_VALUE is more than 1, meaning the same SQL_ID has used multiple execution plans, use COUNT(DISTINCT plan_hash_value) > 1.

SQL_IDs Having More Than One PLAN_HASH_VALUE

SELECT sql_id,
COUNT(DISTINCT plan_hash_value) AS plan_hash_count,
LISTAGG(DISTINCT plan_hash_value, ', ')
WITHIN GROUP (ORDER BY plan_hash_value) AS plan_hash_values,
SUM(executions) AS total_executions
FROM v$sql
WHERE sql_id IS NOT NULL
AND plan_hash_value <> 0
GROUP BY sql_id
HAVING COUNT(DISTINCT plan_hash_value) > 1
ORDER BY plan_hash_count DESC, total_executions DESC;

If You Also Want Executions Greater Than 5

SELECT sql_id,
COUNT(DISTINCT plan_hash_value) AS plan_hash_count,
LISTAGG(DISTINCT plan_hash_value, ', ')
WITHIN GROUP (ORDER BY plan_hash_value) AS plan_hash_values,
SUM(executions) AS total_executions
FROM v$sql
WHERE sql_id IS NOT NULL
AND plan_hash_value <> 0
GROUP BY sql_id
HAVING COUNT(DISTINCT plan_hash_value) > 1
AND SUM(executions) > 5
ORDER BY plan_hash_count DESC, total_executions DESC;

Detailed Output Per SQL_ID and PLAN_HASH_VALUE

Use this after identifying SQL_IDs with multiple plans:

SELECT sql_id,
plan_hash_value,
COUNT(*) AS child_cursor_count,
SUM(executions) AS executions,
MIN(first_load_time) AS first_load_time,
MAX(last_active_time) AS last_active_time
FROM v$sql
WHERE sql_id IN (
SELECT sql_id
FROM v$sql
WHERE sql_id IS NOT NULL
AND plan_hash_value <> 0
GROUP BY sql_id
HAVING COUNT(DISTINCT plan_hash_value) > 1
)
AND plan_hash_value <> 0
GROUP BY sql_id, plan_hash_value
ORDER BY sql_id, executions DESC;

Best RAC Version Using GV$SQL

If it is a RAC database, use this:

SELECT sql_id,
COUNT(DISTINCT plan_hash_value) AS plan_hash_count,
LISTAGG(DISTINCT plan_hash_value, ', ')
WITHIN GROUP (ORDER BY plan_hash_value) AS plan_hash_values,
SUM(executions) AS total_executions
FROM gv$sql
WHERE sql_id IS NOT NULL
AND plan_hash_value <> 0
GROUP BY sql_id
HAVING COUNT(DISTINCT plan_hash_value) > 1
ORDER BY plan_hash_count DESC, total_executions DESC;

Key Condition

HAVING COUNT(DISTINCT plan_hash_value) > 1

This is the main condition to find SQL IDs having multiple plan hash values.

DBA Tip

If a SQL_ID has multiple PLAN_HASH_VALUEs, check:

  • Statistics changes
  • Bind peeking
  • Adaptive cursor sharing
  • Different optimizer environment
  • Index creation or drop
  • SQL profile
  • SQL baseline
  • Object invalidation
  • RAC instance-specific plan difference

Step by Step How to Check PLAN_HASH_VALUE Against SQL_ID in Oracle ?


 Check PLAN_HASH_VALUE Against SQL_ID in Oracle ?

1. Objective

To verify which PLAN_HASH_VALUE is associated with a specific SQL_ID in Oracle.

This is useful when:

  • Checking if SQL execution plan changed
  • Troubleshooting SQL performance issues
  • Comparing current and historical plans
  • Validating SQL Plan Baseline usage
  • Investigating plan regression

2. Required Input

You need the SQL ID.

Example:

SQL_ID = '5g8t1m9n2abc3'

If you do not have the SQL ID, you can search it from V$SQL using part of the SQL text.

SELECT sql_id,
child_number,
plan_hash_value,
sql_text
FROM v$sql
WHERE sql_text LIKE '%your_unique_sql_text%'
AND sql_text NOT LIKE '%v$sql%';


3. Check Current PLAN_HASH_VALUE from Cursor Cache

Use this query when the SQL is currently present in the shared pool.

SET LINESIZE 200
SET PAGESIZE 100

SELECT sql_id,
child_number,
plan_hash_value,
executions,
parsing_schema_name,
last_active_time
FROM v$sql
WHERE sql_id = '&sql_id'
ORDER BY child_number;

Example

SELECT sql_id,
child_number,
plan_hash_value,
executions,
parsing_schema_name,
last_active_time
FROM v$sql
WHERE sql_id = '5g8t1m9n2abc3'
ORDER BY child_number;

How to Read the Output

If output shows:

SQL_ID CHILD_NUMBER PLAN_HASH_VALUE EXECUTIONS
------------- ------------ --------------- ----------
5g8t1m9n2abc3 0 1234567890 150
5g8t1m9n2abc3 1 9876543210 25

It means the same SQL ID has multiple child cursors and multiple execution plans.


4. Check Distinct PLAN_HASH_VALUE for a SQL_ID

Use this if you only want the list of unique plan hash values.

SELECT DISTINCT
sql_id,
plan_hash_value
FROM v$sql
WHERE sql_id = '&sql_id'
ORDER BY plan_hash_value;

Example

SELECT DISTINCT
sql_id,
plan_hash_value
FROM v$sql
WHERE sql_id = '5g8t1m9n2abc3'
ORDER BY plan_hash_value;


5. Check PLAN_HASH_VALUE with Execution Statistics

This query helps identify which plan is used most frequently.

SELECT sql_id,
plan_hash_value,
COUNT(*) child_count,
SUM(executions) total_executions,
MIN(first_load_time) first_load_time,
MAX(last_active_time) last_active_time
FROM v$sql
WHERE sql_id = '&sql_id'
GROUP BY sql_id, plan_hash_value
ORDER BY total_executions DESC;

Interpretation

  • CHILD_COUNT shows how many child cursors used the same plan.
  • TOTAL_EXECUTIONS shows how often that plan was executed.
  • Multiple PLAN_HASH_VALUE entries can indicate plan changes or different optimizer choices.

6. Display Current Execution Plan Using DBMS_XPLAN

Use DBMS_XPLAN.DISPLAY_CURSOR to see the actual cached execution plan. Oracle documentation states that DISPLAY_CURSOR displays the execution plan of a loaded cursor.

SELECT *
FROM TABLE(
DBMS_XPLAN.DISPLAY_CURSOR(
sql_id => '&sql_id',
cursor_child_no => NULL,
format => 'ALLSTATS LAST'
)
);

Example

SELECT *
FROM TABLE(
DBMS_XPLAN.DISPLAY_CURSOR(
sql_id => '5g8t1m9n2abc3',
cursor_child_no => NULL,
format => 'ALLSTATS LAST'
)
);

Notes

  • cursor_child_no => NULL displays all child cursors for that SQL ID.
  • ALLSTATS LAST shows actual runtime statistics for the last execution if statistics are available.
  • If no rows are returned, the SQL may not be available in the cursor cache.

7. Display Plan for a Specific Child Cursor

If the SQL ID has multiple child cursors, check a specific one.

SELECT *
FROM TABLE(
DBMS_XPLAN.DISPLAY_CURSOR(
sql_id => '&sql_id',
cursor_child_no => &child_number,
format => 'ALLSTATS LAST +PEEKED_BINDS +OUTLINE'
)
);

Example

SELECT *
FROM TABLE(
DBMS_XPLAN.DISPLAY_CURSOR(
sql_id => '5g8t1m9n2abc3',
cursor_child_no => 0,
format => 'ALLSTATS LAST +PEEKED_BINDS +OUTLINE'
)
);

This is useful when bind values, optimizer outlines, or child cursor differences need to be reviewed.


8. Check Historical PLAN_HASH_VALUE from AWR

If the SQL is no longer in memory, check AWR history.

SELECT sql_id,
plan_hash_value,
SUM(executions_delta) executions,
MIN(snap_id) first_snap_id,
MAX(snap_id) last_snap_id
FROM dba_hist_sqlstat
WHERE sql_id = '&sql_id'
GROUP BY sql_id, plan_hash_value
ORDER BY first_snap_id;

Example

SELECT sql_id,
plan_hash_value,
SUM(executions_delta) executions,
MIN(snap_id) first_snap_id,
MAX(snap_id) last_snap_id
FROM dba_hist_sqlstat
WHERE sql_id = '5g8t1m9n2abc3'
GROUP BY sql_id, plan_hash_value
ORDER BY first_snap_id;

Oracle documentation confirms that DBMS_XPLAN can display execution plans stored in AWR using DISPLAY_AWR


9. Display Historical Execution Plan from AWR

Use this when you know the historical PLAN_HASH_VALUE.

SELECT *
FROM TABLE(
DBMS_XPLAN.DISPLAY_AWR(
sql_id => '&sql_id',
plan_hash_value => &plan_hash_value,
format => 'TYPICAL'
)
);

Example

SELECT *
FROM TABLE(
DBMS_XPLAN.DISPLAY_AWR(
sql_id => '5g8t1m9n2abc3',
plan_hash_value => 1234567890,
format => 'TYPICAL'
)
);


10. Check Plan Changes Over Time

This query shows when each plan was used across AWR snapshots.

SELECT s.begin_interval_time,
s.end_interval_time,
st.sql_id,
st.plan_hash_value,
st.executions_delta,
ROUND(st.elapsed_time_delta / 1000000, 2) elapsed_sec,
ROUND(st.cpu_time_delta / 1000000, 2) cpu_sec,
st.buffer_gets_delta,
st.disk_reads_delta
FROM dba_hist_sqlstat st
JOIN dba_hist_snapshot s
ON st.snap_id = s.snap_id
AND st.dbid = s.dbid
AND st.instance_number = s.instance_number
WHERE st.sql_id = '&sql_id'
ORDER BY s.begin_interval_time, st.plan_hash_value;

This is helpful for identifying whether a SQL performance issue started after a plan change.


11. Check SQL Plan Baseline for the SQL_ID

If SQL Plan Management is used, check whether the SQL has a baseline.

SELECT sql_handle,
plan_name,
enabled,
accepted,
fixed,
optimizer_cost,
created,
last_executed
FROM dba_sql_plan_baselines
WHERE signature IN (
SELECT exact_matching_signature
FROM v$sql
WHERE sql_id = '&sql_id'
);

Oracle documentation states that DBMS_XPLAN can also display execution plans from SQL plan baselines. 


12. Display SQL Plan Baseline

If you get a SQL_HANDLE from the previous query, use:

SELECT *
FROM TABLE(
DBMS_XPLAN.DISPLAY_SQL_PLAN_BASELINE(
sql_handle => '&sql_handle',
format => 'TYPICAL'
)
);


13. Complete Quick-Check Script

Use this as a ready-to-run script.

SET LINESIZE 220
SET PAGESIZE 100
COLUMN sql_id FORMAT A15
COLUMN parsing_schema_name FORMAT A20
COLUMN first_load_time FORMAT A25
COLUMN last_active_time FORMAT A30

ACCEPT sql_id_input CHAR PROMPT 'Enter SQL_ID: '

PROMPT
PROMPT === Current Cursor Cache Plan Hash Values ===

SELECT sql_id,
child_number,
plan_hash_value,
executions,
parsing_schema_name,
first_load_time,
last_active_time
FROM v$sql
WHERE sql_id = '&sql_id_input'
ORDER BY child_number;

PROMPT
PROMPT === Distinct Current Plan Hash Values ===

SELECT DISTINCT
sql_id,
plan_hash_value
FROM v$sql
WHERE sql_id = '&sql_id_input'
ORDER BY plan_hash_value;

PROMPT
PROMPT === Historical AWR Plan Hash Values ===

SELECT sql_id,
plan_hash_value,
SUM(executions_delta) executions,
MIN(snap_id) first_snap_id,
MAX(snap_id) last_snap_id
FROM dba_hist_sqlstat
WHERE sql_id = '&sql_id_input'
GROUP BY sql_id, plan_hash_value
ORDER BY first_snap_id;

PROMPT
PROMPT === Current Execution Plan from Cursor Cache ===

SELECT *
FROM TABLE(
DBMS_XPLAN.DISPLAY_CURSOR(
sql_id => '&sql_id_input',
cursor_child_no => NULL,
format => 'ALLSTATS LAST'
)
);


14. Common Issues and Checks

Issue 1: No rows from V$SQL

Possible reasons:

  • SQL aged out of shared pool
  • SQL not executed recently
  • Wrong SQL ID
  • Query executed in another PDB or RAC instance

Use AWR:

SELECT sql_id,
plan_hash_value,
SUM(executions_delta) executions
FROM dba_hist_sqlstat
WHERE sql_id = '&sql_id'
GROUP BY sql_id, plan_hash_value;

Issue 2: Multiple PLAN_HASH_VALUE values

Possible reasons:

  • Bind peeking
  • Adaptive cursor sharing
  • Statistics changed
  • Different optimizer environment
  • SQL profile or baseline added
  • Object/index changes

Issue 3: DBMS_XPLAN.DISPLAY_CURSOR gives insufficient data

Try enhanced format:

SELECT *
FROM TABLE(
DBMS_XPLAN.DISPLAY_CURSOR(
sql_id => '&sql_id',
cursor_child_no => NULL,
format => 'ADVANCED ALLSTATS LAST +PEEKED_BINDS +OUTLINE'
)
);


Final Recommended Flow

Step 1: Confirm SQL_ID
Step 2: Check V$SQL for current PLAN_HASH_VALUE
Step 3: Check distinct PLAN_HASH_VALUE values
Step 4: Display current execution plan using DBMS_XPLAN.DISPLAY_CURSOR
Step 5: Check DBA_HIST_SQLSTAT for historical plans
Step 6: Display old plan using DBMS_XPLAN.DISPLAY_AWR
Step 7: Check SQL Plan Baseline if plan stability is required

Best Quick Query

If you need only one query to check the plan hash value for a SQL ID:

SELECT sql_id,
child_number,
plan_hash_value,
executions,
last_active_time
FROM v$sql
WHERE sql_id = '&sql_id'
ORDER BY child_number;

Troubleshooting Privileges

You may need access to views like V$SQL, V$SQL_PLAN, V$SESSION, and V$SQL_PLAN_STATISTICS_ALL for DISPLAY_CURSOR; Oracle documents these privilege requirements for DBMS_XPLAN.DISPLAY_CURSOR

what is catcon.pl (Catalog Container Script) and why very useful for Oracle Multitenant Database ?

  catcon.pl (Catalog Container Script) is an Oracle utility introduced with the Multitenant Architecture (CDB/PDB) to execute SQL scripts ...