skip to content

When a stored routine's body is executed repeatedly with different parameter values, how does the database handle execution plans for the statements inside it, and what problem can that cause?

level: seniorimportance: should knowfreq 38%

answer

  1. plan compiled on first values, reused for all
  2. sniffing (SQL Server) / bind peeking (Oracle) / generic vs custom (PG)
  3. skewed predicate = one plan, two workloads
  4. slow after restart with no deploy
  5. fix: recompile, average plan, split routine, restructure

basics

~20 s

Engines cache plans for statements inside a routine and reuse them across calls, planning them against the first parameter values seen. If the data is skewed, a plan optimal for one value is terrible for another — parameter sniffing. Remedies: force a per-call replan, use a generic plan, split the routine, or restructure the query.

solid answer

~60 s

Parsing and planning cost real CPU, so engines cache plans for the statements inside routines keyed to the routine (plus session, for temporary-table dependencies). The plan is built using the **first** parameter values the engine sees — SQL Server calls this parameter sniffing, Oracle calls it bind peeking, PostgreSQL builds custom plans for the first few executions of a prepared statement and then may switch to a **generic** plan built with average-case selectivity. The failure is skew. `WHERE customer_id = ?` with one customer holding 40% of the rows and the rest holding a handful: the plan compiled for a rare customer uses an index nested loop and is disastrous for the whale, and vice versa. The symptom is a routine that is fast for months, then suddenly slow for everyone after a restart or a statistics update caused it to recompile on an unrepresentative value. Remedies: recompile per execution for the skewed statement; pin an average-case plan; split the branchy routine into separate routines with separate cache entries; or make the query less skew-sensitive.

go deeper

for a junior

Know that the database reuses execution plans for statements inside routines rather than re-optimizing every call.

for a middle

Explain that the plan is built from the first parameter values seen and that skewed data makes that plan wrong for other values.

for a senior

Diagnose from estimate-versus-actual and the compiled-for parameter values, and choose deliberately between per-statement recompile, an average plan, splitting the routine, and restructuring.

for a principal

Frame it as predictability versus peak performance under an SLO, and set a policy: parameterize by default, identify skew-sensitive statements explicitly, and prefer schema or partitioning changes that remove the sensitivity over hints that mask it.

## Why plans are cached inside routines Parsing, rewriting, and optimizing a nontrivial statement can cost more than executing it. A routine that runs thousands of times a minute cannot afford to replan every call, so every engine caches the plans for statements inside routine bodies and reuses them. The cache key is the routine and its statement, not the argument values — that is the whole point, and also the whole problem. A plan is chosen from *estimates*, and estimates depend on the *values* in the predicates. Cache the plan, and you have frozen the value assumption. ## How each engine frames it - **SQL Server — parameter sniffing.** On first compile, the optimizer "sniffs" the actual parameter values and builds a plan tailored to them. That plan stays cached for later calls with any values. - **Oracle — bind peeking**, with *adaptive cursor sharing* layered on: the engine notices when a bind-sensitive cursor performs badly for other values and starts maintaining several plans keyed by selectivity ranges. - **PostgreSQL — custom vs generic plans.** A prepared statement (and a plpgsql statement, which is prepared behind the scenes) is planned per-value for the first executions; if the generic plan's estimated cost is not worse than the average custom plan, it switches to a generic plan built without value knowledge. `plan_cache_mode` forces either behaviour. Different names, one phenomenon: reuse trades per-call optimality for compilation savings. ## The skew failure, concretely One table, one predicate: `WHERE tenant_id = ?`. Ninety-nine tenants have a few hundred rows each; one has ten million. Two sensible plans exist — an index seek with nested loops for the small tenants, a parallel scan with a hash join for the big one. Only one gets cached. If it compiles on a small tenant, the big tenant's call performs ten million index lookups with random I/O. If it compiles on the big tenant, every small call scans and builds a hash table it does not need, and grabs a large memory grant that starves concurrency. What makes it maddening operationally is the trigger: nothing in the code changes. A server restart, a plan-cache eviction, a failover, or an automatic statistics update invalidates the plan, the next call happens to arrive from the wrong end of the distribution, and the system is suddenly slow for everyone. Bisecting the deploy history finds nothing. ## Recognising it The fingerprint is *the same statement, wildly different runtimes correlated with parameter values, and a plan whose estimated rows differ hugely from actual rows for the current call*. If you can capture the cached plan and compare the compiled-for parameter values with the current ones, the diagnosis is immediate. "It got slow after a restart with no deploy" is the operational tell. ## Remedies **Recompile per execution.** Recompiling only the one problematic statement per call (rather than the whole routine) pays optimization cost each time and gets an optimal plan each time. Correct when the statement is expensive relative to planning and the skew is severe. **Force a generic / average plan.** Optimize for an average or explicitly chosen representative value. This deliberately sacrifices the best case to eliminate the disastrous case — usually the right call for latency-SLO workloads where predictability beats peak speed. **Split the routine.** If the body branches on an input flag and each branch wants a different shape, separate routines get separate cache entries. Similarly, routing whale tenants to a different routine than long-tail tenants gives each a plan that fits. **Restructure so skew stops mattering.** A covering index that makes both cases a seek, a partitioning scheme aligned with the skewed column, or splitting one query into two simpler ones can remove the dependence entirely. This is the most durable fix because it does not rely on planner hints. **Local variable indirection** (assign the parameter to a local variable before use) defeats sniffing in some engines by making the value invisible at compile time. It works, but it is obscure and it silently opts you into average-case estimation — document it or prefer an explicit hint. ## Two adjacent effects **Temporary tables inside routines** interact with caching: because a temp table is per-session and its statistics change per execution, engines may recompile statements that reference it, which both costs compilation and, sometimes, saves you from a stale plan. **Cache pollution** is the mirror image. Dynamic SQL built by string concatenation with literal values produces a distinct cache entry per value, filling the cache with single-use plans and evicting useful ones. Parameterize to get one entry — and then live with the sniffing tradeoff above. The two problems pull in opposite directions, and choosing between them is exactly the judgment this question is testing.

  • A routine has been fast for months, then becomes slow overnight with no code deploy. How does that point at plan caching?
    A cached plan survives only until it is invalidated — by a restart, a failover, cache eviction, or an automatic statistics refresh. When it recompiles, it does so against whatever parameter values happen to arrive first, and if those are unrepresentative the new plan can be far worse for the rest of the traffic. The absence of a deploy is precisely what makes plan choice, rather than code or data volume, the leading hypothesis.
  • Why can parameterizing queries to avoid plan-cache pollution make parameter sniffing worse?
    Literal-embedded statements compile a separate plan per distinct value, so each value effectively gets its own tailored plan at the cost of flooding the cache with single-use entries. Parameterizing collapses them into one cache entry, which is what you want for compilation cost and cache health, but that single plan must now serve every value including skewed ones. The two goals genuinely conflict, so the resolution is to parameterize by default and handle the few skew-sensitive statements explicitly.

It is like sizing a delivery van from the first order of the day: fine if orders are uniform, ruinous the day the first one is a single envelope and the rest are pallets.

saying these in an interview costs you the question

  • Believing every execution is planned from scratch, so caching cannot cause variance.
  • Attributing a sudden slowdown to data growth when the plan changed and the data did not.
  • Applying a per-call recompile to the entire routine when only one statement is skew-sensitive.
  • Thinking clearing the plan cache is a fix rather than a coin flip on the next compile.
  • Assuming the generic or average plan is always the safe choice — it is a deliberate trade of best case for worst case.

context