The result told the whole story in one glance: for weeks, plan_hash_value = 1448138561 ran in 6 seconds on average. Overnight, a new plan_hash_value = cpnhz9tgqy81s appeared and was now averaging 380 seconds — a 60x regression, same SQL_ID, same text, different plan.
What bind values were peeked for the good vs. the bad child?
Where does estimated vs. actual rows diverge?
Look at E-Rows vs A-Rows per plan line. A 100x+ divergence at the join/filter step confirms the optimizer is estimating on a bind value that doesn't represent the actual skew of the run.
Why "just gather stats" or "flush the cursor" isn't the fix
Both were tried first, as any team would:
Flushing the cursor (DBMS_SHARED_POOL.PURGE) only resets the problem — the next hard parse might peek a different bad value tomorrow.
Re-gathering stats / histograms on XLA_AE_LINES helped a little but didn't stop the flip, because the underlying issue is architectural: this SQL, inside a seeded Oracle Applications package (XLA subledger accounting engine, not custom code), cannot be edited, and bind peeking will keep rolling the dice on every fresh hard parse driven by concurrent manager sessions, cursor invalidations, or stats jobs.
What the team actually needed was to force the optimizer to stop peeking on this one cursor and instead use bind-variable-independent (generic) cardinality estimates, consistently, every time — without changing a single byte of Oracle-seeded PL/SQL. That's exactly the job of _optim_peek_user_binds combined with SQL Patch.
The Fix: SQL Patch to the Rescue:
Why SQL Patch, and not a hint in code, a Baseline, or a Profile?
DBMS_SQLDIAG.CREATE_SQL_PATCH lets a DBA attach a hint (or an optimizer parameter override, via OPT_PARAM) to a specific SQL_ID without any code change — ideal for a locked-down, Oracle-owned application like EBS's XLA engine, where you are not allowed to modify seeded PL/SQL.
Choosing the hint: OPT_PARAM('_optim_peek_user_binds' 'false')
This is the key decision the senior DBA made, and it's worth explaining plainly to a junior DBA:
_optim_peek_user_binds = TRUE (the default) tells the optimizer: "On hard parse, look at the actual bind values sent, and optimize the plan for those specific values."
Setting it to FALSE for this one cursor only tells the optimizer: "Ignore the specific values you were just handed. Use bind-variable-independent, generic (unpeeked) selectivity estimates instead — the same, stable plan every single time, regardless of which LEDGER_ID or EVENT_TYPE_CODE happens to hard-parse first."
This trades "theoretically optimal for one specific value" for "consistently good, no more surprises" — exactly the trade-off you want for a shared, highly-executed, skewed-data cursor inside a batch program like Create Accounting, where plan stability matters far more than shaving the last millisecond off one lucky value.
This is functionally the same idea Nigel Bayliss's post demonstrates with IGNORE_OPTIM_EMBEDDED_HINTS (removing embedded hint influence) — here we're removing bind-peeking influence instead, via OPT_PARAM, which CREATE_SQL_PATCH accepts exactly like any other hint text.
Applying the patch:
SQL> DECLARE
2 v_patch_name VARCHAR2(30);
3 BEGIN
4 v_patch_name := DBMS_SQLDIAG.CREATE_SQL_PATCH(
5 sql_id => 'cpnhz9tgqy81s',
6 hint_text => 'OPT_PARAM(''_optim_peek_user_binds'' ''false'')',
7 name => 'XLA_FORCE_GENERIC_PLAN'
8 );
9 END;
10 /
PL/SQL procedure successfully completed.
sql_id must be the exact SQL_ID of the offending statement, not the concurrent program's outer SQL_ID or a parent call.
hint_text uses doubled single quotes (''_optim_peek_user_binds'') because we're inside a PL/SQL string literal — this is a very common syntax trip-up.
name is your own label. Make it descriptive and traceable (XLA_FORCE_GENERIC_PLAN tells the next DBA exactly why it exists) — you will thank yourself in six months during an audit or an upgrade.
No downtime, no code deployment, no adpatch/adop cycle — this is a live, additive change at the database layer that Oracle Support will recognize and accept as a supported troubleshooting technique for EBS environments (this is a standard, documented approach for XLA/GL performance issues raised via SR).
Post-Validation: Trust, But Verify:
A SQL Patch that silently fails to attach, or attaches but doesn't get used, is worse than doing nothing — you'll believe the problem is fixed when it isn't. Walk through every one of these checks.
SELECT *
FROM dba_sql_patches where status='ENABLED';
If STATUS is not ENABLED, the patch was created but is dormant — nothing will change. (DBMS_SQLDIAG.ALTER_SQL_PATCH can toggle this if it was disabled.)
Force a fresh hard parse so the new cursor picks up the patch:
SELECT address, hash_value
FROM v$sqlarea
WHERE sql_id = 'cpnhz9tgqy81s';
Confirm the patch is actually being used in the plan:
SELECT * FROM TABLE(
DBMS_XPLAN.DISPLAY_CURSOR('cpnhz9tgqy81s', NULL, 'OUTLINE BASIC NOTE')
);
Look for this at the bottom of the output — it's the definitive proof:
Confirm plan stability across multiple, differently-skewed executions
sql-- Run Create Accounting (or the isolated SQL) for a few different LEDGER_IDs / batches
-- and confirm the SAME plan_hash_value every time
SELECT sql_id, plan_hash_value, executions, elapsed_time/1e6/executions avg_sec,
buffer_gets/executions avg_buffer_gets
FROM v$sql
WHERE sql_id = 'cpnhz9tgqy81s'
ORDER BY last_active_time DESC;
You want to see one plan_hash_value now, consistently, regardless of which ledger or event type ran first.
Confirm plan stability across multiple, differently-skewed executions
This is the step junior DBAs most often skip, and it's the one that actually proves the fix works:
sql-- Run Create Accounting (or the isolated SQL) for a few different LEDGER_IDs / batches
-- and confirm the SAME plan_hash_value every time
SELECT
fcr.request_id,
fcp.concurrent_program_name AS program_short_name,
fcr.actual_start_date,
fcr.actual_completion_date,
ROUND((fcr.actual_completion_date - fcr.actual_start_date) * 24 * 60, 1) AS run_minutes
FROM
apps.fnd_concurrent_requests fcr
JOIN
apps.fnd_concurrent_programs fcp
ON fcr.concurrent_program_id = fcp.concurrent_program_id
AND fcr.program_application_id = fcp.application_id
WHERE
fcp.concurrent_program_name = 'XLAACCPB'
AND fcr.actual_start_date > SYSDATE - 3
ORDER BY
fcr.actual_start_date DESC;
No comments:
Post a Comment