SQL Interview & Case Lab
Back to Business SQL Patterns

Recap — Top-N Without Falling Into the Trap

ROW_NUMBER, RANK, DENSE_RANK. Same query shape, three different answers. The interviewer is testing whether you know which.

16 minutesInteractive tutorial
🎯 Free Guest Mode: You are learning for free. Sign in to save your completion progress and quiz answers.

The setup: find the top 3 products by revenue in each region. Every analyst has been asked this. The query template is:

WITH ranked AS (
    SELECT
        region,
        product,
        revenue,
        ROW_NUMBER() OVER (PARTITION BY region ORDER BY revenue DESC) AS rn
    FROM product_revenue
)
SELECT * FROM ranked WHERE rn <= 3;

The trick: there are three window functions you could use here, and they give different answers when there are ties.

ROW_NUMBER vs RANK vs DENSE_RANK

Suppose three products in a region all have revenue = ₹10,000:

ProductRevenueROW_NUMBERRANKDENSE_RANK
A10,000111
B10,000211
C10,000311
D9,000442
E8,000553
  • ROW_NUMBER — arbitrary tiebreak (often by insertion order). Always returns distinct integers.
  • RANK — same rank for ties, then a gap. Three at rank 1, next is rank 4.
  • DENSE_RANK — same rank for ties, no gap. Three at rank 1, next is rank 2.

Which one does the interviewer want?

It depends on the question:

  • "Give me 3 rows per region" → ROW_NUMBER() <= 3. Returns exactly 3 even with ties (arbitrary tiebreak).
  • "Give me the top 3 products including ties" → DENSE_RANK() <= 3. Could return more than 3 rows if there are ties.
  • "Top 3 by rank, with gaps for ties" → RANK() <= 3. Returns ties at the same rank; if 3 products tie for #1, only those 3 come back (no #4).

The most common interview wrong answer: using ROW_NUMBER when the question said "top 3 including ties". You'll silently drop tied products.

The deterministic-tiebreak trap

ROW_NUMBER() OVER (PARTITION BY region ORDER BY revenue DESC) doesn't guarantee deterministic ordering for ties. If you re-run the query you could get different rows back. The fix:

ROW_NUMBER() OVER (PARTITION BY region ORDER BY revenue DESC, product_id ASC)

Add a tiebreaker column. Now ties resolve deterministically.

The N+1 trap

The classic interview follow-up: "What if N is a parameter?" The right SQL:

WITH ranked AS (
    SELECT *, ROW_NUMBER() OVER (PARTITION BY region ORDER BY revenue DESC) AS rn
    FROM product_revenue
)
SELECT * FROM ranked WHERE rn <= :n;

What you should avoid: LIMIT :n per region — there's no LIMIT PER GROUP in standard SQL. Some databases (PostgreSQL) have LATERAL joins for this; others (MySQL) don't. Window functions are portable.

Performance note

For very large tables with cheap indexes, LATERAL joins can beat window functions:

SELECT r.region, p.*
FROM regions r
JOIN LATERAL (
    SELECT * FROM product_revenue pr
    WHERE pr.region = r.region
    ORDER BY pr.revenue DESC
    LIMIT 3
) p ON TRUE;

But it's PostgreSQL-only. In interviews, default to window functions unless asked.

Takeaway

Listen to the question. "Top 3" with ties means DENSE_RANK or RANK; without ties means ROW_NUMBER. Always ask the interviewer how to handle ties — it shows judgment.

Question 1 of 10Answer saved0 answered

An interviewer asks for 'top 3 products by revenue in each region, including ties'. Which window function?