skip to content

questions

6

Databases keep a cache of already-compiled execution plans. Explain what such a plan cache stores, what entries are keyed on, and why two statements that mean exactly the same thing but differ in whitespace or literal values usually get separate cache entries.

level: middleimportance: must knowfreq 52%

answer

  1. compile once, execute many
  2. key = statement text + session/binding context
  3. near byte-exact match: whitespace and literals matter
  4. bind parameters collapse many keys into one
  5. entry holds plan + dependencies + stats version + counters

basics

~20 s

The cache stores compiled physical plans so repeated statements skip parsing, binding and optimization. Entries are keyed on the statement text (plus session context like schema search order and some settings), and the key is essentially exact-match, so different whitespace or inlined literals produce different keys and separate entries.

solid answer

~60 s

Compiling a statement - parse, bind, rewrite, cost-based optimize - can cost far more CPU than executing a simple lookup. So engines cache the compiled plan and reuse it when the same statement arrives again. **What is stored:** the finished physical plan (operator tree, chosen indexes and join algorithms), plus metadata for invalidation - which catalog objects it depends on, statistics versions, execution counts. **What the key is:** essentially the **statement text**, plus session context that could legitimately change the plan - the schema search order (an unqualified name may resolve differently per user), relevant session settings, sometimes the user. Matching is close to byte-exact and usually case- and whitespace-sensitive, because normalizing text safely is expensive. **Why near-identical statements miss:** `WHERE id = 42` and `WHERE id = 43` are different byte strings, so they are different keys and get separate entries even though the plan shape is identical. Bind parameters fix this: `WHERE id = ?` is one stable text that matches on every execution regardless of value. That is the whole reason prepared statements exist for performance, quite apart from injection safety.

code

sql · 7 lines
sql
-- three distinct cache entries
SELECT * FROM orders WHERE customer_id = 8814;
SELECT * FROM orders WHERE customer_id = 9021;
SELECT  *  FROM orders WHERE customer_id = 8814;  -- extra space: new key

-- one cache entry, reused for every value
SELECT * FROM orders WHERE customer_id = ?;

go deeper

for a junior

Say plans are compiled and cached to avoid re-optimizing, that the key is essentially the statement text, and that bind parameters make the text stable so the plan is reused.

for a middle

Add what the entry contains, why session context is part of the key, and why literal-concatenated SQL creates one entry per value.

for a senior

Talk about hit ratio, compile rate and eviction as observable health signals, and about when reuse is the wrong default for skewed or analytical queries.

for a principal

Frame caching as a shared, memory-bounded resource with cross-tenant effects: eviction pressure, compile CPU as a capacity input, and organizational enforcement of parameterization in client libraries.

## Why a plan cache exists Turning SQL text into a physical plan is expensive. Parsing is cheap, binding is moderate, but cost-based optimization can dominate: the optimizer enumerates access paths and join orders and prices each with statistics, and that search grows quickly with the number of joined relations. For an OLTP statement that reads one row by primary key, compiling can easily cost more CPU than running it - sometimes by an order of magnitude. The fix is obvious: compile once, run many times. The **plan cache** (also called the statement cache, procedure cache, or shared SQL area) holds compiled plans in shared memory so subsequent executions of the same statement skip straight to execution. ## What an entry holds A cache entry typically carries: - the **physical plan** - the operator tree with chosen access paths, join algorithms, join order and memory grants; - **dependency metadata** - the catalog objects referenced, so DDL on any of them can invalidate the entry; - **statistics versions** used at compile time, so a statistics refresh can trigger recompilation; - **usage counters** - execution count, average duration, memory - used both for eviction policy and for diagnostics; - for parameterized statements, the parameter shapes and possibly the values used at first compile. The cache is a memory-bounded shared structure with an eviction policy - typically some cost-aware LRU that considers how expensive an entry was to build and how often it is used. ## The cache key: text plus context The crucial and often-misunderstood part is the key. Engines key on the **statement text**, matched essentially exactly. Practical implications: - extra whitespace, a line break, or different capitalization of keywords can produce a different key; - different literal values produce different text and therefore different keys; - comments embedded in the statement count as text. Text alone is not sufficient, though, because the same text can legitimately mean different things in different sessions. So the key also includes context that could change binding or plan choice: - **schema search order / default schema** - `SELECT * FROM orders` may bind to different tables for different users; - **the executing user**, where privileges or row-level policies affect the plan; - **session settings** that influence the optimizer or semantics. Engines do not normalize text aggressively because doing it safely is itself work, and because two texts that differ only in a literal do not always deserve the same plan (see parameter sniffing). ## Why parameters are the real fix A statement written as `SELECT * FROM orders WHERE customer_id = 8814` produces a distinct cache key for every customer ID an application ever uses. The same statement written with a bind parameter - `... WHERE customer_id = ?` - is a single, stable text, so: - one entry serves every execution; - compilation happens once, not per distinct value; - the cache stays small and the hit rate stays high. With a **prepared statement**, the client explicitly asks the server to parse and bind once and returns a handle; subsequent executions send only the handle plus values. That makes plan reuse deliberate rather than incidental. Note it also *forces* the question of which value's plan you got - the topic of parameter sniffing. ## Server-side auto-parameterization Because unparameterized applications are so common, several engines will, for simple statements, automatically replace literals with parameters to create a shared entry. This helps but is deliberately conservative: engines apply it only where they judge that the plan is unlikely to depend on the literal, since replacing a literal that *does* matter would trade a compile cost for a plan-quality problem. ## What a healthy cache looks like Operationally you want: - **high hit ratio** for the repetitive OLTP workload; - **a bounded number of distinct entries** relative to the number of distinct statement shapes in the application - if you have thousands of entries that differ only in literals, the application is not parameterizing; - **low compile rate** under steady load - a high compile rate means either churn from literals, or repeated invalidation. ## When you do *not* want reuse Plan caching assumes one plan is good for all executions of that text. That assumption breaks when a predicate's selectivity varies wildly with the parameter value, or for large one-off analytical queries where the compile cost is negligible next to runtime and a value-specific plan is worth far more. In those cases the right answer is to make the engine re-plan rather than to reuse - which is exactly the tradeoff the generic-versus-custom-plan decision formalizes. ## Summary for an interview Cache the expensive artefact (the plan), key it on the thing that determines it (text plus binding context), and control the key deliberately with bind parameters. Everything else about plan caching - sniffing, pollution, recompilation - follows from those three sentences.

  • Why do engines not simply normalize whitespace and case before hashing the statement text?
    Normalization costs CPU on every execution and must be provably safe, since string literals and quoted identifiers are case- and whitespace-significant, so a naive normalizer would change meaning. Some engines do a limited version of this through auto-parameterization of simple statements, but they keep it conservative because collapsing two texts that deserve different plans trades a small compile saving for a potentially large plan-quality loss.
  • What does a very large number of cache entries that differ only in literal values tell you about an application?
    That it builds SQL by concatenating values into the statement text instead of using bind parameters. Each distinct value creates a new key, so the cache fills with near-duplicates, compile CPU climbs, useful entries get evicted, and the effective hit rate collapses. It is also the same code pattern that produces SQL-injection exposure, so it is worth fixing on both grounds.

saying these in an interview costs you the question

  • Saying the cache stores query results rather than compiled plans.
  • Believing the engine normalizes SQL text so whitespace and literals do not matter.
  • Thinking the cache key is only the text, ignoring schema search order and session context.
  • Assuming a cached plan is valid forever regardless of schema or statistics changes.
  • Claiming prepared statements exist only for SQL-injection protection.

context

open as a page

A parameterized query normally returns in milliseconds but intermittently takes minutes for days at a time, with no schema change and no data-volume change, and it recovers as soon as the statement is recompiled. Explain the plan-reuse behaviour that causes this and how you would confirm it.

level: seniorimportance: must knowfreq 50%

basics

~20 s

The engine compiled the plan using the first parameter values it saw and cached it. If those values were unrepresentative - very selective or very unselective compared with typical ones - every later execution reuses a plan tuned for the wrong case. Recompiling with different values silently swaps which case suffers.

open as a page

An application builds SQL by concatenating literal values directly into the statement text instead of sending bind parameters. Beyond the SQL-injection risk, describe what this does to the database's execution plan cache and to server CPU under load.

level: middleimportance: should knowfreq 44%

basics

~20 s

Every distinct literal produces a distinct cache key, so the plan cache fills with thousands of near-identical single-use entries. Useful plans get evicted, the hit rate collapses, and the server burns CPU re-optimizing the same query shape over and over, adding latency to every call.

open as a page

When a prepared statement is executed repeatedly with different parameter values, a database can either keep one parameter-independent plan or optimize afresh for each execution's values. Compare the two strategies, and describe how an engine can decide between them automatically.

level: seniorimportance: should knowfreq 36%

basics

~20 s

A generic plan is compiled once using average selectivity and reused for all values: cheap, stable, but never value-specific. A custom plan is optimized per execution using the actual values: accurate, but pays optimization cost every time. Engines can compare observed custom-plan costs against the generic plan's cost and switch when generic is not worse.

open as a page

A database can discard or rebuild a cached execution plan rather than reuse it. What events cause that, and why can a plan that was never invalidated still be a bad plan today?

level: seniorimportance: should knowfreq 34%

basics

~20 s

Plans are invalidated by DDL on referenced objects, index or constraint changes, statistics refreshes, permission or session-setting changes, explicit cache flushes, restarts, and memory-pressure eviction. But data drifting without a statistics refresh changes nothing the engine tracks, so a valid cached plan can quietly become wrong.

open as a page

You own an OLTP service whose hottest query filters on a tenant column where one tenant holds roughly 80% of the rows. The statement is parameterized, plans are shared across tenants, and latency is bimodal. How would you decide between forcing re-planning per execution, splitting the statement, or reshaping the data - and what would you measure to justify the choice?

level: principalimportance: should knowfreq 28%

basics

~20 s

Quantify first: call rate per class, cost of the right versus wrong plan, and optimization cost per compile. High-rate short queries cannot afford per-execution planning, so split the statement so each class gets its own stable plan. If the heavy tenant dominates capacity, reshape - partition or isolate it - so no plan choice is required.

open as a page