Skip to content

Grokem · https://www.grokem.com/data/data-analysis/python-sql/sql-window-functions-cheat-sheet

Python & SQLReference

SQL window functions: a cheat sheet with worked results

The window functions you will use every week (ranking, running totals, previous row, share of total), each with a short query and the result it returns.

Grokem Editorial

Editorial desk

Published
  • 4 min read
Tested withSQLite3.42

A window function calculates a value for each row using a set of related rows (its window) without collapsing them into one row the way GROUP BY does. All the examples use this table:

sql
CREATE TABLE sales (id INTEGER PRIMARY KEY, region TEXT, rep TEXT, sold_on TEXT, amount INTEGER);
INSERT INTO sales (region, rep, sold_on, amount) VALUES
 ('north', 'Ana', '2025-01-05', 120), ('north', 'Ben', '2025-01-09', 200), ('north', 'Ana', '2025-01-15', 250),
 ('south', 'Cy',  '2025-01-06',  90), ('south', 'Di',  '2025-01-12', 300), ('south', 'Cy',  '2025-01-20', 150);

The shape

sql
function(args) OVER (
    PARTITION BY column     -- optional: restart the calculation for each group
    ORDER BY column         -- optional: order within the group
    ROWS BETWEEN ... AND ...  -- optional: which rows form the window (the frame)
)

PARTITION BY is like GROUP BY that keeps every row. ORDER BY decides sequence. The frame, when given, limits the window to a number of rows around the current one.

Ranking

Function What it does
ROW_NUMBER() 1, 2, 3 ... with no ties: tied rows still get different numbers
RANK() Ties share a number and the next number is skipped (1, 1, 3)
DENSE_RANK() Ties share a number and nothing is skipped (1, 1, 2)
NTILE(n) Splits the ordered rows into n buckets of nearly equal size
sql
SELECT region, rep, amount,
       RANK()       OVER w AS rank,
       DENSE_RANK() OVER w AS dense_rank
FROM sales
WINDOW w AS (PARTITION BY region ORDER BY amount DESC)
ORDER BY region, amount DESC;
region rep amount rank dense_rank
north Ana 250 1 1
north Ben 200 2 2
north Ana 120 3 3
south Di 300 1 1
south Cy 150 2 2
south Cy 90 3 3

When two rows tie on the ORDER BY value, ROW_NUMBER() picks between them arbitrarily. Add a second sort column (ORDER BY amount DESC, id) whenever the numbering must be repeatable.

Running totals and shares

sql
SELECT region, sold_on, amount,
       SUM(amount) OVER (PARTITION BY region ORDER BY sold_on) AS running_total,
       ROUND(100.0 * amount / SUM(amount) OVER (PARTITION BY region), 1) AS pct_of_region
FROM sales
ORDER BY region, sold_on;
region sold_on amount running_total pct_of_region
north 2025-01-05 120 120 21.1
north 2025-01-09 200 320 35.1
north 2025-01-15 250 570 43.9
south 2025-01-06 90 90 16.7
south 2025-01-12 300 390 55.6
south 2025-01-20 150 540 27.8

With an ORDER BY the sum covers the rows from the start of the partition up to the current row. Without one it covers the whole partition, which is what you want for a share of total.

Previous and next rows

Function Returns
LAG(col [, offset [, default]]) the value from an earlier row
LEAD(col [, offset [, default]]) the value from a later row
FIRST_VALUE(col) / LAST_VALUE(col) the first or last value of the frame
sql
SELECT region, sold_on, amount,
       amount - LAG(amount) OVER (PARTITION BY region ORDER BY sold_on) AS change
FROM sales
ORDER BY region, sold_on;

The first row of each partition has no previous row, so LAG returns NULL there (pass a third argument to use a default instead). For north the changes are NULL, 80 and 50; for south they are NULL, 210 and -150.

Moving averages and frames

sql
SELECT sold_on, amount,
       AVG(amount) OVER (ORDER BY sold_on ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS avg_2
FROM sales
ORDER BY sold_on;

ROWS BETWEEN 1 PRECEDING AND CURRENT ROW averages the current row and the one before it, so the first row averages only itself.

Pitfalls

  • You cannot filter on a window function in WHERE. Window functions are calculated after WHERE. Put the query in a subquery or common table expression and filter outside it, as the deduplication guide does with rn = 1.
  • LAST_VALUE surprises people. With an ORDER BY the default frame ends at the current row, so LAST_VALUE returns the current row's own value. Give it an explicit frame ending at UNBOUNDED FOLLOWING.
  • ROWS and RANGE differ. ROWS counts physical rows. RANGE treats rows with equal sort values as one group, which changes running totals when there are ties.
  • Support. Window functions arrived in SQLite 3.25 and have been in PostgreSQL, SQL Server, Oracle, BigQuery and Snowflake for years. MySQL has them from version 8.0.

From Grokem: https://www.grokem.com/data/data-analysis/python-sql/sql-window-functions-cheat-sheet

How did this strike you?

Be the first to react – it is anonymous.

Community

Questions about this?

Ask in Questions and help and get help from other readers.

Ask a questionBrowse Questions and help

Comments (2)

  • Nandini

    Do these examples work in MySQL as well, or only in SQLite?

    • Grokem Editorial

      Window functions arrived in MySQL 8.0 (and in SQLite 3.25), with the same `OVER (...)` syntax, so these queries run there unchanged. The examples here use SQLite because anyone can run it without installing a server. MySQL 5.7 does not have them.

Keep going

All in topic

Choose what we may use. You can change this at any time with “Privacy choices” at the foot of every page. Details are in the Cookie policy and the Privacy policy.

Strictly necessary

Remembers your theme, the lessons you marked as done and this choice. It stays on your device, tracks nothing and cannot be switched off.

Always on
What exactly is collected, and why

Grokem’s own counts

Collects
The pages you open and how long you stay, how far you scroll, where you came from, your device type, browser, screen size, language and time zone. Your IP address is turned into a code that changes every day; it is never stored as it is.
Why
To see how long lessons are read and where readers leave. Not shared with anyone.
Stored
A random visitor ID, a visit ID and the time of your last page view in your browser’s storage (gr_vid, gr_sid, gr_last). No name or email is attached.