DEV Community

The AI producer
The AI producer

Posted on

10 SQL Window Functions That Separate Junior From Senior Developers

10 SQL Window Functions That Separate Junior From Senior Developers

If you've ever stared at a SQL query wondering "there has to be a better way," window functions are usually the answer. They let you perform calculations across a set of rows related to the current row — without collapsing them like GROUP BY does.

Here are 10 window functions I use weekly, with examples that show the difference between "it works" and "it's elegant."

1. ROW_NUMBER() — Ranking Without Ties

SELECT 
    employee_name,
    department,
    salary,
    ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) as rank_in_dept
FROM employees;
Enter fullscreen mode Exit fullscreen mode

Junior approach: A subquery with COUNT(*) for every row. Senior approach: One clean window function.

2. RANK() vs DENSE_RANK() — Know the Difference

-- RANK(): 1, 2, 2, 4 (skips after ties)
-- DENSE_RANK(): 1, 2, 2, 3 (no gaps)
SELECT 
    product_name,
    sales,
    RANK() OVER (ORDER BY sales DESC) as rank,
    DENSE_RANK() OVER (ORDER BY sales DESC) as dense_rank
FROM products;
Enter fullscreen mode Exit fullscreen mode

Use DENSE_RANK() when you need a continuous ranking (top 10 products). Use RANK() when position matters (Olympic medals).

3. LAG() — Compare With Previous Row

SELECT 
    date,
    revenue,
    LAG(revenue, 1) OVER (ORDER BY date) as prev_day_revenue,
    revenue - LAG(revenue, 1) OVER (ORDER BY date) as daily_change,
    ROUND(
        (revenue - LAG(revenue, 1) OVER (ORDER BY date)) * 100.0 
        / NULLIF(LAG(revenue, 1) OVER (ORDER BY date), 0), 2
    ) as pct_change
FROM daily_sales;
Enter fullscreen mode Exit fullscreen mode

This replaces a self-join that's 5x slower on large tables.

4. LEAD() — Peek at the Next Row

-- Find users whose subscription expires next month
SELECT 
    user_id,
    plan_name,
    renewal_date,
    LEAD(plan_name) OVER (PARTITION BY user_id ORDER BY renewal_date) as next_plan
FROM subscriptions
WHERE renewal_date < DATE_ADD(CURDATE(), INTERVAL 2 MONTH);
Enter fullscreen mode Exit fullscreen mode

5. SUM() OVER — Running Totals

SELECT 
    date,
    daily_revenue,
    SUM(daily_revenue) OVER (ORDER BY date) as cumulative_revenue,
    SUM(daily_revenue) OVER (
        ORDER BY date 
        ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
    ) as rolling_7_day_avg
FROM daily_sales;
Enter fullscreen mode Exit fullscreen mode

Running totals and moving averages without a single subquery.

6. AVG() OVER — Department Averages in One Query

SELECT 
    employee_name,
    department,
    salary,
    ROUND(AVG(salary) OVER (PARTITION BY department), 2) as dept_avg,
    ROUND(
        salary * 100.0 / AVG(salary) OVER (PARTITION BY department), 1
    ) as pct_of_dept_avg
FROM employees;
Enter fullscreen mode Exit fullscreen mode

Who's above or below their department average — instantly.

7. FIRST_VALUE() and LAST_VALUE() — Boundary Values

SELECT 
    date,
    stock_price,
    FIRST_VALUE(stock_price) OVER (ORDER BY date) as opening_price,
    LAST_VALUE(stock_price) OVER (
        ORDER BY date 
        ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
    ) as closing_price
FROM stock_prices;
Enter fullscreen mode Exit fullscreen mode

Gotcha: LAST_VALUE() without the ROWS BETWEEN frame returns the current row, not the actual last row. This trips up everyone the first time.

8. NTILE() — Bucket Data Into Percentiles

-- Divide customers into 4 quartiles by spending
SELECT 
    customer_name,
    total_spent,
    NTILE(4) OVER (ORDER BY total_spent DESC) as spending_quartile
FROM customers;
Enter fullscreen mode Exit fullscreen mode

Perfect for "top 25%" reports and cohort analysis.

9. PERCENT_RANK() — Exact Percentile Position

SELECT 
    student_name,
    test_score,
    ROUND(PERCENT_RANK() OVER (ORDER BY test_score DESC) * 100, 1) as percentile
FROM test_scores;
Enter fullscreen mode Exit fullscreen mode

More precise than NTILE — gives you the exact position (0.0 to 1.0).

10. CUME_DIST() — Cumulative Distribution

-- What percentage of orders are at or below this amount?
SELECT 
    order_id,
    order_total,
    ROUND(CUME_DIST() OVER (ORDER BY order_total) * 100, 1) as pct_at_or_below
FROM orders;
Enter fullscreen mode Exit fullscreen mode

Great for pricing analysis: "70% of our orders are under $50."

Bonus: The FRAME Clause

Every window function supports a frame:

ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW   -- default for running totals
ROWS BETWEEN 6 PRECEDING AND 6 FOLLOWING            -- centered moving average
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING  -- entire partition
RANGE BETWEEN INTERVAL '7' DAY PRECEDING AND CURRENT ROW   -- date-based window
Enter fullscreen mode Exit fullscreen mode

Understanding frames is what separates "I know window functions" from "I'm an expert."

Why This Matters

Window functions can replace complex self-joins, correlated subqueries, and application-level processing. A query that takes 200 lines of Python post-processing often becomes a 10-line SQL query.


Want 100+ more SQL interview problems with detailed solutions? Check out my SQL Interview Guide — it covers JOINs, window functions, optimization, and real-world scenarios.

What's your favorite window function trick? Drop it in the comments!

Top comments (0)