Aggregate functions are powerful, but they come with a cost: when you ask for a summary, you lose the detail. Window functions remove that trade-off. They let you compute a group-level value — a total, an average, a rank — and place it alongside every individual row, without collapsing anything. This article builds the intuition you need before working with any specific window function.

What Is a Window Function?

A window function performs a calculation across a set of table rows that are somehow related to the current row — and crucially, it does so without merging those rows into a single output row. As the PostgreSQL documentation puts it: "unlike regular aggregate functions, use of a window function does not cause rows to become grouped into a single output row — the rows retain their separate identities." Each row keeps its own identity in the result, but gains access to information computed across its neighbours.

The clearest way to feel the difference is to contrast it with GROUP BY. A grouped aggregate is like asking each relay team for a single number: "What was your team's average finish time?" You get one row per team, and the individual runners disappear into that average. A window function is like standing on the track during the race: you can see your own finishing position and glance sideways to see how every other runner did. Nobody is collapsed into a summary — the detail and the context coexist.

Technically, what marks a function as a window function is the presence of the OVER clause. The same SUM() or AVG() you already use with GROUP BY becomes a window function the moment you attach OVER (...) to it. Without OVER, it is an ordinary aggregate; with OVER, it computes a value for each row while leaving the row count untouched.

The Sample Data

Throughout this article we use a small e-commerce dataset of customers and their orders.

customers

customer_id name city
1 Alice London
2 Bob Paris
3 Carla Berlin
4 Diego Madrid

orders

order_id customer_id total status
101 1 89.50 shipped
102 1 42.00 pending
103 2 150.00 shipped
104 3 27.75 cancelled
105 5 60.00 shipped

GROUP BY vs. Window Function: The Key Difference

Suppose we want to know how much each customer has spent in total. The familiar approach uses GROUP BY:

SELECT customer_id, SUM(total) AS total_spend FROM orders GROUP BY customer_id;

This collapses the five order rows down to one row per customer. The summary is correct — but the individual orders are gone. Now compare the window-function version, which keeps every order row and simply adds the customer's total spend as an extra column:

SELECT order_id, customer_id, total, SUM(total) OVER (PARTITION BY customer_id) AS total_spend FROM orders;

① GROUP BY — collapsed

customer_id total_spend
1 131.50
2 150.00
3 27.75
5 60.00

② Window — every row kept

order_id cust total total_spend
101 1 89.50 131.50
102 1 42.00 131.50
103 2 150.00 150.00
104 3 27.75 27.75
105 5 60.00 60.00

Look closely at the row counts. The GROUP BY query returns four rows — one per customer. The window query returns five rows — exactly as many as the orders table — and each order now carries its customer's total spend. That is the entire idea in a nutshell: same data, enriched versus fewer rows, summarized.

GROUP BY row 1 row 2 row 3 row 4 row 5 sum A sum B 5 rows → fewer rows The detail is collapsed into summary rows. WINDOW FUNCTION row 1 row 2 row 3 row 4 row 5 +val +val +val +val +val 5 rows → 5 rows Every row remains visible and gains a computed value.
Figure 1 — GROUP BY collapses many rows into summary rows; a window function keeps every row and attaches a computed value to each.

The Anatomy of OVER()

Every window function call is built around the OVER clause. As Microsoft's SQL Server documentation states: "the OVER clause determines the partitioning and ordering of a rowset before the associated window function is applied." It has two core pieces you will use constantly:

function_name() OVER ( PARTITION BY /* column(s) */ ORDER BY /* column(s) */ )

PARTITION BY

PARTITION BY divides the rows into independent groups — the "windows" — that share the same value of the partition expression. The function is then computed separately within each group, and the computation restarts for each partition. It is similar in spirit to GROUP BY, but with one essential difference: it does not collapse the rows. If you omit PARTITION BY, the entire result set is treated as a single partition.

ORDER BY (inside OVER)

ORDER BY inside the OVER clause establishes the ordering of rows within each partition. This is essential for operations such as ranking and running calculations. It is important to distinguish this from the ORDER BY at the end of the query: the latter controls the order in which the final result is displayed, while the ORDER BY inside OVER is part of the window definition.

Remember: a function becomes a window function because of OVER. Both PARTITION BY and ORDER BY inside OVER are optional, and you can mix and match them depending on what you need.

A Gentle Worked Example

Let's revisit the partition aggregate from earlier and follow it step by step. We want each order to display the total spend of the customer who placed it. First, conceptually, PARTITION BY customer_id slices the orders into per-customer windows; then SUM(total) is computed within each window and written onto every row.

SELECT order_id, customer_id, total, SUM(total) OVER (PARTITION BY customer_id) AS total_spend FROM orders;

① The partitions

order_id cust total
101 1 89.50
102 1 42.00
103 2 150.00
104 3 27.75
105 5 60.00

② The result + new column

order_id cust total total_spend
101 1 89.50 131.50
102 1 42.00 131.50
103 2 150.00 150.00
104 3 27.75 27.75
105 5 60.00 60.00

Notice that customer 1's two orders both show 131.50 — the sum of 89.50 and 42.00 — repeated on each of their rows. Because there is no ORDER BY here, this is a flat partition total, not a running total. The value is the same for every row in the partition. Adding ORDER BY changes the window definition and, for an aggregate such as SUM(), can produce a cumulative calculation depending on the resulting frame.

A First Look at "The Frame"

There is one more concept worth meeting briefly. A window definition can include a window frame: a specific subset of rows within the partition that is visible to frame-sensitive window calculations for the current row. This is what allows calculations such as running totals and moving averages to work over a changing set of rows.

The important nuance for beginners is that the default frame depends on whether ORDER BY is present. In PostgreSQL, when ORDER BY is supplied, the default frame extends from the start of the partition through the current row and its peers — rows that tie according to the window ordering. When ORDER BY is omitted, the default frame covers the entire partition. That is why our SUM() example above returns the same partition total on every row, whereas adding an ORDER BY can produce running cumulative behaviour.

Frames have their own syntax, including ROWS and RANGE, and they will be covered in depth separately. For now, simply knowing that a frame exists — and that it controls which rows are visible to a frame-sensitive calculation — is enough.

The next diagram makes this concrete. To keep the focus on how ORDER BY reshapes the frame, it drops PARTITION BY entirely and treats the whole table as a single window — so the numbers you see are sums across all five orders, not per-customer totals like the worked example above.

OVER () No ORDER BY entire partition is the default frame frame = entire partition order_id total SUM() 101 89.50 369.25 102 42.00 369.25 103 150.00 369.25 104 27.75 369.25 105 60.00 369.25 Same value on every row The frame covers all 5 rows. OVER (ORDER BY order_id) With ORDER BY ordered frame with a cumulative SUM() frame = start → current row order_id total SUM() 101 89.50 89.50 102 42.00 131.50 103 150.00 281.50 104 27.75 309.25 105 60.00 369.25 The frame grows row by row Each row gets a larger cumulative sum.
Figure 2 — Without ORDER BY, the default frame covers the entire partition. With ORDER BY, the default frame runs from the start of the partition through the current row and its peers. Because order_id is unique here, this produces a running total.

Why Window Functions Matter

Window functions unlock a whole category of analytical queries that are awkward or slow to express otherwise. A few of the things they make easy — each of which deserves its own treatment:

  • Ranking — numbering or ranking rows within a group, such as the top order per customer.
  • Running totals — cumulative sums that grow as you move through ordered rows.
  • Comparing each row to a group average — e.g., flagging orders above their customer's mean.
  • Moving averages — smoothing time series over a sliding window of rows.
  • Top-N per group — selecting the best few rows within each partition.

All of these share the same DNA as our example: a calculation over related rows, returned without throwing the detail away.

Cross-Database Support

Window functions are part of the SQL standard, introduced in SQL:2003 (ISO/IEC 9075, the fifth revision of SQL, published on 1 March 2004). Oracle had shipped an early implementation even before that, in Oracle8i in 1998. Today the core syntax is essentially the same across the major engines:

Database Supported since
PostgreSQL Version 8.4 (2009)
MySQL Version 8.0 (2018)
Microsoft SQL Server SQL Server 2005 (full framing since 2012)
SQLite Version 3.25.0 (September 2018)
Amazon Redshift Core analytic feature

This portability means the intuition you build here transfers directly between systems, with only minor dialect differences around advanced framing options.

Where We Go From Here

You now have the one idea that everything else builds on: a window function computes across related rows while letting each row keep its place in the output. OVER is the switch that turns an aggregate into a window function; PARTITION BY chooses the groups; ORDER BY sets the ordering within the window; and the frame fine-tunes exactly which rows are in view for frame-sensitive calculations. With that foundation in place, the next step is to put individual functions to work — assigning ranks, building running totals, and reaching across rows to compare values — and to see just how much expressive power this single clause delivers.

Main References

  1. PostgreSQL Global Development Group. PostgreSQL Documentation: 3.5. Window Functions (Tutorial).
    https://www.postgresql.org/docs/current/tutorial-window.html
  2. PostgreSQL Global Development Group. PostgreSQL Documentation: Window Function Calls.
    https://www.postgresql.org/docs/current/sql-expressions.html#SYNTAX-WINDOW-FUNCTIONS
  3. Oracle Corporation. MySQL 8.0 Reference Manual: Window Functions.
    https://dev.mysql.com/doc/refman/8.0/en/window-functions.html
  4. Microsoft. SELECT - OVER Clause (Transact-SQL) — SQL Server Documentation.
    https://learn.microsoft.com/en-us/sql/t-sql/queries/select-over-clause-transact-sql
  5. Amazon Web Services. Amazon Redshift Developer Guide: Window functions.
    https://docs.aws.amazon.com/redshift/latest/dg/c_Window_functions.html
← Previous article Next article →
← Back to all articles