Top-N per group query in SQL - Time & Space Complexity
When we ask for the top N items in each group from a database, we want to know how the work grows as the data grows.
How does the time to get these top items change when there are more groups or more rows?
Analyze the time complexity of the following code snippet.
SELECT department, employee, salary
FROM (
SELECT department, employee, salary,
ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS rn
FROM employees
) sub
WHERE rn <= 3;
This query finds the top 3 highest salaries in each department from the employees table.
Identify the loops, recursion, array traversals that repeat.
- Primary operation: The window function scans all rows and assigns a rank within each department group.
- How many times: It processes every row once, but internally it sorts each group to assign ranks.
Explain the growth pattern intuitively.
| Input Size (n) | Approx. Operations |
|---|---|
| 10 | About 10 rows processed, sorting small groups quickly |
| 100 | About 100 rows processed, sorting groups takes more time |
| 1000 | About 1000 rows processed, sorting each group grows with group size |
Pattern observation: The work grows roughly in proportion to the total number of rows, with extra cost for sorting within each group.
Time Complexity: O(n log n)
This means the time grows a bit faster than the number of rows because sorting happens inside each group.
[X] Wrong: "The query only looks at the top 3 rows per group, so it runs in constant time per group."
[OK] Correct: The database must first sort all rows in each group to find the top 3, so it processes all rows, not just 3.
Understanding how grouping and sorting affect query time helps you explain your approach clearly and shows you know how databases handle data efficiently.
"What if we changed ROW_NUMBER() to RANK() or DENSE_RANK()? How would the time complexity change?"
Practice
ROW_NUMBER() OVER (PARTITION BY column ORDER BY column) clause do in a SQL query?Solution
Step 1: Understand ROW_NUMBER() function
ROW_NUMBER() assigns a unique sequential number to rows in the result set.Step 2: Understand PARTITION BY usage
PARTITION BY divides rows into groups, and ROW_NUMBER() restarts numbering for each group.Final Answer:
Assigns a unique rank number to each row within each group defined by the partition column. -> Option AQuick Check:
ROW_NUMBER() with PARTITION BY = unique rank per group [OK]
- Thinking ROW_NUMBER() counts total rows
- Confusing PARTITION BY with filtering
- Assuming it filters rows automatically
employees table?Solution
Step 1: Understand ROW_NUMBER() usage in subquery
ROW_NUMBER() must be used in a subquery or CTE to assign ranks before filtering.Step 2: Filter rows by rank in outer query
The outer query filters rows where the rank (rn) is 2 or less per department.Final Answer:
SELECT * FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS rn FROM employees) sub WHERE rn <= 2; -> Option AQuick Check:
Use subquery with ROW_NUMBER() and filter by rank [OK]
- Trying to filter ROW_NUMBER() directly in WHERE clause
- Using LIMIT with PARTITION BY (not valid syntax)
- Using TOP without proper partitioning
sales with columns region, salesperson, and amount, what will this query return?SELECT region, salesperson, amount FROM (SELECT region, salesperson, amount, ROW_NUMBER() OVER (PARTITION BY region ORDER BY amount DESC) AS rn FROM sales) AS ranked WHERE rn = 1;
Solution
Step 1: Analyze ROW_NUMBER() with PARTITION BY region
Rows are grouped by region and ranked by amount descending, so rn=1 is highest amount per region.Step 2: Filter WHERE rn = 1
This keeps only the top-ranked row per region, i.e., the highest sale amount record.Final Answer:
The highest sale amount record for each region. -> Option CQuick Check:
ROW_NUMBER() = 1 per group returns top record [OK]
- Thinking it returns all rows sorted
- Confusing rn=1 with lowest amount
- Assuming amount = 1 filters rows
orders table:SELECT customer_id, order_id, order_date, ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY order_date DESC) AS rank FROM orders WHERE rank <= 3;
Solution
Step 1: Understand alias usage in WHERE clause
Aliases like 'rank' defined in SELECT cannot be used in WHERE clause directly because WHERE is evaluated before SELECT.Step 2: Correct approach using subquery or CTE
Use a subquery or CTE to assign ROW_NUMBER(), then filter on 'rank' in outer query.Final Answer:
You cannot filter on an alias in the WHERE clause directly. -> Option DQuick Check:
Filter on ROW_NUMBER() alias in outer query, not WHERE [OK]
- Using alias in WHERE clause directly
- Thinking ORDER BY DESC is invalid
- Adding unnecessary GROUP BY
products table with columns category, product_id, and price. You want to find the top 2 most expensive products per category, but if two products have the same price, include both even if it means more than 2 per category. Which query achieves this?Solution
Step 1: Understand difference between ROW_NUMBER(), RANK(), and DENSE_RANK()
ROW_NUMBER() assigns unique ranks, so ties get different numbers. RANK() assigns same rank to ties and skips ranks after. DENSE_RANK() assigns same rank to ties but no gaps.Step 2: Choose RANK() to include ties at second place
RANK() allows including all products tied at rank 2, possibly more than 2 rows per category.Final Answer:
Use RANK() partitioned by category ordered by price DESC and filter WHERE rank <= 2. -> Option BQuick Check:
RANK() includes ties with same rank [OK]
- Using ROW_NUMBER() excludes ties
- Using DENSE_RANK() includes ties but no gaps
- Using COUNT() is invalid for ranking
Ready to prove it?
Pass the test and this pattern moves unknown
