Understanding the WINDOW Clause in MySQL

In MySQL, the WINDOW clause is an optional clause that we can use to create a named window. The named window can then be referred to from a window function.

Many SQL developers define their window functions directly in the OVER clause. But that’s not the only way to do it. We can also define them in a WINDOW clause, and then refer to them in the OVER clause.

When we define the window function in a WINDOW clause, we name it. When we do this, we can refer to that name from the OVER clause. This eliminates the need to include the definition directly inside the OVER clause.

Named windows can be useful if we need to use multiple OVER clauses in our query that have the same definition. This saves us from having to duplicate the code used to create the window function specification.

Syntax

The syntax for creating named windows goes like this:

WINDOW window_name AS (window_spec)
    [, window_name AS (window_spec)] ...

Where window_spec goes like this:

window_spec:
    [window_name] [partition_clause] [order_clause] [frame_clause]

So window_spec is the same code that we’d use if we were to define the window function directly inside the OVER clause.

We can see that the syntax allows for creating multiple named windows within a single query. To do this, we simply separate them with a comma.

The WINDOW clause is placed between the positions of the HAVING and ORDER BY clauses in the query.

Example

Suppose we have the following query:

SELECT 
    District, 
    Name, 
    Population,
    SUM(Population) OVER(PARTITION BY District)
FROM City 
WHERE CountryCode = 'AUS' 
ORDER BY District, Name, "City Population";

Here, the last column uses a window function to calculate its values. We’ve used the OVER clause with the PARTITION BY sub clause to define the window function. So it’s defined as SUM(Population) OVER(PARTITION BY District).

Let’s change the above query to use a named window:

SELECT 
    District, 
    Name, 
    Population,
    SUM(Population) OVER(wf)
FROM City 
WHERE CountryCode = 'AUS' 
WINDOW wf AS (PARTITION BY District)
ORDER BY District, Name, "City Population";

This time we’ve added a WINDOW clause, and we’ve moved the PARTITION BY District part down to that clause. We’ve replaced it with the name that we’ve given the window function (in this case we’ve named the window wf).

Code Reuse

Named windows can be useful for reusing code so that our queries become more concise. In particular, if our query contains multiple columns with the same window function definition, we can define it once within a named window, then refer to that named window multiple times in the query.

Consider the following query:

SELECT
  ProductPrice,
  ROW_NUMBER() OVER (ORDER BY ProductPrice) AS "Row Number",
  RANK()       OVER (ORDER BY ProductPrice) AS "Rank",
  DENSE_RANK() OVER (ORDER BY ProductPrice) AS "Dense Rank"
FROM Products;

Result:

+--------------+------------+------+------------+
| ProductPrice | Row Number | Rank | Dense Rank |
+--------------+------------+------+------------+
|         9.99 |          1 |    1 |          1 |
|        11.99 |          2 |    2 |          2 |
|        14.75 |          3 |    3 |          3 |
|        25.99 |          4 |    4 |          4 |
|        25.99 |          5 |    4 |          4 |
|        33.49 |          6 |    6 |          5 |
|        55.99 |          7 |    7 |          6 |
|       245.00 |          8 |    8 |          7 |
+--------------+------------+------+------------+
8 rows in set (0.00 sec)

This query has the same window function repeated across three columns. This is a perfect opportunity to use the WINDOW clause.

Here’s how we can use the WINDOW clause to obtain the same result:

SELECT
  ProductPrice,
  ROW_NUMBER() OVER wf AS "Row Number",
  RANK()       OVER wf AS "Rank",
  DENSE_RANK() OVER wf AS "Dense Rank"n
FROM Products
WINDOW wf AS (ORDER BY ProductPrice);

Result:

+--------------+------------+------+------------+
| ProductPrice | Row Number | Rank | Dense Rank |
+--------------+------------+------+------------+
|         9.99 |          1 |    1 |          1 |
|        11.99 |          2 |    2 |          2 |
|        14.75 |          3 |    3 |          3 |
|        25.99 |          4 |    4 |          4 |
|        25.99 |          5 |    4 |          4 |
|        33.49 |          6 |    6 |          5 |
|        55.99 |          7 |    7 |          6 |
|       245.00 |          8 |    8 |          7 |
+--------------+------------+------+------------+
8 rows in set (0.00 sec)

So we can see that we got the same result, but with less code (and without having to repeat the same window function specification three times).

Note that in this example, I didn’t enclose the window’s name in parentheses. For example, I used OVER wf instead of OVER(wf). I could have used parentheses – the parentheses are optional in this case. However, the parentheses can be useful if we want to provide extra clauses in order to modify the named window.

Modifying a Named Window

We can modify a named window by providing extra clauses. To do this, we need to enclose the window’s name in parentheses, then append the other clause/s within the parentheses.

Example:

SELECT 
    District, 
    Name, 
    Population,
    SUM(Population) OVER(wf) AS "Default",
    SUM(Population) OVER(wf ORDER BY Name ASC) AS "Ascending",
    SUM(Population) OVER(wf ORDER BY Name DESC) AS "Descending"
FROM City 
WHERE CountryCode = 'AUS' 
WINDOW wf AS (PARTITION BY District)
ORDER BY District, Name, "City Population";

Result:

+-----------------+---------------+------------+---------+-----------+------------+
| District        | Name          | Population | Default | Ascending | Descending |
+-----------------+---------------+------------+---------+-----------+------------+
| Capital Region  | Canberra      |     322723 |  322723 |    322723 |     322723 |
| New South Wales | Central Coast |     227657 | 3993949 |    227657 |    3993949 |
| New South Wales | Newcastle     |     270324 | 3993949 |    497981 |    3766292 |
| New South Wales | Sydney        |    3276207 | 3993949 |   3774188 |    3495968 |
| New South Wales | Wollongong    |     219761 | 3993949 |   3993949 |     219761 |
| Queensland      | Brisbane      |    1291117 | 1805236 |   1291117 |    1805236 |
| Queensland      | Cairns        |      92273 | 1805236 |   1383390 |     514119 |
| Queensland      | Gold Coast    |     311932 | 1805236 |   1695322 |     421846 |
| Queensland      | Townsville    |     109914 | 1805236 |   1805236 |     109914 |
| South Australia | Adelaide      |     978100 |  978100 |    978100 |     978100 |
| Tasmania        | Hobart        |     126118 |  126118 |    126118 |     126118 |
| Victoria        | Geelong       |     125382 | 2990711 |    125382 |    2990711 |
| Victoria        | Melbourne     |    2865329 | 2990711 |   2990711 |    2865329 |
| West Australia  | Perth         |    1096829 | 1096829 |   1096829 |    1096829 |
+-----------------+---------------+------------+---------+-----------+------------+
14 rows in set (0.00 sec)

Note that we can only add properties to a named window. We can’t modify any existing properties. In the above example, we added the ORDER BY clause. Given the ORDER BY clause wasn’t included in the named window’s definition, we were able to run it without error.

Here’s what happens if we try to modify a property that already exists in the named window:

SELECT 
    District, 
    Name, 
    Population,
    SUM(Population) OVER(wf ORDER BY Name)
FROM City 
WHERE CountryCode = 'AUS' 
WINDOW wf AS (ORDER BY District);

Result:

ERROR 3583 (HY000): Window '<unnamed window>' cannot inherit 'wf' since both contain an ORDER BY clause.

In this case I tried to modify the named window with an ORDER BY clause. The problem is that the named window already includes an ORDER BY clause in its definition.

If we try to double up on the PARTITION BY clause, we get a different error:

SELECT 
    District, 
    Name, 
    Population,
    SUM(Population) OVER(wf PARTITION BY Name)
FROM City 
WHERE CountryCode = 'AUS' 
WINDOW wf AS (PARTITION BY District);

Result:

ERROR 3581 (HY000): A window which depends on another cannot define partitioning.

In this case the named window is defined as PARTITION BY District. When I called that named window, I tried to modify it with another PARTITION BY clause. The error tells me that I can’t do that.

Referring to Another Named Window

Named windows can refer to other named windows.

Example:

SELECT 
    District, 
    Name, 
    Population,
    SUM(Population) OVER(wf3)
FROM City 
WHERE CountryCode = 'AUS' 
WINDOW wf1 AS (PARTITION BY District), wf2 AS (wf1), wf3 AS (wf2)
ORDER BY District, Name, "City Population";

Result:

+-----------------+---------------+------------+---------------------------+
| District        | Name          | Population | SUM(Population) OVER(wf3) |
+-----------------+---------------+------------+---------------------------+
| Capital Region  | Canberra      |     322723 |                    322723 |
| New South Wales | Central Coast |     227657 |                   3993949 |
| New South Wales | Newcastle     |     270324 |                   3993949 |
| New South Wales | Sydney        |    3276207 |                   3993949 |
| New South Wales | Wollongong    |     219761 |                   3993949 |
| Queensland      | Brisbane      |    1291117 |                   1805236 |
| Queensland      | Cairns        |      92273 |                   1805236 |
| Queensland      | Gold Coast    |     311932 |                   1805236 |
| Queensland      | Townsville    |     109914 |                   1805236 |
| South Australia | Adelaide      |     978100 |                    978100 |
| Tasmania        | Hobart        |     126118 |                    126118 |
| Victoria        | Geelong       |     125382 |                   2990711 |
| Victoria        | Melbourne     |    2865329 |                   2990711 |
| West Australia  | Perth         |    1096829 |                   1096829 |
+-----------------+---------------+------------+---------------------------+
14 rows in set (0.01 sec)

As seen here, we can create a chain of named windows. These can contain forward or backward references, but not cycles.

Here’s another, valid, example:

SELECT 
    District, 
    Name, 
    Population,
    SUM(Population) OVER(wf3)
FROM City 
WHERE CountryCode = 'AUS' 
WINDOW wf1 AS (wf2), wf2 AS (), wf3 AS (wf1)
ORDER BY District, Name, "City Population";

Result:

+-----------------+---------------+------------+---------------------------+
| District        | Name          | Population | SUM(Population) OVER(wf3) |
+-----------------+---------------+------------+---------------------------+
| Capital Region  | Canberra      |     322723 |                  11313666 |
| New South Wales | Central Coast |     227657 |                  11313666 |
| New South Wales | Newcastle     |     270324 |                  11313666 |
| New South Wales | Sydney        |    3276207 |                  11313666 |
| New South Wales | Wollongong    |     219761 |                  11313666 |
| Queensland      | Brisbane      |    1291117 |                  11313666 |
| Queensland      | Cairns        |      92273 |                  11313666 |
| Queensland      | Gold Coast    |     311932 |                  11313666 |
| Queensland      | Townsville    |     109914 |                  11313666 |
| South Australia | Adelaide      |     978100 |                  11313666 |
| Tasmania        | Hobart        |     126118 |                  11313666 |
| Victoria        | Geelong       |     125382 |                  11313666 |
| Victoria        | Melbourne     |    2865329 |                  11313666 |
| West Australia  | Perth         |    1096829 |                  11313666 |
+-----------------+---------------+------------+---------------------------+
14 rows in set (0.00 sec)

However as mentioned, we can’t create a cycle. Here’s what happens if we try to create a cycle:

SELECT 
    District, 
    Name, 
    Population,
    SUM(Population) OVER(wf3)
FROM City 
WHERE CountryCode = 'AUS' 
WINDOW wf1 AS (PARTITION BY District), wf2 AS (wf3), wf3 AS (wf2)
ORDER BY District, Name, "City Population";

Result:

ERROR 3580 (HY000): There is a circularity in the window dependency graph.

In this case wf2 references wf3, but wf3 references wf2. This creates a circular reference, which is not allowed, and so we get the error.