Skip to content
About
Main content

Scenarios

Longer problems, like those in senior system design interviews. A strong answer estimates with numbers first, names the mechanism, then recommends a fix and says what it costs.

Try each one before you open the answer.
1A dashboard query regressed from 3 seconds to 25 minutes after the nightly load. Nobody changed code, indexes or hardware. Walk through your diagnosis.Show the answer

Find the bad estimate

Run EXPLAIN ANALYZE and find the lowest node where estimated and actual rows differ by more than 10×. Suppose it is the scan of the newest partition: estimated 1 row, actually 9 million.

Explain

The new partition has no , so the used its fixed placeholder constants and chose a nested loop with index lookups: right for 1 row, fatal for 9 million.

Fix

Run ANALYZE on the partition, and make it the last step of the load job, paying the fixed cost at the cheapest moment. Longer term, write statistics at load time, or use a format that stores per-file statistics.

Do not start with

Cost settings such as random_page_cost: the mistake was about row counts, not cost ratios.

Why waiting is worse

, about 10 days on a billion-row table growing 1% a day.

Read moreDiagnosing a bad planWhen there are no statistics at allAuto-analyze

2A BI tool generates a query that joins 6 dimension tables but selects columns from only 2. How do you speed it up without touching the tool?Show the answer

Join elimination

Check whether the planner can drop the unused joins: the must be enforced and the referenced columns unique. Validate the constraints on the data, then enforce them, or mark them as trusted (RELY) only if the load pipeline guarantees them. Removing four joins beats any join-order tuning.

Outer joins

Check that left joins followed by a filter that rejects become inner joins, so the remaining join order can be searched freely.

Predicate form

Long OR chains may defeat because normalization to conjunctive form is capped; IN lists work.

If the tool cannot change

Put a thin view in front of it that exposes only the needed joins.

Trade-offs

Enforced constraints cost load time; trusting unenforced ones trades correctness for speed.

Read moreJoin eliminationQuery normalization

3A query using NOT IN against a 200-million-row table takes 40 minutes; the same query written with NOT EXISTS takes 2 seconds. Explain it to a manager, and fix it.Show the answer

Explain

The two are not equivalent when the subquery's column can be NULL: NOT IN with any NULL in the set returns no rows. So the planner may not turn NOT IN into an unless it can prove there are no NULLs, and it runs the subquery as a nested loop, costing about outer rows × subquery cost. NOT EXISTS has no NULL trap and becomes an anti-join.

Fix

If the column should never be NULL, add a NOT NULL constraint, which makes the rewrite legal; or write NOT EXISTS. If NULLs are valid and the intent is "not in the set, ignoring NULLs", NOT EXISTS is also the right meaning.

Verify

Check with EXPLAIN that the plan shows an anti-join, and compare the row counts of both forms before switching.

Read moreSubquery decorrelationIt is not unconditional

4A query joining three views was fast until a fourth layer of views was added. Now it is slow, and its run time varies between runs. Diagnose it.Show the answer

Count tables after expansion

Three views over five tables each is 15 tables. Exhaustive join-order search is practical up to about 12 and costs O(3ⁿ), so beyond that the planner switches to greedy or genetic search. PostgreSQL's genetic is not deterministic: it can return different plans for the same query, which explains run times of 20 seconds one day and 3 minutes the next.

Fix, in order

  1. Flatten the view stack to the tables the dashboard actually needs.
  2. Eliminate joins using enforced foreign keys.
  3. For the few critical queries, constrain join order (for example join_collapse_limit) so the search is small and stable.
  4. As a last resort, pin the plan with a baseline and review it on a schedule.

Trade-off

Pinning buys stability at the cost of adapting to data. In production, variance matters more than the mean.

Read moreView expansionWhen DP is out of reachReduce the number of decisions

5Estimate whether an index on orders.customer_id helps a query that returns 2% of a billion-row table stored on NVMe.Show the answer

Estimate

The crossover is about . With 100 rows per page and the default random-to-sequential ratio of 4, it is about 0.25%, so at 2% a scan wins. With NVMe and a ratio near 1.1, it is about 0.9%: still below 2%.

Unless

The table is clustered by customer_id (correlation near 1), in which case the index is cheap. Wide rows (fewer per page) raise the crossover; columnar storage lowers it.

Recommend

Do not rely on the 5% rule. Measure both plans with EXPLAIN ANALYZE, check the correlation statistic, and consider a bitmap scan as the middle path.

Caveat

All of this assumes a good estimate; if that is off by 10×, the decision is wrong whatever the formula says.

Read moreAccess path selectionLab: the index-versus-scan crossover

6Design the process that keeps statistics fresh for a fact table that grows by 1% every night.Show the answer

The problem

The default threshold waits for 0.1 × n + 50 changes: at a billion rows, 100 million changes, about 10 days. New partitions have no statistics at all.

Design

  1. Make ANALYZE of the newly loaded partition the last step of the load.
  2. Store statistics at write time if the format supports it (per-file min/max and counts at commit).
  3. Define for correlated columns that matter, such as city and postcode, or product and promotion.
  4. Count distinct values for key columns with a full-pass sketch, not from a sample.
  5. Alert when estimated and actual rows differ by more than 10× on the top queries, using captured EXPLAIN ANALYZE output.

Cost

Each analysis is a scan with a fixed cost, so choose intervals from cost against benefit; do not analyze everything daily.

Read moreStatisticsAuto-analyzeCounting distinct values from a sample

7A senior engineer proposes denormalizing everything into one big table to stop plan problems. Evaluate the proposal.Show the answer

Partly right, for the wrong reason

The usual argument, scan efficiency, is weak: columnar storage already reads only the columns needed. The real benefit is less risk: no join order to get wrong, no correlation across joins to mis-estimate, no assumption to violate, so the tail of bad plans disappears.

Costs

Storage multiplied, harder updates, redundancy, and the pre-joined table must be refreshed.

Recommend

Denormalize only for a few hot queries whose stability matters, as pre-joined marts refreshed after each load; keep the elsewhere; diagnose the rest properly; and pin the plans that must not change.

Be clear

This reduces risk by removing decisions; it does not make the optimizer any better.

Read moreReduce the number of decisionsWhat the planner cannot do

8A vendor says their optimizer is 30% better on average. Should you adopt it for production?Show the answer

Ask about the tail

A better mean bought with occasional 100× regressions is usually a bad deal: variance, not the mean, causes incidents.

Evaluate

Replay your own top queries and compare the distribution of per-query ratios, not just the average. Check plan stability across runs and after statistics refreshes.

Ask

How it handles missing statistics, correlated columns, and views that expand beyond 12 tables; and whether hints or plan baselines exist for queries that must not change.

Recommend

A staged rollout with plan capture, and automatic fallback to the old plan on regression.

Read moreA layer limited by what it can knowWhen DP is out of reach

9A dashboard for one store scans 4 TB per run on an object-store lake billed by bytes scanned. Redesign it.Show the answer

Diagnose

Check which filters reached the scan, and how many files were read. Typical causes: the store filter sits on a dimension table, so only a can use it; and files are in arrival order, so every file's store_id range covers everything.

Fix, by leverage

  1. Make the date filter , and declare the partition as a transform of the timestamp.
  2. Cluster each day's files by (store_id, order_id), so row-group statistics narrow.
  3. Make sure the engine builds a runtime filter from the small stores side: a 400-row build costs almost nothing.
  4. Project only the needed columns, nested ones included.
  5. Use Top-N for the final LIMIT.

Estimate

From 4.2 TB to about 11 GB, about 380× less.

Trade-offs

Clustering costs write time and gives only one ordering, so a dashboard filtered by product gains nothing; a runtime filter makes the build finish first. If the same page recurs, a keyed by snapshot gives a further large gain.

Read moreWhat data reduction forbidsRuntime filtersProjection pushdownResult-set caching

10A parameterized query runs in 0.2 s for most tenants and 9 minutes for the largest one, but is fast when run by hand. What happened, and what do you do?Show the answer

Diagnose

: the cached plan was built for a small tenant (an index lookup) and reused for the large one. Confirm by comparing EXPLAIN of the prepared statement with the literal version, and check whether a statistics refresh recently invalidated plans.

Options

Force custom plans for the known large tenants; use one for all (robust but mediocre); keep several plans by class of value; or plan for the worst case.

Recommend

Robust generic plans for the many, custom plans for the few largest, and an alert when a plan's hash changes after analyze.

Trade-off

Re-planning on every call costs planning time each time: fine for a 9-minute query, not for a 1 ms one.

Read morePrepared statements and plan cachingA link back to statistics

11A team turns on adaptive query execution and sees no change on a slow job. Why?Show the answer

Diagnose

The error is probably before the first stage boundary: a wrong first access path, a filter that cannot be pushed down, a missing partition filter, or a subquery that was not decorrelated. Adaptive execution helps with sizing, join substitution and skew after a shuffle.

Check

Where the first is, and where the estimate first goes wrong (estimated against actual rows per node).

Fix

Correct the part by hand (statistics, a rewrite, a hint), then let adaptive execution handle the rest.

Cost

Less determinism, and harder analysis of plan regressions.

Read moreThe limits of adaptivityCosts

12A 2-billion-row fact table is unsorted on customer_id, and users filter on it with both equality and ranges. What can you do?Show the answer

Equality

per .

Ranges

are useless on unsorted data: with a million rows per group, each group's range covers ≈ 99.9998% of the domain. No metadata fixes that. Either cluster by customer_id (a write cost, and the table gets only one ordering for other access patterns) or build a real index or a second copy sorted by customer (space and maintenance).

Decide

By how common each kind of query is.

Read moreThree families of skipping metadataWhy min/max skipping works or fails completely

13SELECT COUNT(*) FROM events WHERE day = '2025-01-01' takes 40 s on one table and 0.2 s on its twin. How do you explain it?Show the answer

The mechanism

A count can be answered from metadata only if the filter is fully resolved by pruning and the stored counts are accurate.

Likely causes

The slow twin uses with , so file counts are not live counts; or a filter remains that pruning cannot fully resolve, for example a partition based on ts while day is a derived column.

Verify

Does the scan node read any data files at all?

Fix

Compact to apply deletes, maintain a live count, or declare the partition as a transform of the timestamp.

Read moreMetadata-only answersPartition pruning

14Decide whether to enable just-in-time compilation for an interactive BI workload.Show the answer

Estimate

Break-even is n* = C / Δc: with a 50 ms compile and 20 ns saved per row, 2.5 million rows.

Decide

If most interactive queries touch fewer rows, compilation only adds latency; for the few heavy ones it pays. Prefer an engine that compiles adaptively, interpreting first and switching once compiled code is ready, or set a cost threshold.

Note

Vectorization and compilation are substitutes for each other; measure on your own workload.

Read moreCompile or interpret

15Should you turn on a query result cache for the whole warehouse?Show the answer

The rule

Hit rate × query cost must exceed storage plus invalidation cost.

Where it works

With versioned, immutable tables, the validity check is one snapshot comparison, so yes for dashboards that repeat. For tables updated in place, tracking changes can cost more than the query.

Matching

Exact-text matching has a poor hit rate (whitespace, parameters, session settings); semantic matching is incomplete and fails silently.

First

Measure the repeat rate from the query log: recurrence is a property of people, not of data.

Read moreResult-set caching