By Fatskills Exam Guides Team — the exam nerds behind 28,500+ quizzes and 2.1M practice questions across 500+ global exams.
(ROW_NUMBER, RANK, DENSE_RANK, NTILE)
Window functions let you analyze data across rows without collapsing them—unlike GROUP BY, which squashes rows into aggregates. Think of them as a periscope that lets you peek at neighboring rows while keeping the full dataset intact.
GROUP BY
Why this matters in production:- Ranking: "Show me the top 3 sales reps per region" (without GROUP BY destroying the detail).- Time-series gaps: "Flag missing days in a daily sales log" (without self-joins).- Running totals: "Calculate cumulative revenue by month" (without recursive CTEs).- Percentiles: "Identify the 90th percentile of order values" (without manual sorting).
Real-world scenario:You inherit a dashboard that shows "Top 5 products by revenue per store." The current query uses a GROUP BY + subquery for each store, running in 12 seconds. Rewrite it with RANK()—it drops to 0.8 seconds and scales to 10,000 stores.
RANK()
n
PARTITION BY
ORDER BY
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
sales_reps
rep_id
region
revenue
quarter
Write a query to: 1. Rank sales reps by revenue within each region.2. Show only the top 2 reps per region.3. Handle ties (e.g., two reps with $100K revenue).
SELECT region, rep_id, revenue FROM sales_reps ORDER BY region, revenue DESC;
Expected output:
region | rep_id | revenue ---------+--------+-------- East | 101 | 150000 East | 102 | 120000 East | 103 | 120000 -- Tie West | 201 | 180000 West | 202 | 160000 ...
SELECT region, rep_id, revenue, RANK() OVER (PARTITION BY region ORDER BY revenue DESC) AS rank FROM sales_reps;
Key:- PARTITION BY region: Resets the rank for each region.- ORDER BY revenue DESC: Ranks highest revenue first.
PARTITION BY region
ORDER BY revenue DESC
Output:
region | rep_id | revenue | rank -------+--------+---------+------ East | 101 | 150000 | 1 East | 102 | 120000 | 2 East | 103 | 120000 | 2 -- Tie West | 201 | 180000 | 1 West | 202 | 160000 | 2
WITH ranked_reps AS ( SELECT region, rep_id, revenue, RANK() OVER (PARTITION BY region ORDER BY revenue DESC) AS rank FROM sales_reps ) SELECT region, rep_id, revenue, rank FROM ranked_reps WHERE rank <= 2;
Why this works:- The CTE (ranked_reps) calculates ranks first.- The outer query filters for rank <= 2.
ranked_reps
rank <= 2
SELECT region, rep_id, revenue, RANK() OVER (PARTITION BY region ORDER BY revenue DESC) AS rank, DENSE_RANK() OVER (PARTITION BY region ORDER BY revenue DESC) AS dense_rank FROM sales_reps;
region | rep_id | revenue | rank | dense_rank -------+--------+---------+------+----------- East | 101 | 150000 | 1 | 1 East | 102 | 120000 | 2 | 2 East | 103 | 120000 | 2 | 2 East | 104 | 110000 | 4 | 3 -- Key difference!
Production insight:- Use RANK() for "Top N" where gaps matter (e.g., "Top 3" should skip rank 4 if there’s a tie).- Use DENSE_RANK() for consecutive numbering (e.g., customer tiers).
DENSE_RANK()
sql CREATE INDEX idx_sales_reps_region_revenue ON sales_reps (region, revenue DESC);
SELECT *
QUALIFY
sql -- Snowflake/BigQuery SELECT region, rep_id, revenue FROM sales_reps QUALIFY RANK() OVER (PARTITION BY region ORDER BY revenue DESC) <= 2;
sql RANK() OVER (PARTITION BY region ORDER BY revenue DESC) AS revenue_rank
sql WITH daily_sales AS ( SELECT date, SUM(amount) AS total FROM orders GROUP BY date ), ranked_sales AS ( SELECT date, total, RANK() OVER (ORDER BY total DESC) AS daily_rank FROM daily_sales ) SELECT * FROM ranked_sales WHERE daily_rank <= 5;
COALESCE
NULLS LAST
sql ORDER BY revenue DESC NULLS LAST
ROW_NUMBER()
NULLS FIRST/LAST
Answer: Use RANK() if ties should share a rank.
Percentiles:
NTILE(100)
PERCENT_RANK()
Answer: NTILE(100) for approximate buckets; PERCENT_RANK() for exact percentiles.
Running totals:
ROWS UNBOUNDED PRECEDING
sql SUM(revenue) OVER (ORDER BY month ROWS UNBOUNDED PRECEDING)
NTILE(n)
You have a table employee_salaries with columns: department, employee_id, salary.Write a query to: 1. Rank employees by salary within each department.2. Show only employees in the top 25% of their department (use NTILE(4)).
employee_salaries
department
employee_id
salary
NTILE(4)
WITH ranked_employees AS ( SELECT department, employee_id, salary, NTILE(4) OVER (PARTITION BY department ORDER BY salary DESC) AS salary_quartile FROM employee_salaries ) SELECT department, employee_id, salary FROM ranked_employees WHERE salary_quartile = 1;
Why it works:- NTILE(4) divides each department into 4 equal buckets (quartiles).- salary_quartile = 1 filters for the top 25%.
salary_quartile = 1
ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC)
RANK() OVER (PARTITION BY dept ORDER BY salary DESC)
DENSE_RANK() OVER (PARTITION BY dept ORDER BY salary DESC)
NTILE(4) OVER (PARTITION BY dept ORDER BY salary DESC)
SUM(salary) OVER (ORDER BY date ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING)
PARTITION BY region, month
QUALIFY RANK() OVER (...) <= 2
ORDER BY salary DESC NULLS LAST
Join 4M+ learners. Unlock unlimited quizzes, wrong-answer tracking, flashcards + reminders, study guides, and 1-on-1 challenges.