Window Functions

A window function performs an aggregate-like operation over a set of query rows, but unlike an aggregate function (see Aggregate), which collapses a group into a single result row, a window function produces one result per query row.
SELECT SUM(profit) AS total_profit FROM sales;
→ a single row: 7535
SELECT country, SUM(profit) AS country_profit
  FROM sales GROUP BY country ORDER BY country;
→ one row per country: Finland 1610, India 1350, USA 4575
SELECT year, country, product, profit,
  SUM(profit) OVER() AS total_profit,
  SUM(profit) OVER(PARTITION BY country) AS country_profit
  FROM sales ORDER BY country, year, product;
→ every original row is preserved, each carrying both the grand total (7535) and its country's subtotal.

CREATE TABLE sales (year INT, country VARCHAR(20), product VARCHAR(20), profit INT);
INSERT INTO sales VALUES
    (2000,'Finland','Computer',1500), (2000,'Finland','Phone',100),
    (2001,'Finland','Phone',10),
    (2000,'India','Calculator',75), (2000,'India','Calculator',75),
    (2000,'India','Computer',1200),
    (2000,'USA','Calculator',75), (2000,'USA','Computer',1500),
    (2001,'USA','Calculator',50), (2001,'USA','Computer',1500),
    (2001,'USA','Computer',1200), (2001,'USA','TV',150), (2001,'USA','TV',100);

SELECT SUM(profit) AS total_profit FROM sales;

SELECT country, SUM(profit) AS country_profit
    FROM sales GROUP BY country ORDER BY country;

SELECT year, country, product, profit,
    SUM(profit) OVER() AS total_profit,
    SUM(profit) OVER(PARTITION BY country) AS country_profit
    FROM sales
    ORDER BY country, year, product, profit;

SELECT SUM(profit) AS total_profit FROM sales:
total_profit
7535
SELECT country, SUM(profit) AS country_profit FROM sales GROUP BY country ORDER BY country:
countrycountry_profit
Finland1610
India1350
USA4575
SELECT year, country, product, profit, SUM(profit) OVER() AS total_profit, SUM(profit) OVER(PARTITION BY country) AS country_profit FROM sales ORDER BY country, year, product, profit:
yearcountryproductprofittotal_profitcountry_profit
2000FinlandComputer150075351610
2000FinlandPhone10075351610
2001FinlandPhone1075351610
2000IndiaCalculator7575351350
2000IndiaCalculator7575351350
2000IndiaComputer120075351350
2000USACalculator7575354575
2000USAComputer150075354575
2001USACalculator5075354575
2001USAComputer120075354575
2001USAComputer150075354575
2001USATV10075354575
2001USATV15075354575
(adapted from https://dev.mysql.com/doc/refman/8.0/en/window-functions-usage.html)

Window-Only Functions

MySQL also supports nonaggregate functions that exist only as window functions, for which the OVER clause is mandatory:
SELECT year, country, product, profit,
  ROW_NUMBER() OVER(PARTITION BY country) AS row_num1,
  ROW_NUMBER() OVER(PARTITION BY country ORDER BY year, product) AS row_num2
FROM sales;

row_num1 numbers rows within each country's partition in whatever order they happen to be produced (undefined without an ORDER BY); row_num2 numbers them deterministically by year then product.


SELECT year, country, product, profit,
    ROW_NUMBER() OVER(PARTITION BY country) AS row_num1,
    ROW_NUMBER() OVER(PARTITION BY country ORDER BY year, product) AS row_num2
    FROM sales;

yearcountryproductprofitrow_num1row_num2
2000FinlandComputer150021
2000FinlandPhone10012
2001FinlandPhone1033
2000IndiaCalculator7521
2000IndiaCalculator7532
2000IndiaComputer120013
2000USACalculator7551
2000USAComputer150042
2001USACalculator5023
2001USAComputer150034
2001USAComputer120075
2001USATV15016
2001USATV10067

Named Windows

A window definition can be named with WINDOW and reused across multiple OVER clauses. The two statements below are equivalent:
SELECT val,
  ROW_NUMBER() OVER (ORDER BY val) AS 'row_number',
  RANK() OVER (ORDER BY val) AS 'rank',
  DENSE_RANK() OVER (ORDER BY val) AS 'dense_rank'
FROM numbers;
SELECT val,
  ROW_NUMBER() OVER w AS 'row_number',
  RANK() OVER w AS 'rank',
  DENSE_RANK() OVER w AS 'dense_rank'
FROM numbers
WINDOW w AS (ORDER BY val);
A named window can also be extended with additional clauses at the point of use:
SELECT DISTINCT year, country,
  FIRST_VALUE(year) OVER (w ORDER BY year ASC) AS first,
  FIRST_VALUE(year) OVER (w ORDER BY year DESC) AS last
FROM sales
WINDOW w AS (PARTITION BY country);

Practical Patterns

Deleting duplicate rows via a ranked CTE:
WITH CTE AS
(SELECT *, RANK() OVER (ORDER BY col1,col2,col3) AS R FROM MyTable)
DELETE FROM CTE
WHERE R IN (SELECT R FROM CTE GROUP BY R HAVING COUNT(*)>1);
Pagination with ROW_NUMBER():
SELECT * FROM (
  SELECT ROW_NUMBER() OVER (ORDER BY OrderDate) AS RowNum, *
  FROM Orders
  WHERE OrderDate >= '1980-01-01'
) AS RowConstrainedResult
WHERE RowNum >= 1 AND RowNum < 20
ORDER BY RowNum;

-- Delete duplicate rows, keeping one copy per (col1,col2,col3) group
WITH CTE AS
(SELECT *, RANK() OVER (ORDER BY col1,col2,col3) AS R FROM MyTable)
DELETE FROM CTE
WHERE R IN (SELECT R FROM CTE GROUP BY R HAVING COUNT(*)>1);

-- Page through Orders 19 rows at a time using ROW_NUMBER()
SELECT * FROM (
    SELECT ROW_NUMBER() OVER (ORDER BY OrderDate) AS RowNum, *
    FROM Orders
    WHERE OrderDate >= '1980-01-01'
) AS RowConstrainedResult
WHERE RowNum >= 1 AND RowNum < 20
ORDER BY RowNum;