SQL Window Functions Explained with Examples
If you've been writing SQL for a while, you've probably hit a wall where GROUP BY just isn't enough. You need to rank rows, compute running totals, or compare a value with the previous row — all without collapsing your result set. That's where window functions come in.
What Are Window Functions?
A window function performs a calculation across a set of rows that are somehow related to the current row. Unlike GROUP BY, window functions don't collapse rows — you keep the full result set while adding computed columns.
SELECT
employee_name,
department,
salary,
RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dept_rank
FROM employees
This ranks employees by salary within each department, without removing any rows.
The OVER() Clause
Every window function uses OVER() to define the window. The two main parts are:
PARTITION BY— Divides rows into groups (like GROUP BY, but without collapsing)ORDER BY— Determines the order of rows within each partition
ROW_NUMBER() — Unique Sequential Numbers
SELECT
order_id,
customer_id,
ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY order_date) AS order_sequence
FROM orders
This gives each customer's orders a sequential number (1st order, 2nd order, etc.).
RANK() and DENSE_RANK() — Handling Ties
SELECT
student_name,
score,
RANK() OVER (ORDER BY score DESC) AS rank,
DENSE_RANK() OVER (ORDER BY score DESC) AS dense_rank
FROM exam_results
RANK() leaves gaps after ties (1, 2, 2, 4). DENSE_RANK() doesn't (1, 2, 2, 3).
LAG() and LEAD() — Accessing Adjacent Rows
SELECT
report_date,
revenue,
LAG(revenue) OVER (ORDER BY report_date) AS prev_revenue,
revenue - LAG(revenue) OVER (ORDER BY report_date) AS revenue_change
FROM monthly_revenue
LAG() accesses the previous row, LEAD() accesses the next. Perfect for calculating period-over-period changes.
Running Totals with SUM() OVER()
SELECT
transaction_date,
amount,
SUM(amount) OVER (ORDER BY transaction_date) AS running_total
FROM transactions
This creates a cumulative sum that grows with each row.
Moving Averages with ROWS BETWEEN
SELECT
report_date,
daily_sales,
AVG(daily_sales) OVER (
ORDER BY report_date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) AS seven_day_avg
FROM sales_data
This computes a 7-day moving average — incredibly useful for smoothing noisy metrics.
NTILE() — Bucketing Rows
SELECT
customer_id,
total_spend,
NTILE(4) OVER (ORDER BY total_spend DESC) AS spend_quartile
FROM customer_summary
Divides customers into 4 equal buckets by spending. Quartile 1 = top spenders.
When to Use Window Functions
- Ranking items within groups (top products per category)
- Calculating running totals and averages
- Comparing current row to previous/next row
- Deduplicating rows (ROW_NUMBER + CTE to keep only the latest)
- Percentile and quartile analysis
Performance Tips
- Index the ORDER BY column: Window functions sort data; indexes help.
- Limit partitions: Very large partitions are expensive.
- Use CTEs: Break complex window function queries into readable Common Table Expressions.
Conclusion
Window functions are the bridge between simple queries and advanced analytics. Once you internalize OVER(PARTITION BY ... ORDER BY ...), you'll find yourself reaching for them constantly. Start with ROW_NUMBER and RANK, then graduate to LAG/LEAD and running totals — your SQL game will never be the same.
