skip to content

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%

answer

  1. price re-planning as compile_cpu x call_rate
  2. build the 2x2 plan cost matrix before deciding
  3. split = one stable plan per class, one compile each
  4. partition/isolate the whale = structural fix, fixes noisy neighbour too
  5. monitor p99 per tenant class, not global p99

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.

solid answer

~1 min

I would treat it as a capacity decision, not a tuning trick. **Measure:** executions per second split by tenant class; p50/p99 latency per class; the runtime of each class under both candidate plans (forced literal test); optimization CPU per compile; and current plan-cache compile rate and contention. **Then apply the arithmetic.** Per-execution re-planning costs `compile_cpu x call_rate`. At thousands of calls per second on a millisecond query, that alone can consume a core or more and add plan-cache latch contention - usually disqualifying. It is defensible only for low-rate, expensive statements. **Splitting the statement** is normally the best value: route the known-heavy tenant to a separate statement text so each class gets its own cache entry, compiled once, stable. One compile per class, no sniffing lottery, and the intent is explicit in code. Cost: application change and a routing rule that must be maintained as tenant sizes drift. **Reshaping the data** - partitioning by tenant, or moving the whale to its own shard - removes the dilemma structurally and fixes the noisy-neighbour capacity problem too. Highest cost, best long-term answer if that tenant keeps growing. I would ship the split for immediate relief and put reshaping on the roadmap, with monitoring on per-class p99 and plan changes.

go deeper

for a junior

Recognise that one shared plan cannot suit both a huge tenant and tiny ones, and that separating the cases is the direction of travel.

for a middle

Compare the concrete options and note that re-planning every execution costs CPU proportional to call rate.

for a senior

Build and use the plan cost matrix, ship the statement split with per-class metrics, and keep a pinned plan only as a time-boxed mitigation.

for a principal

Sequence mitigation, fix and structural change across timescales; price optimization CPU as capacity; set per-class SLOs and guardrails; and weigh tenant isolation against migration cost and distribution drift.

## Framing the decision The symptom - bimodal latency on a shared plan over a skewed predicate - is a parameter-sniffing outcome, but at this scale the interesting question is not "what is the mechanism" but "what is the right system design". Three families of answer exist, with very different costs and half-lives. ## Step 1: quantify before choosing Nothing here can be decided from principles alone. The numbers that determine the answer: - **Traffic mix.** Executions per second for the whale tenant versus the long tail. A 100:1 call-rate ratio points to a different answer than a 1:1 ratio, even with the same row skew. - **Plan cost matrix.** Run each class under each candidate plan (by testing with literal values, or by forcing each plan) and record actual runtimes. You need four numbers: small-tenant-under-selective-plan, small-tenant-under-scan-plan, whale-under-selective-plan, whale-under-scan-plan. The asymmetry in that matrix tells you how much a wrong plan actually costs. - **Optimization cost.** Measure CPU per compile for this statement. Multiply by the call rate to price the re-plan-always option honestly. - **Current shared-resource health.** Plan-cache compile rate, cache contention waits, CPU headroom. If the server is already near the compile-contention knee, per-execution planning is off the table regardless of its accuracy benefit. - **Distribution trajectory.** Is the whale growing? Are there several emerging whales? A rule that names one tenant rots quickly if the answer is yes. ## Step 2: evaluate the options against those numbers **Re-plan on every execution.** Always the right plan; cost is `compile_cpu x call_rate` plus contention. For a 200 microsecond OLTP lookup at 5,000 calls per second, a 1 ms compile is a 5x cost increase and a throughput ceiling - disqualifying. For a query executed a few times a minute, it is free and correct, and I would take it immediately. The honest rule: per-execution planning scales with call rate, and OLTP call rates are exactly where it does not fit. **Force a single generic plan.** Removes the lottery and makes latency predictable, which has real operational value: predictable is often more valuable than fast. But it is only acceptable if the plan cost matrix shows the middle-ground plan is *tolerable* for both classes. With 80/20 skew it usually is not - the average describes no real tenant. **Split the statement.** Give the heavy class its own statement text so the cache holds two entries, each compiled once and each stable. Benefits: one compile per class, no dependence on which value compiled first, explicit intent in the code, and independent monitoring per class. Costs: an application change; a routing predicate that must be maintained as the tenant distribution shifts; and mild duplication in the data-access layer. Mitigate the rot by deriving the routing from a maintained size threshold rather than a hardcoded tenant identifier. **Reshape the data.** Partition by tenant so the whale's rows live separately and per-partition statistics describe each class honestly; or move the whale to a dedicated shard or database. This eliminates the plan-choice problem as a side effect and simultaneously addresses the noisy-neighbour issues that a whale tenant always brings - buffer-cache dominance, lock contention, backup and maintenance windows. Costs are the largest: migration, routing at the service layer, operational complexity, and possibly cross-tenant query paths that no longer work. **Improve the estimates.** Cheapest to try and worth checking first: ensure a histogram exists on the tenant column with enough resolution to represent the whale as a most-common value. Sometimes the optimizer is choosing badly only because it cannot see the skew, and better statistics plus a value-specific planning mode resolves it without structural change. This rarely suffices at 80% skew but costs an hour to rule out. **Pin a plan.** Fast, brittle, and it freezes a decision against today's data while hiding future regressions. Acceptable as an incident mitigation with an explicit expiry, never as the design. ## Step 3: sequence the response A credible principal answer separates timescales: - **Now (hours):** confirm the diagnosis, check statistics/histogram quality, and mitigate - force the plan that protects the majority of traffic, or force re-planning if the call rate can absorb it. - **Next (days):** ship the statement split, with per-class latency metrics and a documented routing rule. - **Later (quarters):** decide whether the whale justifies partitioning or physical isolation, driven by its growth curve and by whether other whales are emerging. ## Step 4: define success and guard it Declare the target explicitly - for example, p99 per tenant class rather than a global p99, because a global percentile hides exactly this failure mode. Then add guardrails: alert on plan changes for the hot statements, on compile-rate spikes, and on per-class p99 divergence. The failure you are engineering against is not "slow query"; it is "a plan silently flipped and one class of users fell off a cliff", and only per-class observability catches that early. ## The judgement being tested That you price optimization CPU against call rate rather than assuming re-planning is free; that you recognise splitting as converting a runtime gamble into a design decision; that you know reshaping fixes more than plan choice; and that you attach a measurement and a rollback story to whatever you pick.

  • Why is a global p99 latency target the wrong SLO for this workload?
    Because the small tenants may be a minority of executions, so their catastrophic latency can sit entirely inside the tail that the global p99 rounds away - or conversely the whale's slow calls can dominate the tail and hide that everyone else is fine. Per-class percentiles make the bimodality visible and let you alert on the class that is actually suffering. It also makes the effect of a plan change measurable within minutes rather than debatable.
  • What is the risk of routing based on a hardcoded whale tenant identifier?
    Tenant sizes drift: today's whale may shrink and a new tenant may grow into the same problem, at which point the routing rule silently sends the wrong class down the wrong path and the incident reappears under a different name. Deriving the route from a maintained size threshold or a periodically refreshed classification keeps the rule correct as the distribution changes. Whatever the mechanism, it needs an owner and a review cadence, since it is application logic encoding a data property.

saying these in an interview costs you the question

  • Jumping straight to a plan hint or pinned plan without measuring the cost matrix.
  • Assuming per-execution re-planning is free, ignoring compile CPU multiplied by call rate.
  • Proposing a single generic plan without checking it is tolerable for both classes.
  • Treating it purely as a query-tuning problem and ignoring the noisy-neighbour capacity dimension.
  • Hardcoding one tenant identifier into routing with no plan for distribution drift.

context