Wicked Smart Data
LearnArticlesAbout
Sign InSign Up
LearnArticlesAboutContact
Sign InSign Up
Wicked Smart Data

The go-to platform for professionals who want to master data, automation, and AI — from Excel fundamentals to cutting-edge machine learning.

Platform

  • Learning Paths
  • Articles
  • About
  • Contact

Connect

  • Contact Us
  • RSS Feed

© 2026 Wicked Smart Data. All rights reserved.

Privacy PolicyTerms of Service
All Articles
Hero image for Window Functions: RANK, ROW_NUMBER, and LAG

Window Functions: RANK, ROW_NUMBER, and LAG

SQL⚡ Practitioner18 min readMar 23, 2026Updated Mar 24, 2026
Table of Contents
  • Prerequisites
  • Understanding Window Functions vs Traditional Approaches
  • RANK vs ROW_NUMBER: Handling Ties Like a Pro
  • When to Use Each Function
  • LAG and LEAD: Time Series Analysis Made Simple
  • Basic LAG Usage for Period-over-Period Analysis
  • Advanced LAG Patterns: Multiple Periods and Default Values
  • LEAD for Forward-Looking Analysis
  • Combining LAG with Conditional Logic for Trend Analysis
  • Advanced Partitioning Strategies
  • Multi-Level Partitioning
  • Dynamic Partitioning for Comparative Analysis

Master SQL Window Functions: RANK, ROW_NUMBER, and LAG in Real-World Scenarios

Picture this: You're analyzing e-commerce performance data and need to identify the top 3 products by revenue in each category, find the best-performing month for each product, and calculate month-over-month growth rates. Your first instinct might be to write multiple queries with subqueries and self-joins, creating a mess of complex SQL that's hard to read and maintain.

There's a better way. Window functions let you perform these calculations elegantly in a single query, computing rankings, running totals, and period-over-period comparisons without the complexity of traditional grouping approaches. They're the difference between amateur and professional-level SQL.

By the end of this lesson, you'll wield three of the most powerful window functions with confidence, turning complex analytical questions into clean, readable SQL.

What you'll learn:

  • How RANK and ROW_NUMBER differ and when to use each for realistic ranking scenarios
  • Techniques for handling ties in rankings using DENSE_RANK vs RANK
  • LAG and LEAD patterns for period-over-period analysis and trend detection
  • Advanced partitioning strategies to segment your analysis by business dimensions
  • Performance optimization techniques for window functions in large datasets

Prerequisites

You should be comfortable with basic SQL queries including SELECT, WHERE, GROUP BY, and ORDER BY. Familiarity with JOINs and subqueries will help, though we'll show you how window functions often eliminate the need for complex subquery patterns.

We'll use PostgreSQL syntax throughout, but these concepts apply to all modern databases (SQL Server, MySQL 8.0+, Oracle, etc.) with minor syntax variations.

Understanding Window Functions vs Traditional Approaches

Before diving into specific functions, let's establish what makes window functions special. Consider this common scenario: you need the top 2 sales representatives by revenue in each region from a sales table.

Here's the traditional approach with subqueries:

-- Traditional approach: complex and hard to read
SELECT s.rep_name, s.region, s.total_revenue
FROM (
    SELECT rep_name, region, total_revenue,
           (SELECT COUNT(*) 
            FROM sales s2 
            WHERE s2.region = s1.region 
            AND s2.total_revenue > s1.total_revenue) + 1 as rank_num
    FROM sales s1
) s
WHERE s.rank_num <= 2
ORDER BY s.region, s.rank_num;

Now with window functions:

-- Window function approach: clean and intuitive
SELECT rep_name, region, total_revenue,
       RANK() OVER (PARTITION BY region ORDER BY total_revenue DESC) as revenue_rank
FROM sales
WHERE revenue_rank <= 2
ORDER BY region, revenue_rank;

Wait—that second query won't work. Here's why window functions are tricky: you can't use the window function result in the WHERE clause directly. The window function is evaluated after the WHERE clause. Here's the correct approach:

-- Correct window function approach
WITH ranked_sales AS (
    SELECT rep_name, region, total_revenue,
           RANK() OVER (PARTITION BY region ORDER BY total_revenue DESC) as revenue_rank
    FROM sales
)
SELECT rep_name, region, total_revenue, revenue_rank
FROM ranked_sales
WHERE revenue_rank <= 2
ORDER BY region, revenue_rank;

This pattern—using a CTE (Common Table Expression) to apply the window function, then filtering the results—is fundamental to working with window functions effectively.

RANK vs ROW_NUMBER: Handling Ties Like a Pro

Let's work with a realistic dataset: monthly product sales data where ties frequently occur.

-- Sample data setup
CREATE TABLE monthly_sales AS
SELECT * FROM (VALUES
    ('2023-01', 'Laptop Pro', 'Electronics', 15000),
    ('2023-01', 'Office Chair', 'Furniture', 8000),
    ('2023-01', 'Desk Lamp', 'Furniture', 3000),
    ('2023-01', 'Smartphone', 'Electronics', 15000), -- tied with Laptop
    ('2023-02', 'Laptop Pro', 'Electronics', 18000),
    ('2023-02', 'Office Chair', 'Furniture', 8000), -- same as previous month
    ('2023-02', 'Desk Lamp', 'Furniture', 3500),
    ('2023-02', 'Smartphone', 'Electronics', 14000)
) AS t(month, product_name, category, revenue);

Now let's see how RANK and ROW_NUMBER handle ties differently:

SELECT month, product_name, category, revenue,
       RANK() OVER (PARTITION BY month ORDER BY revenue DESC) as rank_with_ties,
       ROW_NUMBER() OVER (PARTITION BY month ORDER BY revenue DESC) as row_number,
       DENSE_RANK() OVER (PARTITION BY month ORDER BY revenue DESC) as dense_rank
FROM monthly_sales
ORDER BY month, revenue DESC;

Results:

month   | product_name | category    | revenue | rank_with_ties | row_number | dense_rank
--------|-------------|-------------|---------|----------------|------------|------------
2023-01 | Laptop Pro  | Electronics |   15000 |              1 |          1 |          1
2023-01 | Smartphone  | Electronics |   15000 |              1 |          2 |          1
2023-01 | Office Chair| Furniture   |    8000 |              3 |          3 |          2
2023-01 | Desk Lamp   | Furniture   |    3000 |              4 |          4 |          3
2023-02 | Laptop Pro  | Electronics |   18000 |              1 |          1 |          1
2023-02 | Smartphone  | Electronics |   14000 |              2 |          2 |          2
2023-02 | Office Chair| Furniture   |    8000 |              3 |          3 |          3
2023-02 | Desk Lamp   | Furniture   |    3500 |              4 |          4 |          4

Notice the key differences:

  • RANK(): Both tied products get rank 1, then the next rank is 3 (skipping 2)
  • ROW_NUMBER(): Assigns unique sequential numbers even for ties (the order for ties depends on the database's internal ordering)
  • DENSE_RANK(): Both tied products get rank 1, but the next rank is 2 (no gaps)

When to Use Each Function

Use RANK() when you need traditional competitive ranking (like Olympic medals):

-- Find products that rank in top 3, accounting for ties properly
WITH product_rankings AS (
    SELECT month, product_name, revenue,
           RANK() OVER (PARTITION BY month ORDER BY revenue DESC) as revenue_rank
    FROM monthly_sales
)
SELECT *
FROM product_rankings
WHERE revenue_rank <= 3;
-- If two products tie for #1, both get rank 1, and the next is rank 3

Use ROW_NUMBER() when you need exactly N items regardless of ties:

-- Select exactly 2 products per month, breaking ties arbitrarily
WITH product_selection AS (
    SELECT month, product_name, revenue,
           ROW_NUMBER() OVER (PARTITION BY month ORDER BY revenue DESC, product_name) as rn
    FROM monthly_sales
)
SELECT *
FROM product_selection
WHERE rn <= 2;
-- Always returns exactly 2 products per month

Use DENSE_RANK() when gaps in ranking don't make sense:

-- Create performance tiers without gaps
SELECT month, product_name, revenue,
       CASE 
           WHEN DENSE_RANK() OVER (PARTITION BY month ORDER BY revenue DESC) = 1 THEN 'Tier 1'
           WHEN DENSE_RANK() OVER (PARTITION BY month ORDER BY revenue DESC) = 2 THEN 'Tier 2'
           ELSE 'Tier 3'
       END as performance_tier
FROM monthly_sales;

Pro Tip: When using ROW_NUMBER() to break ties, always include additional columns in your ORDER BY clause to make the tie-breaking deterministic. Otherwise, you might get different results between query runs.

LAG and LEAD: Time Series Analysis Made Simple

LAG and LEAD functions access data from other rows relative to the current row. They're perfect for period-over-period comparisons, trend analysis, and identifying changes over time.

Let's expand our dataset with more temporal data:

CREATE TABLE quarterly_revenue AS
SELECT * FROM (VALUES
    ('Q1-2022', 'Laptop Pro', 120000),
    ('Q2-2022', 'Laptop Pro', 135000),
    ('Q3-2022', 'Laptop Pro', 128000),
    ('Q4-2022', 'Laptop Pro', 155000),
    ('Q1-2023', 'Laptop Pro', 148000),
    ('Q2-2023', 'Laptop Pro', 162000),
    ('Q1-2022', 'Smartphone', 200000),
    ('Q2-2022', 'Smartphone', 185000),
    ('Q3-2022', 'Smartphone', 195000),
    ('Q4-2022', 'Smartphone', 220000),
    ('Q1-2023', 'Smartphone', 210000),
    ('Q2-2023', 'Smartphone', 235000)
) AS t(quarter, product_name, revenue);

Basic LAG Usage for Period-over-Period Analysis

SELECT quarter, product_name, revenue,
       LAG(revenue) OVER (PARTITION BY product_name ORDER BY quarter) as previous_quarter_revenue,
       revenue - LAG(revenue) OVER (PARTITION BY product_name ORDER BY quarter) as quarter_over_quarter_change,
       ROUND(
           (revenue - LAG(revenue) OVER (PARTITION BY product_name ORDER BY quarter)) * 100.0 / 
           LAG(revenue) OVER (PARTITION BY product_name ORDER BY quarter), 2
       ) as pct_change
FROM quarterly_revenue
ORDER BY product_name, quarter;

Results:

quarter  | product_name | revenue | previous_quarter_revenue | quarter_over_quarter_change | pct_change
---------|--------------|---------|--------------------------|----------------------------|------------
Q1-2022  | Laptop Pro   |  120000 |                     NULL |                       NULL |       NULL
Q2-2022  | Laptop Pro   |  135000 |                   120000 |                      15000 |      12.50
Q3-2022  | Laptop Pro   |  128000 |                   135000 |                      -7000 |      -5.19
Q4-2022  | Laptop Pro   |  155000 |                   128000 |                      27000 |      21.09
Q1-2023  | Laptop Pro   |  148000 |                   155000 |                      -7000 |      -4.52
Q2-2023  | Laptop Pro   |  162000 |                   148000 |                      14000 |       9.46

Notice how LAG returns NULL for the first row in each partition—there's no previous value to reference.

Advanced LAG Patterns: Multiple Periods and Default Values

You can look back multiple periods and provide default values:

-- Compare current quarter to same quarter last year
SELECT quarter, product_name, revenue,
       LAG(revenue, 4, 0) OVER (PARTITION BY product_name ORDER BY quarter) as same_quarter_last_year,
       revenue - LAG(revenue, 4, 0) OVER (PARTITION BY product_name ORDER BY quarter) as year_over_year_change
FROM quarterly_revenue
ORDER BY product_name, quarter;

The second parameter (4) means "look back 4 rows," and the third parameter (0) is the default value when no previous row exists.

LEAD for Forward-Looking Analysis

LEAD works the same way but looks forward:

-- Identify quarters where revenue will decline next quarter
SELECT quarter, product_name, revenue,
       LEAD(revenue) OVER (PARTITION BY product_name ORDER BY quarter) as next_quarter_revenue,
       CASE 
           WHEN LEAD(revenue) OVER (PARTITION BY product_name ORDER BY quarter) < revenue 
           THEN 'Decline Expected'
           WHEN LEAD(revenue) OVER (PARTITION BY product_name ORDER BY quarter) > revenue 
           THEN 'Growth Expected'
           ELSE 'Unknown'
       END as trend_prediction
FROM quarterly_revenue
ORDER BY product_name, quarter;

Combining LAG with Conditional Logic for Trend Analysis

Here's a more sophisticated example that identifies trend patterns:

WITH trend_analysis AS (
    SELECT quarter, product_name, revenue,
           LAG(revenue, 1) OVER (PARTITION BY product_name ORDER BY quarter) as prev_1,
           LAG(revenue, 2) OVER (PARTITION BY product_name ORDER BY quarter) as prev_2,
           LAG(revenue, 3) OVER (PARTITION BY product_name ORDER BY quarter) as prev_3
    FROM quarterly_revenue
)
SELECT quarter, product_name, revenue,
       CASE 
           WHEN revenue > prev_1 AND prev_1 > prev_2 AND prev_2 > prev_3 THEN 'Accelerating Growth'
           WHEN revenue > prev_1 AND prev_1 > prev_2 THEN 'Growing'
           WHEN revenue < prev_1 AND prev_1 < prev_2 THEN 'Declining'
           WHEN revenue < prev_1 AND prev_1 < prev_2 AND prev_2 < prev_3 THEN 'Steep Decline'
           ELSE 'Volatile'
       END as trend_pattern
FROM trend_analysis
WHERE prev_3 IS NOT NULL  -- Only analyze quarters with sufficient history
ORDER BY product_name, quarter;

Advanced Partitioning Strategies

So far, we've used simple partitioning (by product, by month). But real-world analysis often requires more sophisticated partitioning strategies.

Multi-Level Partitioning

-- Sample data: sales by rep, region, and month
CREATE TABLE sales_performance AS
SELECT * FROM (VALUES
    ('2023-01', 'North', 'Alice Johnson', 45000),
    ('2023-01', 'North', 'Bob Smith', 38000),
    ('2023-01', 'South', 'Carol Davis', 52000),
    ('2023-01', 'South', 'Dave Wilson', 41000),
    ('2023-02', 'North', 'Alice Johnson', 48000),
    ('2023-02', 'North', 'Bob Smith', 42000),
    ('2023-02', 'South', 'Carol Davis', 49000),
    ('2023-02', 'South', 'Dave Wilson', 44000),
    ('2023-03', 'North', 'Alice Johnson', 51000),
    ('2023-03', 'North', 'Bob Smith', 39000),
    ('2023-03', 'South', 'Carol Davis', 55000),
    ('2023-03', 'South', 'Dave Wilson', 47000)
) AS t(month, region, rep_name, sales);

-- Rank reps within their region for each month
SELECT month, region, rep_name, sales,
       RANK() OVER (PARTITION BY month, region ORDER BY sales DESC) as regional_rank,
       RANK() OVER (PARTITION BY month ORDER BY sales DESC) as overall_rank
FROM sales_performance
ORDER BY month, region, regional_rank;

Dynamic Partitioning for Comparative Analysis

Sometimes you need different partitioning strategies in the same query:

-- Compare each rep to regional peers and track individual performance over time
SELECT month, region, rep_name, sales,
       -- Regional ranking each month
       RANK() OVER (PARTITION BY month, region ORDER BY sales DESC) as monthly_regional_rank,
       -- Individual performance tracking
       LAG(sales) OVER (PARTITION BY rep_name ORDER BY month) as prev_month_sales,
       sales - LAG(sales) OVER (PARTITION BY rep_name ORDER BY month) as month_over_month,
       -- Regional context
       AVG(sales) OVER (PARTITION BY month, region) as regional_avg_sales,
       sales - AVG(sales) OVER (PARTITION BY month, region) as vs_regional_avg
FROM sales_performance
ORDER BY month, region, monthly_regional_rank;

Conditional Partitioning

You can even create partitions based on data characteristics:

-- Analyze high performers (>45k) separately from others
SELECT month, rep_name, sales,
       CASE WHEN sales > 45000 THEN 'High Performer' ELSE 'Standard Performer' END as performance_tier,
       RANK() OVER (
           PARTITION BY month, 
                       CASE WHEN sales > 45000 THEN 'High' ELSE 'Standard' END 
           ORDER BY sales DESC
       ) as tier_rank
FROM sales_performance
ORDER BY month, performance_tier, tier_rank;

Performance Optimization for Window Functions

Window functions can be expensive on large datasets. Here's how to optimize them:

Index Strategy

-- For queries partitioning by region and ordering by date
CREATE INDEX idx_sales_region_date ON sales_performance (region, month);

-- For individual time series analysis
CREATE INDEX idx_sales_rep_date ON sales_performance (rep_name, month);

Limiting Window Function Scope

Instead of calculating window functions on entire tables, limit the dataset first:

-- Inefficient: window function on full table
SELECT rep_name, sales, 
       RANK() OVER (ORDER BY sales DESC) as sales_rank
FROM large_sales_table;  -- millions of rows

-- Efficient: filter first, then apply window function
WITH recent_sales AS (
    SELECT rep_name, sales
    FROM large_sales_table
    WHERE month >= '2023-01'  -- much smaller dataset
)
SELECT rep_name, sales,
       RANK() OVER (ORDER BY sales DESC) as sales_rank
FROM recent_sales;

Reusing Window Specifications

When using the same window specification multiple times, define it once:

-- Instead of repeating the window specification
SELECT month, rep_name, sales,
       RANK() OVER (PARTITION BY month ORDER BY sales DESC),
       DENSE_RANK() OVER (PARTITION BY month ORDER BY sales DESC),
       ROW_NUMBER() OVER (PARTITION BY month ORDER BY sales DESC)
FROM sales_performance;

-- Define the window once
SELECT month, rep_name, sales,
       RANK() OVER monthly_sales_window,
       DENSE_RANK() OVER monthly_sales_window,
       ROW_NUMBER() OVER monthly_sales_window
FROM sales_performance
WINDOW monthly_sales_window AS (PARTITION BY month ORDER BY sales DESC);

Hands-On Exercise: E-commerce Performance Dashboard

Let's build a comprehensive sales performance analysis that combines all the techniques we've covered. You'll create a dashboard-style query that provides multiple insights from a single dataset.

Setup

First, create this sample e-commerce dataset:

CREATE TABLE ecommerce_sales AS
SELECT * FROM (VALUES
    ('2023-01', 'Electronics', 'Laptop Pro', 'Alice', 25000),
    ('2023-01', 'Electronics', 'Smartphone X', 'Bob', 18000),
    ('2023-01', 'Electronics', 'Tablet Plus', 'Carol', 12000),
    ('2023-01', 'Books', 'Data Science Guide', 'Alice', 8000),
    ('2023-01', 'Books', 'SQL Mastery', 'Dave', 6000),
    ('2023-01', 'Home', 'Smart Speaker', 'Eve', 15000),
    ('2023-02', 'Electronics', 'Laptop Pro', 'Alice', 28000),
    ('2023-02', 'Electronics', 'Smartphone X', 'Bob', 19500),
    ('2023-02', 'Electronics', 'Tablet Plus', 'Carol', 11000),
    ('2023-02', 'Books', 'Data Science Guide', 'Alice', 8500),
    ('2023-02', 'Books', 'SQL Mastery', 'Dave', 7200),
    ('2023-02', 'Home', 'Smart Speaker', 'Eve', 16500),
    ('2023-03', 'Electronics', 'Laptop Pro', 'Alice', 32000),
    ('2023-03', 'Electronics', 'Smartphone X', 'Bob', 21000),
    ('2023-03', 'Electronics', 'Tablet Plus', 'Carol', 10500),
    ('2023-03', 'Books', 'Data Science Guide', 'Alice', 9200),
    ('2023-03', 'Books', 'SQL Mastery', 'Dave', 7800),
    ('2023-03', 'Home', 'Smart Speaker', 'Eve', 14000)
) AS t(month, category, product, sales_rep, revenue);

Your Task

Create a comprehensive analysis query that shows:

  1. Each product's monthly ranking within its category
  2. Month-over-month growth for each product
  3. Each sales rep's overall ranking for the month
  4. Whether each product is the top performer in its category for that month
  5. The performance trend (improving, declining, stable) for each product

Requirements

  • Use appropriate window functions for each calculation
  • Handle NULL values properly in your LAG calculations
  • Include meaningful column names
  • Order results logically for a dashboard presentation

Starter Code Structure

WITH base_analysis AS (
    -- Your window function calculations here
    SELECT month, category, product, sales_rep, revenue,
           -- Add your window functions
    FROM ecommerce_sales
),
trend_analysis AS (
    -- Add trend calculations here
    SELECT *,
           -- Calculate trends based on previous calculations
    FROM base_analysis
)
SELECT -- Final select with clean column names and logical ordering
FROM trend_analysis
ORDER BY -- Choose appropriate ordering

Solution

Here's a complete solution that demonstrates all the concepts we've covered:

WITH base_analysis AS (
    SELECT month, category, product, sales_rep, revenue,
           -- Product rankings within category each month
           RANK() OVER (PARTITION BY month, category ORDER BY revenue DESC) as category_rank,
           -- Sales rep overall ranking each month
           DENSE_RANK() OVER (PARTITION BY month ORDER BY revenue DESC) as rep_overall_rank,
           -- Previous month revenue for growth calculations
           LAG(revenue) OVER (PARTITION BY product ORDER BY month) as prev_month_revenue,
           -- Is this the #1 product in its category this month?
           CASE 
               WHEN RANK() OVER (PARTITION BY month, category ORDER BY revenue DESC) = 1 
               THEN 'Category Leader' 
               ELSE 'Follower' 
           END as category_position
    FROM ecommerce_sales
),
trend_analysis AS (
    SELECT *,
           -- Month-over-month growth
           COALESCE(revenue - prev_month_revenue, 0) as mom_growth,
           CASE 
               WHEN prev_month_revenue IS NULL THEN 'New Product'
               WHEN revenue > prev_month_revenue THEN 'Growing'
               WHEN revenue < prev_month_revenue THEN 'Declining'
               ELSE 'Stable'
           END as trend_direction,
           -- Growth percentage (handle division by zero)
           CASE 
               WHEN prev_month_revenue IS NULL OR prev_month_revenue = 0 THEN NULL
               ELSE ROUND((revenue - prev_month_revenue) * 100.0 / prev_month_revenue, 1)
           END as mom_growth_pct
    FROM base_analysis
)
SELECT month,
       category,
       product,
       sales_rep,
       revenue,
       category_rank,
       rep_overall_rank,
       category_position,
       mom_growth,
       mom_growth_pct,
       trend_direction
FROM trend_analysis
ORDER BY month, category, category_rank;

This solution demonstrates:

  • Multiple partitioning strategies (by month+category, by month, by product)
  • Proper handling of NULL values with COALESCE and CASE statements
  • Combining window functions with conditional logic
  • Creating business-friendly labels and insights

Common Mistakes & Troubleshooting

Mistake 1: Using Window Functions in WHERE Clauses

-- This will NOT work
SELECT product, revenue,
       RANK() OVER (ORDER BY revenue DESC) as rank
FROM products
WHERE RANK() OVER (ORDER BY revenue DESC) <= 3;  -- ERROR!

Why it fails: Window functions are evaluated after WHERE clauses. SQL processes clauses in this order: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT.

The fix: Use a CTE or subquery:

WITH ranked_products AS (
    SELECT product, revenue,
           RANK() OVER (ORDER BY revenue DESC) as rank
    FROM products
)
SELECT * FROM ranked_products WHERE rank <= 3;

Mistake 2: Forgetting to Handle NULLs in LAG Calculations

-- Problematic: division by zero when LAG returns NULL
SELECT month, revenue,
       (revenue - LAG(revenue) OVER (ORDER BY month)) / LAG(revenue) OVER (ORDER BY month) * 100 as growth_pct
FROM monthly_data;

The fix: Always handle NULL cases:

SELECT month, revenue,
       CASE 
           WHEN LAG(revenue) OVER (ORDER BY month) IS NULL THEN NULL
           WHEN LAG(revenue) OVER (ORDER BY month) = 0 THEN NULL
           ELSE (revenue - LAG(revenue) OVER (ORDER BY month)) * 100.0 / LAG(revenue) OVER (ORDER BY month)
       END as growth_pct
FROM monthly_data;

Mistake 3: Incorrect Partitioning Leading to Wrong Results

-- Wrong: This ranks across ALL months, not within each month
SELECT month, product, revenue,
       RANK() OVER (ORDER BY revenue DESC) as monthly_rank  -- Missing PARTITION BY
FROM monthly_sales;

-- Correct: Partition by month to rank within each month
SELECT month, product, revenue,
       RANK() OVER (PARTITION BY month ORDER BY revenue DESC) as monthly_rank
FROM monthly_sales;

Mistake 4: ROW_NUMBER with Non-Deterministic Ordering

-- Problematic: ties broken randomly, results may vary between runs
SELECT product, revenue,
       ROW_NUMBER() OVER (ORDER BY revenue DESC) as row_num
FROM products;

The fix: Include additional columns to make ordering deterministic:

SELECT product, revenue,
       ROW_NUMBER() OVER (ORDER BY revenue DESC, product) as row_num
FROM products;

Mistake 5: Inefficient Window Function Usage

-- Inefficient: same window specification repeated multiple times
SELECT month, product, revenue,
       RANK() OVER (PARTITION BY month ORDER BY revenue DESC),
       DENSE_RANK() OVER (PARTITION BY month ORDER BY revenue DESC),
       PERCENT_RANK() OVER (PARTITION BY month ORDER BY revenue DESC)
FROM sales;

-- Efficient: define window once
SELECT month, product, revenue,
       RANK() OVER w,
       DENSE_RANK() OVER w,
       PERCENT_RANK() OVER w
FROM sales
WINDOW w AS (PARTITION BY month ORDER BY revenue DESC);

Summary & Next Steps

Window functions transform how you approach analytical SQL. Instead of complex self-joins and subqueries, you now have clean, readable tools for ranking (RANK, ROW_NUMBER, DENSE_RANK), time-series analysis (LAG, LEAD), and comparative analysis through intelligent partitioning.

The key insights you've mastered:

RANK vs ROW_NUMBER: RANK handles ties by giving them the same rank and skipping subsequent ranks, while ROW_NUMBER assigns unique sequential numbers regardless of ties. DENSE_RANK gives tied values the same rank but doesn't skip subsequent ranks.

LAG and LEAD: These functions unlock period-over-period analysis, trend detection, and forward-looking insights. Always handle NULL values properly, especially in calculations, and remember you can look back or forward multiple periods.

Partitioning Strategy: The PARTITION BY clause is where the real power lies. You can segment your analysis by business dimensions (region, category, time periods) to create sophisticated multi-level analytics in a single query.

Performance Considerations: Window functions can be expensive on large datasets. Create appropriate indexes on partition and order columns, filter datasets before applying window functions, and reuse window specifications when possible.

Logical Next Steps

  1. Advanced Window Functions: Explore NTILE for quartile analysis, FIRST_VALUE/LAST_VALUE for range analytics, and aggregate window functions like SUM() OVER for running totals and moving averages.

  2. Window Frames: Learn about ROWS and RANGE frame specifications to control exactly which rows your window function considers—essential for moving averages, cumulative sums, and sliding window analysis.

  3. Complex Analytical Patterns: Combine window functions with CTEs and conditional logic to build sophisticated analytical frameworks for cohort analysis, funnel tracking, and advanced business intelligence scenarios.

Learning Path: Advanced SQL Queries

Next

Common Table Expressions (CTEs) for Cleaner SQL

Related Articles

SQL🔥 Expert

Advanced SQL Filtering and Sorting: WHERE, AND, OR, IN, BETWEEN, ORDER BY Mastery

17 min
SQL⚡ Practitioner

Advanced Data Filtering and Sorting: Mastering WHERE Clauses and ORDER BY in SQL

13 min
SQL🌱 Foundation

SQL Filtering and Sorting: Master WHERE, AND, OR, IN, BETWEEN, and ORDER BY

13 min

On this page

  • Prerequisites
  • Understanding Window Functions vs Traditional Approaches
  • RANK vs ROW_NUMBER: Handling Ties Like a Pro
  • When to Use Each Function
  • LAG and LEAD: Time Series Analysis Made Simple
  • Basic LAG Usage for Period-over-Period Analysis
  • Advanced LAG Patterns: Multiple Periods and Default Values
  • LEAD for Forward-Looking Analysis
  • Combining LAG with Conditional Logic for Trend Analysis
  • Conditional Partitioning
  • Performance Optimization for Window Functions
  • Index Strategy
  • Limiting Window Function Scope
  • Reusing Window Specifications
  • Hands-On Exercise: E-commerce Performance Dashboard
  • Setup
  • Your Task
  • Requirements
  • Starter Code Structure
  • Solution
  • Common Mistakes & Troubleshooting
  • Mistake 1: Using Window Functions in WHERE Clauses
  • Mistake 2: Forgetting to Handle NULLs in LAG Calculations
  • Mistake 3: Incorrect Partitioning Leading to Wrong Results
  • Mistake 4: ROW_NUMBER with Non-Deterministic Ordering
  • Mistake 5: Inefficient Window Function Usage
  • Summary & Next Steps
  • Logical Next Steps
  • Advanced Partitioning Strategies
  • Multi-Level Partitioning
  • Dynamic Partitioning for Comparative Analysis
  • Conditional Partitioning
  • Performance Optimization for Window Functions
  • Index Strategy
  • Limiting Window Function Scope
  • Reusing Window Specifications
  • Hands-On Exercise: E-commerce Performance Dashboard
  • Setup
  • Your Task
  • Requirements
  • Starter Code Structure
  • Solution
  • Common Mistakes & Troubleshooting
  • Mistake 1: Using Window Functions in WHERE Clauses
  • Mistake 2: Forgetting to Handle NULLs in LAG Calculations
  • Mistake 3: Incorrect Partitioning Leading to Wrong Results
  • Mistake 4: ROW_NUMBER with Non-Deterministic Ordering
  • Mistake 5: Inefficient Window Function Usage
  • Summary & Next Steps
  • Logical Next Steps