skip to content

A nightly reporting statement joins around 25 relations. It spends longer being planned than executing, and its chosen plan differs between runs. How would you approach this?

level: principalimportance: nice to knowfreq 24%

answer

  1. Both symptoms = past the exhaustive-search threshold
  2. Count flattened relations, not names in the text
  3. Decompose → exponentially smaller problems
  4. Plan reuse / pinning = pay planning once
  5. Fix estimates before buying more search

basics

~20 s

Both symptoms say the query is past the optimizer's exhaustive-search threshold and is being planned by a randomized heuristic. Reduce the effective relation count by decomposing the statement or materializing stable sub-results, make planning happen once via plan reuse or pinning, and fix the statistics that guide whichever search runs.

solid answer

~60 s

Two symptoms, one cause: 25 relations is far past any optimizer's exhaustive-search threshold, so the join order is coming from a bounded heuristic — and if that heuristic is randomized, plan variation between runs is expected behaviour, not a bug. My order of attack: 1. **Measure the split** — planning time versus execution time, and whether the slow runs correlate with a particular plan shape. 2. **Reduce the search problem.** Break the statement into stages that materialize stable intermediate results, so the optimizer faces two 12-relation problems instead of one 25-relation one. Watch for views and subqueries flattening into one giant optimization region. 3. **Make planning happen once** — prepared/cached plans, or an engine-level pinned plan for this statement — so planning cost is amortized and the plan stops moving. 4. **Fix estimates.** Past the threshold, search quality depends entirely on cardinality inputs; refresh statistics and address obvious correlation errors before adding search budget. 5. Only then consider raising the join-count threshold, and measure the planning time it buys. Longer term, ask whether a 25-way join belongs in this engine at all or wants a different data layout owned by the modelling side.

go deeper

for a junior

Recognize that a very wide join is unusual and that both planning cost and plan choice are involved; escalate rather than guess.

for a middle

Explain that the query is past the exhaustive-search threshold, and propose splitting the statement and reusing plans.

for a senior

Sequence the work: measure the planning/execution split, shrink the optimization region, amortize planning, then correct estimates before touching search knobs.

for a principal

Own the tradeoff explicitly — predictability versus adaptability, planning budget versus plan quality — and be willing to conclude the workload needs a different physical layout rather than more optimizer tuning.

## Reading the symptoms Two observations arrive together and they are diagnostic. **Planning longer than execution.** Exhaustive join-order search is exponential (`3^n` combinations, `2^n` memo entries). At 25 relations that is astronomically out of reach, so the engine has already given up on exhaustive search — but even the heuristic search over a graph that wide, together with access-path and operator enumeration per node, is expensive. If planning dominates, the query is either being replanned on every execution (no plan reuse) or the search budget is set far too generously. **Unstable plans between runs.** Deterministic search cannot do this unless its inputs changed. So either the statistics changed underneath (auto-refresh fired between runs), or the optimizer is using a randomized/genetic search whose outcome depends on a seed. Both are worth distinguishing before acting, because the fixes differ. ## Step 1 — measure before touching anything Separate planning time from execution time for a cold plan and a warm one. Record the plan shape per run and correlate with runtime. You want to know: is the variance costing real time, or are the different plans all roughly equal? A query whose plan wobbles between three plans that all run in 40 seconds is a cosmetic problem; one that wobbles between 40 seconds and 40 minutes is an availability problem. ## Step 2 — shrink the optimization region This is the highest-leverage move, because it changes the exponent. - **Decompose the statement.** Compute a stable, well-understood intermediate — the reduced fact set, or the resolved dimension keys — into a temporary or intermediate table, then join against it. Two 12-relation problems are not half of a 25-relation problem; they are exponentially smaller. - **Find hidden relations.** Layered views, inlined subqueries and table functions flatten into the same optimization region. A statement that mentions six names can present twenty-five base relations to the optimizer. Look at the flattened relation count, not the text. - **Remove joins that contribute nothing.** Wide reporting statements accumulate joins whose only purpose is a column that is no longer selected, or an existence check better expressed as a semi-join. Every relation removed cuts the space super-linearly. ## Step 3 — pay for planning once A nightly report should not be replanned per execution. Options, roughly in order of preference: - Prepared statements / server-side plan cache, so repeated executions reuse a plan. - An engine-level facility for storing and reusing a validated plan for a specific statement, which also freezes the shape and removes the run-to-run variance. - As a last resort, optimizer directives that constrain the join order. These are debt: they must be revisited when data volumes shift, and they silently prevent the optimizer from adapting. Freezing a plan is a real decision, not a hack — for a nightly batch job, predictable 12 minutes usually beats an average of 9 minutes with an occasional 90. ## Step 4 — fix the inputs before buying more search Once the search is heuristic, its quality is dominated by the cardinality estimates it is scoring candidates with. Estimation error compounds multiplicatively up a deep join tree: a 3x underestimate at each of five levels is a 243x error at the top, which is more than enough to make the search prefer a catastrophic order. Refresh statistics, look for the obviously wrong estimates by comparing estimated to actual row counts at each level, and address correlated predicates. This is nearly always worth more than extra search budget. ## Step 5 — only now, tune the search itself Raising the exhaustive-search threshold can be right for a long-running batch statement: two extra seconds of planning against twelve minutes of execution is a fine trade, and exact search may find a materially better order. Measure it — raise the threshold, record planning time and total runtime, and keep the change only if it pays. For randomized search, fixing the seed restores determinism at the cost of possibly freezing in a mediocre plan. ## Step 6 — question the shape of the work A 25-way join in a transactional schema is often a sign the workload is analytic and is being served by a layout designed for transactional writes. The right long-term answer may be a different physical layout or a purpose-built reporting store — a modelling decision rather than an optimizer decision. Raise it explicitly rather than tuning forever. ## The framing that lands in an interview Say plainly: *the optimizer's job is a search under a time budget, and at 25 relations the budget has bound. My levers are to make the problem smaller, to pay for planning once, and to improve the estimates the search is steering by — in that order. Tuning the search itself is last, and freezing the plan is a legitimate answer for a batch job.*

  • How would you decide whether pinning the plan is acceptable?
    Weigh predictability against adaptability. For a nightly batch with stable data volumes, a frozen plan that always runs in twelve minutes beats an average of nine with occasional catastrophic runs. But a pinned plan is debt: it must be re-validated when data volume or distribution shifts, so it needs an owner and a review trigger, not just a one-time fix.
  • Why does splitting one 25-relation statement into two 12-relation stages help so much?
    Because join-order search cost is exponential in the number of relations, not linear. Two smaller problems are each vastly cheaper than one large one, and each may fall back under the exhaustive-search threshold, restoring the optimality guarantee for both halves. The cost is materializing an intermediate result and losing cross-stage optimization.
  • When is raising the join-count threshold the right call?
    When execution dominates and planning is amortized — a long-running analytic or batch statement where a second or two of extra planning is repaid many times over by a better order. It is the wrong call for OLTP, where planning happens inside the user-facing latency budget on every plan-cache miss, and it never removes the exponential blow-up, only postpones it.

saying these in an interview costs you the question

  • Treating unstable plans as a caching bug rather than a symptom of heuristic/randomized search
  • Jumping straight to optimizer directives before measuring or reducing the join count
  • Assuming more search budget fixes plans that are bad because of wrong cardinality estimates
  • Counting the names in the FROM clause instead of the flattened base-relation count
  • Insisting a pinned plan is always bad practice, ignoring that batch jobs value predictability over average speed

context