Overview

Window functions compute values across a set of rows related to the current row, without collapsing those rows like GROUP BY does. They are essential for ranking, running totals, and comparing a row to its neighbors. This tutorial uses a consistent dataset to demonstrate each function.

Sample Data

-- sales
+----+---------+--------+------------+
| id | region  | amount | sale_date  |
+----+---------+--------+------------+
| 1  | EU      | 100    | 2026-01-05 |
| 2  | EU      | 250    | 2026-01-12 |
| 3  | EU      | 180    | 2026-02-03 |
| 4  | US      | 300    | 2026-01-08 |
| 5  | US      | 150    | 2026-01-20 |
| 6  | US      | 400    | 2026-02-11 |
+----+---------+--------+------------+

Window Function Syntax

function_name(...) OVER (
  [PARTITION BY column]
  [ORDER BY column]
  [frame_clause]
)
ClausePurpose
PARTITION BYSplit rows into groups; the function restarts for each group
ORDER BYDefine the order within each partition
Frame clauseRestrict which rows the function sees (e.g., ROWS BETWEEN ...)

Ranking Functions

SELECT
  id,
  region,
  amount,
  ROW_NUMBER() OVER (PARTITION BY region ORDER BY amount DESC) AS row_num,
  RANK()       OVER (PARTITION BY region ORDER BY amount DESC) AS rank_val,
  DENSE_RANK() OVER (PARTITION BY region ORDER BY amount DESC) AS dense_rank_val
FROM sales;
FunctionBehavior on ties
ROW_NUMBERAlways unique, ties broken arbitrarily
RANKTies share a rank; next rank skips (1, 1, 3)
DENSE_RANKTies share a rank; next rank does not skip (1, 1, 2)

Running Totals

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

The frame clause ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW restricts the sum to all rows up to the current one within the partition.

Comparing to Previous and Next Rows

SELECT
  id,
  sale_date,
  amount,
  LAG(amount)  OVER (ORDER BY sale_date) AS prev_amount,
  LEAD(amount) OVER (ORDER BY sale_date) AS next_amount,
  amount - LAG(amount) OVER (ORDER BY sale_date) AS change
FROM sales
ORDER BY sale_date;

LAG and LEAD accept optional arguments for offset and default:

LAG(amount, 1, 0) OVER (ORDER BY sale_date)

Moving Averages

SELECT
  sale_date,
  amount,
  AVG(amount) OVER (
    ORDER BY sale_date
    ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
  ) AS moving_avg_3
FROM sales;

First and Last Values in a Partition

SELECT
  id,
  region,
  amount,
  FIRST_VALUE(amount) OVER (PARTITION BY region ORDER BY sale_date) AS first_sale,
  LAST_VALUE(amount)  OVER (
    PARTITION BY region
    ORDER BY sale_date
    ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
  ) AS last_sale
FROM sales;

LAST_VALUE needs an explicit frame ending at UNBOUNDED FOLLOWING; otherwise it defaults to the current row.

Percent of Total

SELECT
  region,
  amount,
  ROUND(100.0 * amount / SUM(amount) OVER (), 2) AS pct_of_total
FROM sales;

An empty OVER () computes across all rows, giving the grand total.

Window Functions vs GROUP BY

AspectGROUP BYWindow function
Output rowsOne per groupOne per input row
Can mix with detail columnsNoYes
Typical useSummariesRankings, running totals, comparisons

Filtering on a Window Result

Window functions are evaluated after WHERE, so you cannot filter on them directly. Wrap the query in a subquery or CTE:

WITH ranked AS (
  SELECT
    id, region, amount,
    ROW_NUMBER() OVER (PARTITION BY region ORDER BY amount DESC) AS rn
  FROM sales
)
SELECT * FROM ranked WHERE rn = 1;

Supported Databases

Window functions are supported in PostgreSQL, MySQL 8+, SQL Server, Oracle, SQLite 3.25+, and BigQuery. Always check the version of your database before relying on them.