Question presented to candidate: "You need the 3 most recent orders for every customer. You could write it with ROW_NUMBER() OVER (PARTITION BY ...), with a LATERAL join, or with a correlated subquery. They all return the same rows, so which one would you ship, and how does the answer change when there are 100 customers versus 200,000 customers on the same 1 million orders?"
What a strong answer should cover:
- All three return the same rows when they use the same order and tiebreaker; the difference is how much data each one reads
- Window function: reads every row of the table once (sorted, or in index order), cost grows with the total number of rows, not with N; PostgreSQL 15+ adds a Run Condition but still reads all rows
- LATERAL (
CROSS JOIN LATERAL/ SQL ServerCROSS APPLY): one index seek plus N rows per group, cost grows with groups x N; needs an index on(group_key, sort_key)and a cheap list of groups - Correlated subquery:
IN (... LIMIT N)runs once per outer row (not per group), and theCOUNT(*)form is quadratic inside each group; it is the portable fallback, rarely the fast one - The decision rule: few groups with many rows each and a matching index means LATERAL; many small groups, no index, or "rank everything anyway" means the window; prove it with
EXPLAIN ANALYZE(loops and rows)
Clarifying questions expected:
- How many groups are there, and how many rows per group (the ratio decides it)?
- Is there an index on
(customer_id, ordered_at DESC), and is there a separate customers table to drive the loop? - Do ties need to be handled (exactly N rows, or every row tied at the cut)?
Code / implementation expected: Yes — the same top-N query written all three ways, plus the plans that explain why one reads 10 rows and another reads 100,000.
Schema used by the examples: