SQL Window Functions: Running Totals, Rankings and Month-on-Month in One Query

📎 This article includes 3 downloadable practice files ↓

⏱ 3 min readUpdated 28 September 2026

GROUP BY collapses rows into totals. Window functions calculate across related rows without collapsing them — so each row keeps its detail and gains a running total, a rank or last month’s value. They work in PostgreSQL, SQL Server, Oracle, MySQL 8+ and SQLite 3.25+.

In this article
  1. The shape
  2. 1. Running total
  3. 2. Ranking: ROW_NUMBER, RANK, DENSE_RANK
  4. 3. Top-N per group
  5. 4. Month-on-month with LAG
  6. 5. Moving average
  7. 6. Share of total without a join
  8. 7. De-duplicate: keep the latest record per customer
  9. Excel equivalents, for intuition

The shape

function_name(...) OVER (
    PARTITION BY group_columns     -- restart for each group (optional)
    ORDER BY sort_columns          -- order inside the group
    ROWS BETWEEN ... AND ...       -- which rows to include (optional)
)

Our example table monthly_sales(region, month, amount) has one row per region per month.

1. Running total

SELECT region, month, amount,
       SUM(amount) OVER (PARTITION BY region ORDER BY month) AS running_total
FROM monthly_sales;

Each region’s running total restarts because of PARTITION BY region. Remove it for one company-wide running total.

2. Ranking: ROW_NUMBER, RANK, DENSE_RANK

SELECT region, month, amount,
       ROW_NUMBER() OVER (PARTITION BY month ORDER BY amount DESC) AS rn,
       RANK()       OVER (PARTITION BY month ORDER BY amount DESC) AS rnk,
       DENSE_RANK() OVER (PARTITION BY month ORDER BY amount DESC) AS drnk
FROM monthly_sales;
Amounts ROW_NUMBER RANK DENSE_RANK
500 1 1 1
400 2 2 2
400 3 2 2
300 4 4 3

3. Top-N per group

“Best two months for each region” — rank inside a subquery, then filter:

SELECT * FROM (
    SELECT region, month, amount,
           ROW_NUMBER() OVER (PARTITION BY region ORDER BY amount DESC) AS rn
    FROM monthly_sales
) t
WHERE rn <= 2;

You cannot put a window function directly in WHERE, because windows are calculated after filtering. The subquery (or a CTE) is the standard pattern.

4. Month-on-month with LAG

SELECT region, month, amount,
       LAG(amount) OVER (PARTITION BY region ORDER BY month) AS prev_month,
       amount - LAG(amount) OVER (PARTITION BY region ORDER BY month) AS change,
       ROUND(100.0 * (amount - LAG(amount) OVER (PARTITION BY region ORDER BY month))
             / NULLIF(LAG(amount) OVER (PARTITION BY region ORDER BY month), 0), 1) AS change_pct
FROM monthly_sales;

LEAD looks forward instead. LAG(amount, 12) gives the same month last year when every month is present.

5. Moving average

SELECT region, month, amount,
       AVG(amount) OVER (PARTITION BY region ORDER BY month
                         ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS avg_3m
FROM monthly_sales;

6. Share of total without a join

SELECT region, month, amount,
       ROUND(100.0 * amount / SUM(amount) OVER (PARTITION BY month), 1) AS share_pct
FROM monthly_sales;

7. De-duplicate: keep the latest record per customer

WITH ranked AS (
  SELECT *, ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY updated_at DESC) AS rn
  FROM customers_raw
)
SELECT * FROM ranked WHERE rn = 1;
💡 Window functions are also available in Excel users’ favourite tools: Power Query’s “Group By → All Rows” plus an index column mimics ROW_NUMBER, and DAX has RANKX and WINDOW-style functions.

Excel equivalents, for intuition

SQL Excel idea
Running total =SUMIFS(C$2:C2, A$2:A2, A2)
RANK =COUNTIFS(Month,[@Month],Amount,">"&[@Amount])+1
LAG A lookup of the previous month’s row

Practise these queries in the browser in the SQL Playground.

📎 Practice files for this article

  • 📄
    SQLite practice databasemonthly_sales (48 rows) and orders (400 rows). Opens in DB Browser for SQLite.
    ⬇ DB · 28 KB
  • 🗃️
    All 7 queries (.sql)Every query from the guide plus the classic mistake, ready to run.
    ⬇ SQL · 1 KB
  • 🧾
    monthly_sales.csvThe same data for Excel or any database.
    ⬇ CSV · 1 KB

Free to use for learning. Files with macros (.bas) are plain text — import them with Alt+F11 → File → Import File, and always test on a copy.

✨ Ask AI about this article

Stuck on a step? Ask a question and the AI answers using this article.

Free · AI can be wrong