Skip to main content
AdministrationPerformance Tuning

Oracle SQL Plan Instability: Why SQL Profiles Are Not Enough and SQL Plan Baselines Are the Reliable Solution

By May 13, 2025No Comments5 min read

In performance-critical environments, having consistent SQL execution plans is essential. A common challenge arises when Oracle’s Cost-Based Optimizer switches execution plans, resulting in unpredictable performance.

We recently encountered this scenario where a query ran with excellent performance using one plan, but occasionally degraded due to the optimizer choosing an alternate, inefficient plan — even with a SQL Profile enabled. This blog documents the issue, our attempted solutions, and why SQL Plan Baselines ultimately proved to be the right fix.

Business Use Case

A financial reporting query on a table FIN_STAGE.account_snapshot_journal was used to retrieve account-level data for each branch based on latest timestamps and filtering on valid date ranges. The SQL used timestamp range filtering, complex CASE logic, and joined aggregated data back to the base set.

Example Query

SELECT c.max_time, c.branch_code, c.acct_no,
       b.limit_amt, b.avail_amt, b.status_flag, b.latest_flag
FROM (
    SELECT a.txn_time, a.limit_amt, a.avail_amt, a.branch_code,
           CASE
               WHEN EXTRACT(YEAR FROM a.valid_date) = EXTRACT(YEAR FROM SYSDATE) THEN 'ACTIVE'
               WHEN a.valid_date <= SYSDATE THEN 'ACTIVE'
               WHEN EXTRACT(YEAR FROM a.valid_date) = EXTRACT(YEAR FROM ADD_MONTHS(SYSDATE, -12)) THEN 'FUTURE'
               WHEN a.valid_date > ADD_MONTHS(SYSDATE, -24) THEN 'FUTURE'
               ELSE NULL
           END AS status_flag,
           'Y' AS latest_flag
    FROM FIN_STAGE.account_snapshot_journal a
    WHERE txn_time BETWEEN :1 AND :2
) b
JOIN (
    SELECT MAX(txn_time) AS max_time, branch_code, acct_no
    FROM FIN_STAGE.account_snapshot_journal
    WHERE txn_time BETWEEN :3 AND :4
    GROUP BY branch_code, acct_no
) c ON b.branch_code = c.branch_code AND b.txn_time = c.max_time;

The Problem

The query intermittently switched between two execution plans:

  • Plan Hash 1311461192: Optimal plan — executed in ~70ms
  • Plan Hash 492203874: Suboptimal plan — took over 300 seconds with high logical I/O

Even after applying a SQL Profile, the optimizer occasionally reverted to the bad plan, leading to application delays.

SQL Profile: First Attempt

We generated a SQL Profile using Oracle SQLT or coe_xfr_sql_profile.sql to push the optimizer toward the good plan.

SELECT name, status
FROM dba_sql_profiles
WHERE name = 'coe_c47h6yfcvbw6k_1311461192';

The profile was successfully created and enabled. For a couple of weeks, it ensured consistent performance.

However, this was short-lived

Issue Returns: SQL Profile Not Enough

Despite having the SQL Profile in place, AWR and v$sql showed the optimizer had again chosen the alternate plan 492203874. Analysis confirmed:

  • Same SQL ID and SQL text
  • Same bind values
  • SQL Profile was still enabled

Root Cause:
SQL Profiles provide guidance to the optimizer — not enforcement. They can be ignored if the optimizer determines that another plan is more “appropriate” based on internal costing or runtime metadata.

SQL Patch: Enforcing the Plan Manually

As an intermediate step before using SQL Plan Baseline, we created a SQL Patch with specific outline hints to lock the optimizer behavior.

SQL Patch Code

BEGIN
  DBMS_SQLDIAG.create_sql_patch(
    sql_id     => 'c47h6yfcvbw6k',
    hint_text  => q'[
      IGNORE_OPTIM_EMBEDDED_HINTS
      OPTIMIZER_FEATURES_ENABLE('19.1.0')
      DB_VERSION('19.1.0')
      ALL_ROWS
      OUTLINE_LEAF(@"SEL$3")
      OUTLINE_LEAF(@"SEL$55EFA6E7")
      MERGE(@"SEL$F5BB74E1" >"SEL$4")
      OUTLINE(@"SEL$4")
      OUTLINE(@"SEL$F5BB74E1")
      MERGE(@"SEL$2" >"SEL$1")
      OUTLINE(@"SEL$1")
      OUTLINE(@"SEL$2")
      NO_ACCESS(@"SEL$55EFA6E7" "C"@"SEL$1")
      INDEX(@"SEL$55EFA6E7" "A"@"SEL$2" ("ACCOUNT_SNAPSHOT_JOURNAL"."TXN_TIME"))
      LEADING(@"SEL$55EFA6E7" "C"@"SEL$1" "A"@"SEL$2")
      USE_NL(@"SEL$55EFA6E7" "A"@"SEL$2")
      NLJ_BATCHING(@"SEL$55EFA6E7" "A"@"SEL$2")
      INDEX_RS_ASC(@"SEL$3" "ACCOUNT_SNAPSHOT_JOURNAL"@"SEL$3" ("ACCOUNT_SNAPSHOT_JOURNAL"."TXN_TIME"))
      BATCH_TABLE_ACCESS_BY_ROWID(@"SEL$3" "ACCOUNT_SNAPSHOT_JOURNAL"@"SEL$3")
      USE_HASH_AGGREGATION(@"SEL$3")
    ]',
    name       => 'patch_c47h6yfcvbw6k'
  );
END;

This forced the optimizer to follow the hint path and restored the expected performance — however, it’s not a long-term enterprise-level solution.


Final Resolution: SQL Plan Baseline

To achieve complete stability, we loaded the good plan from cursor cache into the SQL Plan Baseline repository.

EXEC DBMS_SPM.LOAD_PLANS_FROM_CURSOR_CACHE(
  sql_id => 'c47h6yfcvbw6k',
  plan_hash_value => 1311461192,
  fixed => 'YES',
  enabled => 'YES');

Then we dropped the SQL Profile:

EXEC DBMS_SQLTUNE.DROP_SQL_PROFILE('coe_c47h6yfcvbw6k_1311461192');

From that point on, the optimizer consistently picked the fixed baseline plan, and performance never degraded again.

Summary Table: Options Comparison

MethodEnforces PlanRecommended ForDrawbacks
SQL ProfileNoMinor instability casesMay be ignored
SQL PatchYesImmediate fix for plan injectionNot scalable for all workloads
SQL Plan BaselineYesCritical queries needing controlRequires capture and validation

Final Takeaway

If you are experiencing plan instability:

  • SQL Profiles may temporarily help, but they are not reliable for long-term enforcement.
  • SQL Patches offer tactical control but are harder to manage across environments.
  • SQL Plan Baselines (SPM) are the most dependable method to guarantee consistent execution plan behavior for critical workloads.

Recommendation:
Always prefer SQL Plan Baseline over SQL Profile when plan stability is a must.

Leave a Reply