Top N Per Group: Advanced
"Now Do It Without Window Functions"
Identify when a question requires per-group ranking instead of a global ORDER BY LIMIT.
- ▸"top N highest/lowest X per group"
- ▸"the most-recent N records for each customer"
- ▸"for every department, return the top performer"
- ▸"top N including ties" / "top N salary levels" (tells: RANK vs DENSE_RANK)
- ▸"how would you keep this updated as the data changes?" (the materialization escalation)
What the interviewer is actually testing at this level
The question has three layers and the interviewer scores each one independently. Layer one: do you produce the right query with the right tie-handling function and a deterministic tiebreaker? Most candidates clear this. Layer two: can you reason about the supporting infrastructure (index, cluster key, partition layout) that makes the query viable at billion-row scale? Some candidates clear this. Layer three: can you design the materialization and refresh strategy, defend it against alternatives, and articulate the failure modes you have seen in production? Most of the conversation lives here.
- ▸Layer 1: correct query with deterministic tiebreaker (most candidates clear)
- ▸Layer 2: supporting infrastructure (index, cluster key, partition layout)
- ▸Layer 3: materialization and refresh strategy with production failure modes
- ▸All three layers must surface for a strong-hire verdict
- Writes ROW_NUMBER with a tiebreaker, defends the function choice cleanly
- Discusses indexes when prompted
- Goes thin when the interviewer asks 'how do you keep this updated'
- Treats the question as primarily a SQL question
- Writes the query in two minutes and then narrates the architecture
- Names the materialization strategy unprompted (table, view, or on-demand)
- Defends the strategy against the refresh-vs-cost tradeoff
- Anticipates the production failure modes: skew, drift, late updates
Why this question stays in staff loops
Correlated-Subquery Top-N Fallback
Write a correct top-N-per-group query as a correlated subquery (no window functions), with a tie-breaking condition that encodes the ORDER BY contract and matches ROW_NUMBER semantics.
The correlated-subquery form, with the tiebreaker contract
Narrating the subquery to the interviewer
The choice the interviewer is silently scoring
The OR cascade in the subquery is the ORDER BY, rewritten as a predicate. Read them side by side: ORDER BY salary DESC, hire_date ASC, employee_id ASC becomes 'e2 outranks e1 if higher salary, or equal salary and earlier hire_date, or equal on both and lower employee_id.' The last clause must key on a unique column, or ties never fully break and you get RANK, not exactly-N.
Where this query actually lives
Top-N Performance and Index Strategy
Choose between ROW_NUMBER, RANK, and DENSE_RANK based on whether ties should inflate, collapse, or be broken arbitrarily.
The function-choice decision as a contract
Concrete failure mode for each wrong choice
| Function | Tie behavior | Total rows per group | Bug surface |
|---|---|---|---|
| ROW_NUMBER | Tiebreaker breaks tie | Exactly N | Wrong tiebreaker → non-determinism, wrong winners |
| RANK | Ties share rank, gap follows | ≥ N | Tied positions inflate the result silently |
| DENSE_RANK | Ties share rank, no gap | ≥ N | Many employees at top distinct salaries explodes the count |
The NULL question at scale
The dialect quirk that matters at scale
Distributed engines reward queries that are explicit about determinism. 'The tiebreaker column guarantees the same result on parallel workers; without it, two workers can see the same tied rows in different orders and produce different ROW_NUMBER assignments.' That sentence is what distinguishes a candidate who has tuned for a single engine from a candidate who has shipped against many.
Lateral / CROSS APPLY Top-N Per Row
Solve top-N-per-group using a correlated subquery or self-join when the interviewer bans window functions.
The correlated subquery plan, by engine
The self-join fallback
The semantic conversion that distinguishes the answer
- Engine is modern (Postgres 12+, BigQuery, Snowflake)
- Optimizer rewrites the subquery into a hash semi-join
- Readability matters and the team will revisit the SQL
- RANK semantics are acceptable
- Engine is older or has weak subquery rewriting
- Plan predictability matters more than syntactic elegance
- You need full control over the join order
- The tiebreaker is multi-column and the subquery becomes unwieldy
Mention specific engine versions in this discussion. 'On Postgres 12 and above, the optimizer rewrites this; on Postgres 11 and below, it does not, and the self-join is required.' Specificity at this layer is the signal. Generic statements about 'older engines' read as bookish; specific version numbers read as someone who has tuned a query on that engine.
Defending the Tie Rule on Performance
Discuss index strategies, partition pruning, and how to handle "top N with ties" at warehouse scale.
Layer 1 and 2: physical layout and data skew
Layer 3: materialization and refresh
Layer 4: late-arriving updates and contract drift
| Architectural question | Mid-level phrasing | Staff phrasing |
|---|---|---|
| Where does the top-N live? | "In the dashboard query." | "In a top_employees_per_dept table refreshed on the data's cadence; the dashboard does a single point lookup. The window function lives in the refresh job, not the read path." |
| What's the scale bottleneck? | "Sorting each partition." | "The shuffle that co-locates each partition on one worker; once co-located, the sort is cheap. If the source is partitioned by date and queried by department, the shuffle is the dominant cost." |
| What about a single huge partition? | "It runs slower." | "Window functions don't parallelize within a partition. The skew turns a parallel query into a sequential one. Mitigation: salt the giant partition with a sub-key, or materialize it separately, and alert when any partition exceeds 5% of fact-table rows." |
| What about retroactive corrections? | "Re-run the job." | "Versioned materialization keyed on (department_id, refresh_version). Corrections trigger a partial recompute scoped to the affected department; dashboards read the latest version; old versions retained for audit with a TTL cleanup." |
| What if dev uses Postgres and prod uses Snowflake? | "Same SQL, should work." | "Same SQL, different determinism. Postgres tends to be stable for the wrong reason (a stable scan order); Snowflake's parallel passes can produce different ROW_NUMBER assignments on tied rows. The deterministic tiebreaker has to include a unique column to survive both engines." |
- Recommends an index and a cluster key
- Suggests materialization without naming the refresh cadence
- Cannot articulate what happens on data skew
- Has no answer for retroactive updates
- Maps the query to its supporting layer on both OLTP and OLAP engines
- Names the skew failure mode and the mitigation
- Recommends incremental refresh tied to the data change rate
- Articulates the operational concerns: corrections, deletes, partition drift
Distributed execution: the bottleneck is the shuffle
- ▸Source partitioned or clustered by the partition column → shuffle is a no-op
- ▸Source partitioned by date → expensive shuffle on every refresh
- ▸One hot partition → single worker dominates; mitigate by splitting or materializing separately
- ▸Materialization with incremental refresh aligned to the data change rate
The closing summary
> You are in a Meta data engineering interview. The interviewer asks: 'You own the executive compensation dashboard. It shows top 3 highest-paid employees per department, refreshed hourly off a billion-row employees table. Walk me through your design.'
OR cascade is the ORDER BY rewritten as a predicate, and its final clause must key on a unique column such as employee_id. Counting only strictly-greater salaries gives you RANK semantics, not exactly N rows per group.ROW_NUMBER for exactly N rows to act on, RANK for top N positions where ties consume extra rows, DENSE_RANK for top N distinct levels. The wrong pick is a quiet bug that surfaces when finance audits the bonus pool.ROW_NUMBER whose ORDER BY does not break every tie is non-deterministic because parallel workers see tied rows in different orders; a query that is stable on single-node Postgres can return different results in production on Snowflake.HAVING COUNT(e2.employee_id) < 3 is the form to reach for where the rewrite does not happen.hash(advertiser_id) % 8 and materialized it separately.The single most common SQL interview question, hiding in plain sight
- Category
- SQL
- Difficulty
- advanced
- Duration
- 25 minutes
- Challenges
- 0 hands-on challenges
Topics covered: "Now Do It Without Window Functions", Correlated-Subquery Top-N Fallback, Top-N Performance and Index Strategy, Lateral / CROSS APPLY Top-N Per Row, Defending the Tie Rule on Performance
Lesson Sections
- "Now Do It Without Window Functions" (concepts: sqlRowNumber)
The question opens with 'top 3 highest-paid employees per department.' By the third follow-up, the conversation is about how to maintain that view continuously as the employees table changes, what happens when a region's headcount triples, and whether the right answer is a materialized view, a precomputed table, or an on-demand query. The SQL itself is settled in the first two minutes. The remaining time is about whether you can design the system that owns the SQL. What the interviewer is actual
- Correlated-Subquery Top-N Fallback (concepts: sqlSubqueryCorrelated)
At this level the interviewer often removes window functions from the table: 'now do it without ROW_NUMBER.' The move that earns the credit is not just producing a working correlated subquery; it is producing one whose tie-breaking condition is a deliberate contract, then pivoting the conversation to where the query actually lives in production: as a materialized table, refreshed on a cadence, fed by a partitioned source, consumed by a dashboard with a documented freshness SLA. The correlated-su
- Top-N Performance and Index Strategy (concepts: sqlRankDenseRank)
By this layer, the choice between ROW_NUMBER, RANK, and DENSE_RANK is not a syntax memory test. The interviewer is checking whether you can map the business question to the right function and articulate the failure modes of picking wrong. The function choice is a contract; the contract has consequences when ties exist in real data. The function-choice decision as a contract Each function answers a different question. ROW_NUMBER answers 'who are the N people we want to act on, exactly N, ties bro
- Lateral / CROSS APPLY Top-N Per Row (concepts: sqlSelfJoin)
At this level the no-window question becomes a probe for whether you understand the optimizer's behavior across engines. The two fallback forms are familiar: correlated subquery and self-join. The senior conversation is about which one the engine actually plans well and what it costs when the plan goes wrong. The correlated subquery plan, by engine On a modern engine, the correlated subquery 'WHERE (SELECT COUNT(*) FROM e2 WHERE ... ) < 3' is rewritten by the optimizer into a hash-based set oper
- Defending the Tie Rule on Performance (concepts: sqlRankDenseRank)
The closing layer is the architectural conversation. The interviewer asks how this scales to a billion rows, then how it scales to a billion rows with hourly refresh, then how it scales to a billion rows with hourly refresh on a distributed engine with skewed partition sizes. The answer is layered. Each layer adds a concept; each layer requires you to name a tradeoff. This is the section where the candidate's depth becomes visible. Layer 1 and 2: physical layout and data skew On an OLTP database