Advancedadvanced

How to use window functions in SQL

Calculate running totals, rankings and moving averages without collapsing rows.

ROW_NUMBER — unique row rankAll databases
SELECT
  name,
  country,
  age,
  ROW_NUMBER() OVER (ORDER BY age DESC) AS rank
FROM users;

Assigns a unique sequential number to each row based on the ORDER BY. No ties.

RANK and DENSE_RANKAll databases
SELECT
  name,
  score,
  RANK()       OVER (ORDER BY score DESC) AS rank,
  DENSE_RANK() OVER (ORDER BY score DESC) AS dense_rank
FROM leaderboard;
-- RANK:       1, 2, 2, 4  (skips 3)
-- DENSE_RANK: 1, 2, 2, 3  (no skips)

RANK skips numbers after ties. DENSE_RANK does not skip.

When to use: Leaderboards, competition rankings.

PARTITION BY — rank within groupsAll databases
SELECT
  country,
  name,
  age,
  ROW_NUMBER() OVER (
    PARTITION BY country
    ORDER BY age DESC
  ) AS rank_in_country
FROM users;

PARTITION BY restarts the window function for each group. Ranks users within their country.

When to use: Top N per group — the most common window function use case.

Running total with SUM OVERPostgreSQL
SELECT
  created_at::date AS date,
  total,
  SUM(total) OVER (ORDER BY created_at) AS running_total
FROM orders
ORDER BY created_at;

Calculates a cumulative sum that grows with each row.

When to use: Revenue dashboards, cumulative metrics.

LAG and LEAD — access previous/next rowsAll databases
SELECT
  date,
  revenue,
  LAG(revenue)  OVER (ORDER BY date) AS prev_day_revenue,
  LEAD(revenue) OVER (ORDER BY date) AS next_day_revenue,
  revenue - LAG(revenue) OVER (ORDER BY date) AS day_over_day_change
FROM daily_revenue;

LAG accesses the previous row, LEAD accesses the next row. Useful for period-over-period comparisons.

When to use: Day-over-day, month-over-month change calculations.

Common Mistakes

  • Forgetting ORDER BY inside OVER() for running totals — without it, SUM OVER gives the total for all rows
  • PARTITION BY ≠ GROUP BY — window functions do not collapse rows, GROUP BY does
← Back to SQL Reference