Skip to main content
AllDevToolsHub
2026-06-12
Last reviewed: Aug 2026
SQL
Est Read: 10_MIN

SQL Window Functions: Calculating Running Totals and Ranks

SQL Window Functions: Calculating Running Totals and Ranks
Processing_Node: 01

#1SQL window functions: the analytics feature that keeps row detail

What we tested: We formatted and ran SQL queries against SQLite (in-browser) and PostgreSQL 16. Query parsing, formatting output, and execution plans were compared across dialects.

Window functions are useful when you want analytics without losing the original row.

They let you calculate ranks, running totals, and moving averages while keeping each record visible, which makes them much easier to use for reporting and debugging than a pile of subqueries.


#21. Window Functions vs. GROUP BY: The Fundamental Difference

Understanding window functions starts with seeing how they differ from standard SQL aggregate functions (SUM, AVG, COUNT, MAX, MIN):

#3Standard GROUP BY (Collapses Rows)

When you execute a query with GROUP BY, the database groups rows matching the group key together and reduces them to a single output row per group:

sql
-- Standard GROUP BY query
SELECT category, SUM(price) AS total_category_sales
FROM products
GROUP BY category;

Output:

protocol
category    | total_category_sales
------------+---------------------
Electronics | 4500.00
Furniture   | 1200.00

Notice that individual product names, IDs, and prices are lost. You only get the summary row.

#3Window Function OVER() (Preserves Individual Rows)

A window function performs the exact same aggregation, but attaches the calculated result as a new column on every individual row:

sql
-- Window function query
SELECT 
  name, 
  category, 
  price,
  SUM(price) OVER(PARTITION BY category) AS total_category_sales
FROM products;

Output:

protocol
name         | category    | price   | total_category_sales
-------------+-------------+---------+---------------------
Laptop       | Electronics | 1200.00 | 4500.00
Smartphone   | Electronics |  800.00 | 4500.00
Monitor      | Electronics |  500.00 | 4500.00
Desk Chair   | Furniture   |  300.00 | 1200.00
Standing Desk| Furniture   |  900.00 | 1200.00

Every row remains intact. You can compare an individual product's price (1200.00) directly against its category total (4500.00) within the same query without a JOIN or subquery.


#22. Anatomy of a Window Function: The OVER() Clause

The syntax for any SQL window function follows a standard pattern:

sql
FUNCTION() OVER (
  [PARTITION BY partition_column]
  [ORDER BY sort_column [ASC|DESC]]
  [ROWS|RANGE frame_specification]
)

#3Component 1: PARTITION BY (Optional)

PARTITION BY divides the query result set into subsets or "windows" of rows. The window function is evaluated independently within each partition. If omitted, the entire result set is treated as a single window.

#3Component 2: ORDER BY (Optional)

ORDER BY defines the logical ordering of rows within each partition. For functions like running totals (SUM), ranking (ROW_NUMBER), or row navigation (LAG), the ordering is essential to determine the calculation sequence.

#3Component 3: Window Frame (ROWS / RANGE)

The frame specification defines a dynamic subset of rows within the partition to include in the calculation relative to the current row.


#23. Calculating Running Totals and Cumulative Aggregates

Calculating a running total (cumulative sum) is one of the most common analytical tasks.

#3Running Total Query

sql
SELECT 
  sale_date,
  amount,
  SUM(amount) OVER (
    ORDER BY sale_date 
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  ) AS running_total
FROM sales;

Output:

protocol
sale_date  | amount | running_total
-----------+--------+--------------
2025-01-01 | 100.00 | 100.00
2025-01-02 | 150.00 | 250.00
2025-01-03 | 200.00 | 450.00
2025-01-04 |  50.00 | 500.00

When ORDER BY is included inside OVER(), SQL defaults the frame specification to ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. This means for each row, SUM() calculates the sum of all rows from the beginning of the partition up to the current row.

#3Partitioned Running Total (e.g., Per Customer)

To reset the running total for each customer, add PARTITION BY:

sql
SELECT 
  customer_id,
  sale_date,
  amount,
  SUM(amount) OVER (
    PARTITION BY customer_id 
    ORDER BY sale_date
  ) AS customer_running_total
FROM sales;

#24. Ranking Functions: ROW_NUMBER, RANK, and DENSE_RANK

SQL provides three distinct functions for ranking rows within a partition. Understanding how they handle ties is critical:

FunctionSequence for Ties (Tied at 2nd Place)Skips Numbers?Use Case
ROW_NUMBER()1, 2, 3, 4NoUnique row pagination, deduping
RANK()1, 2, 2, 4YesStandard competition ranking
DENSE_RANK()1, 2, 2, 3NoContinuous rank tiers

#3Demonstration Query

sql
SELECT 
  student_name,
  score,
  ROW_NUMBER() OVER (ORDER BY score DESC) AS row_num,
  RANK()       OVER (ORDER BY score DESC) AS rnk,
  DENSE_RANK() OVER (ORDER BY score DESC) AS dense_rnk
FROM exam_results;

Output:

protocol
student_name | score | row_num | rnk | dense_rnk
-------------+-------+---------+-----+----------
Alice        |  95   |    1    |  1  |    1
Bob          |  90   |    2    |  2  |    2
Charlie      |  90   |    3    |  2  |    2
David        |  85   |    4    |  4  |    3  <-- Notice: RANK skipped 3; DENSE_RANK did not
Eve          |  80   |    5    |  5  |    4

#3Real-World Pattern: Top N Per Group using a CTE

Suppose you want to find the top 2 highest-paid employees in each department. Because window functions cannot be placed directly in a WHERE clause, wrap the window query in a Common Table Expression (CTE):

sql
WITH RankedEmployees AS (
  SELECT 
    employee_id,
    name,
    department_id,
    salary,
    DENSE_RANK() OVER (
      PARTITION BY department_id 
      ORDER BY salary DESC
    ) AS salary_rank
  FROM employees
)
SELECT department_id, name, salary, salary_rank
FROM RankedEmployees
WHERE salary_rank <= 2;

#25. Navigating Adjacent Rows: LAG and LEAD

LAG() accesses data from a previous row in the partition, while LEAD() accesses data from a subsequent row, without self-joins.

#3Calculating Month-over-Month Growth Rate

sql
SELECT 
  month,
  revenue,
  LAG(revenue, 1) OVER (ORDER BY month) AS prev_month_revenue,
  revenue - LAG(revenue, 1) OVER (ORDER BY month) AS absolute_growth,
  ROUND(
    (revenue - LAG(revenue, 1) OVER (ORDER BY month)) / 
    LAG(revenue, 1) OVER (ORDER BY month) * 100, 2
  ) AS percentage_growth
FROM monthly_financials;

Output:

protocol
month   | revenue  | prev_month_revenue | absolute_growth | percentage_growth
--------+----------+--------------------+-----------------+------------------
2025-01 | 50000.00 | NULL               | NULL            | NULL
2025-02 | 62000.00 | 50000.00           | 12000.00        | 24.00%
2025-03 | 58000.00 | 62000.00           | -4000.00        | -6.45%

LAG(column, offset, default_value) allows setting a default value instead of NULL when accessing preceding rows outside the window bounds.


#26. Advanced Window Frames: Moving Averages

By explicitly defining ROWS BETWEEN, you can build moving averages (smooth time series trends):

#37-Day Moving Average Query

sql
SELECT 
  record_date,
  daily_active_users,
  AVG(daily_active_users) OVER (
    ORDER BY record_date
    ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
  ) AS seven_day_moving_avg
FROM daily_metrics;

This calculates the average of the current row and the 6 preceding rows (total 7 rows), creating a smooth 7-day rolling trend.


#27. Performance and Indexing Best Practices

While window functions are significantly faster than correlated subqueries, improperly structured queries on large tables (millions of rows) can consume high memory.

#31. Match Indexes to PARTITION BY and ORDER BY

For optimal performance, create composite database indexes that cover the PARTITION BY and ORDER BY columns in exact sequence:

sql
-- Index optimization for: OVER (PARTITION BY department_id ORDER BY salary DESC)
CREATE INDEX idx_emp_dept_salary ON employees(department_id, salary DESC);

When an index matches the window definition, the database engine can evaluate the window function directly from the index scan without allocating memory for an explicit sort step.

#32. Filter Rows BEFORE the Window Step

Window functions execute after WHERE, GROUP BY, and HAVING clauses in SQL execution order. Ensure your WHERE clause filters out unnecessary historical data before the database builds the window frame:

protocol
SQL Logical Execution Order:
1. FROM / JOIN
2. WHERE           <-- Filters data FIRST
3. GROUP BY
4. HAVING
5. WINDOW / OVER() <-- Evaluates window functions
6. SELECT
7. DISTINCT
8. ORDER BY
9. LIMIT / OFFSET

#3Filtering Rows inside Window Frames (EXCLUDE Clause)

Modern SQL (PostgreSQL 12+, SQLite 3.28+) supports the EXCLUDE sub-clause inside window frame specifications:

  • EXCLUDE CURRENT ROW: Excludes the current row from the frame calculation.
  • EXCLUDE GROUP: Excludes the current row and any tied rows in the ordering.
  • EXCLUDE TIES: Retains the current row but excludes any tied peer rows.
  • EXCLUDE NO OTHERS: Default behavior (excludes nothing).
sql
-- Calculate average price of OTHER products in the same category (excluding self)
SELECT 
  name,
  category,
  price,
  AVG(price) OVER (
    PARTITION BY category
    ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
    EXCLUDE CURRENT ROW
  ) AS avg_other_products_price
FROM products;

#3Window Function Execution Order vs. Aggregate Functions

A common source of confusion is how window functions interact with GROUP BY when both appear in the same query.

sql
-- Query with BOTH GROUP BY and Window Functions
SELECT 
  department_id,
  SUM(salary) AS total_dept_salary,
  RANK() OVER (ORDER BY SUM(salary) DESC) AS dept_salary_rank
FROM employees
GROUP BY department_id;

How SQL processes this query:

  1. First, GROUP BY department_id collapses raw employee rows into department summary rows and computes SUM(salary).
  2. Next, the window function RANK() OVER (ORDER BY SUM(salary) DESC) runs on top of the aggregated department summary rows.

This allows you to rank aggregated groups directly without needing an extra subquery!


#28. Named Windows: Simplifying Complex Queries with WINDOW Clause

When a query contains multiple window functions that share the exact same PARTITION BY and ORDER BY specification, repeating the OVER (...) definition leads to verbose, hard-to-maintain SQL.

The SQL standard provides the WINDOW clause to define a named, reusable window specification at the bottom of the query:

sql
-- Verbose query repeating OVER clause:
SELECT 
  employee_id,
  department_id,
  salary,
  AVG(salary)  OVER (PARTITION BY department_id ORDER BY salary DESC) AS dept_avg,
  MAX(salary)  OVER (PARTITION BY department_id ORDER BY salary DESC) AS dept_max,
  MIN(salary)  OVER (PARTITION BY department_id ORDER BY salary DESC) AS dept_min,
  RANK()       OVER (PARTITION BY department_id ORDER BY salary DESC) AS salary_rank
FROM employees;

-- Clean query using the named WINDOW clause:
SELECT 
  employee_id,
  department_id,
  salary,
  AVG(salary)  OVER w AS dept_avg,
  MAX(salary)  OVER w AS dept_max,
  MIN(salary)  OVER w AS dept_min,
  RANK()       OVER w AS salary_rank
FROM employees
WINDOW w AS (PARTITION BY department_id ORDER BY salary DESC);

Using the WINDOW clause improves query readability, reduces syntax errors, and simplifies query maintenance when updating analytical partition rules.


#29. Debugging Window Queries Safely

When reviewing or writing complex analytical SQL queries, unformatted SQL with nested CTEs and multiple OVER() clauses becomes hard to read.

Use the AllDevToolsHub SQL Formatter:

  • Beautify Query Structure: Cleans up nested OVER(), PARTITION BY, and CTE logic.
  • Dialect Conversion: Convert queries between PostgreSQL, MySQL, SQL Server, and Oracle.
  • 100% Local Execution: Debug queries containing schema details without sending database structures to remote servers.

#2Summary

SQL Window Functions represent one of the most powerful capabilities in modern database analytics:

  • SUM() OVER(PARTITION BY ... ORDER BY ...): Running totals and cumulative sums
  • ROW_NUMBER() / DENSE_RANK(): Deduplication and top-N ranking per category
  • LAG() / LEAD(): Period-over-period growth and time series comparison
  • ROWS BETWEEN 6 PRECEDING AND CURRENT ROW: Moving averages and rolling trends

Debug and format your analytical queries at the AllDevToolsHub SQL Suite.


#2Related Tools

#2Related Articles


#2Frequently Asked Questions

Q: Why can't I use a window function in a WHERE clause?

A: In SQL execution order, the WHERE clause is evaluated before window functions are computed. To filter by a window function result (e.g., WHERE rank <= 3), calculate the window function inside a CTE or subquery first, then filter in the outer query.

Q: What is the difference between ROWS and RANGE in window frames?

A: ROWS counts physical rows relative to the current row regardless of duplicate values in the sort column. RANGE evaluates logical value offsets, if multiple rows have duplicate values in the ORDER BY column, RANGE treats them as a single tied unit. For 99% of running total queries, ROWS is preferred for explicit performance.


Written by Rahul Jalavadiya, founder of AllDevToolsHub. All tools run locally in your browser.

#2Sources / Further reading

Quick Summary

Window functions are a powerful SQL feature that allows you to perform calculations across a set of rows related to the current row. Unlike `GROUP BY`, window functions don't collapse your result set, making them perfect for running totals, moving averages, and ranking.

Key Takeaways

Key Takeaways

  • `OVER` defines the 'window' of rows for the calculation.
  • `PARTITION BY` groups rows within the window (like `GROUP BY` but for sub-sections).
  • `ORDER BY` inside the window determines the sequence for running calculations.
  • Common functions include `ROW_NUMBER()`, `RANK()`, `SUM()`, and `AVG()`.
Use Cases

When to use it

  • Calculating a running total of sales by month.
  • Finding the top 3 products in every category.
  • Comparing a current row's value to the previous row (using `LAG`).
  • Calculating a 7-day moving average of website traffic.
Watch out

Common Mistakes

  • Using `RANK()` when you wanted `DENSE_RANK()` (handling ties differently).
  • Forgetting the `ORDER BY` in a running total (resulting in a static sum).
  • Using window functions in the `WHERE` clause (they must go in `SELECT` or `ORDER BY`).
  • Confusing `PARTITION BY` with `GROUP BY`.
FAQ

SQL Window Functions: Calculating Running Totals and Ranks, Frequently Asked

What is the difference between `RANK()` and `ROW_NUMBER()`?

`ROW_NUMBER()` always gives a unique number to every row. `RANK()` gives the same number to rows with identical values (ties), then skips the next numbers in the sequence.

Do all databases support window functions?

Most modern RDBMS (Postgres, MySQL 8+, SQL Server, Oracle, SQLite 3.25+) support them. If you're on an older version of MySQL, you might be out of luck.

Can I use `SUM()` as a window function?

Yes! `SUM(amount) OVER (ORDER BY date)` will give you a running total without collapsing the rows.

RJRahul JalavadiyaFounder & Lead Engineer
Published 2026-06-12Last reviewed 2026-08-23

Tools Mentioned in This Article

Tools, tactics, and toughened-up tips, once a week

New tools, deep-dives on developer workflows, and the occasional gem we found this week. No spam, no tracking. Unsubscribe anytime.

Found an error or have feedback?

We correct errors quickly and document changes in our changelog. Report issues at support@alldevtoolshub.com.

Last reviewed: 2026-08-23
Security Memo
AT

Rahul Jalavadiya

Engineering Protocol V1

Specializing in local-first architecture and Zero-Trust developer workflows. No data leaves the machine.