๐Ÿงพ SQL Window Functions Cheat Sheet โ€“ The Complete Quick Reference

This SQL window functions cheat sheet is the single page you can keep open while you write analytic SQL. Every function, every frame clause, every common pattern โ€” with copy-ready syntax and a link to a practice problem you can solve right now.

Whether youโ€™re prepping for a SQL interview, debugging a dashboard query, or just trying to remember the difference between RANK() and DENSE_RANK() at 11pm, this guide has you covered.

๐Ÿ“„ Want it offline? Download the printable PDF version โ€” same content, fits on two pages.


๐Ÿš€ Why a Window Functions Cheat Sheet

"Xorthax's quartermaster doesn't memorize every starship manual โ€” they keep a reference card on the bridge. You should too."

Window functions are powerful but unforgiving. One missing ORDER BY, one wrong frame clause, and your โ€œrunning totalโ€ returns the same number on every row. This cheat sheet gives you the exact patterns that work, organized so you can find them fast.

  • Scan it when you remember the pattern but forgot the syntax.
  • Print it for interview prep โ€” every function comes with a "when to use it" cue.
  • Click through to the matching practice problem to lock in the muscle memory.



๐Ÿงฌ Window Function Anatomy

Every window function follows the same shape. Memorize this skeleton and you can always fill in the blanks:

The universal window function syntax โ€” every function plugs into this pattern.
FUNCTION(expr) OVER (
  [PARTITION BY partition_col, ...]   -- group rows into windows
  [ORDER BY sort_col [ASC|DESC]]      -- order within each window
  [ROWS|RANGE|GROUPS BETWEEN          -- which rows count for each calc
       frame_start AND frame_end]
)
ClauseWhat it doesRequired?
OVER ( ... )Marks the function as a window function.โœ… Always
PARTITION BYSplits rows into independent groups.Optional โ€” defaults to one window over all rows
ORDER BYDefines row order inside each partition.Required for ranking, value, & distribution functions
ROWS / RANGE / GROUPSSelects which rows feed each calculation.Optional โ€” has a default that often surprises you

๐Ÿ‘‰ Want the deep dive on each clause? See How Window Functions Work.




๐Ÿงฎ The Four Function Categories

Every window function belongs to one of four families. Knowing the category tells you what defaults to expect.

Category Functions Needs ORDER BY? Accepts frame?
Aggregate SUM, AVG, COUNT, MIN, MAX Optional โœ… Yes
Ranking ROW_NUMBER, RANK, DENSE_RANK, NTILE โœ… Required โŒ No
Value / Navigation LAG, LEAD, FIRST_VALUE, LAST_VALUE, NTH_VALUE โœ… Required FIRST/LAST/NTH only
Distribution PERCENT_RANK, CUME_DIST โœ… Required โŒ No



โž• Aggregate Window Functions

The same aggregates you already know (SUM, AVG, etc.) โ€” but applied per-row, without collapsing the result set.

FunctionUse it whenโ€ฆ
SUM(expr) OVER (...)Running totals, cumulative revenue, partition totals on every row.
AVG(expr) OVER (...)Moving averages, rolling means, partition-level averages.
COUNT(*) OVER (...)Running counts, partition sizes, "how many in my group so far".
MIN(expr) OVER (...)Lowest seen so far, partition minimum repeated on every row.
MAX(expr) OVER (...)Highest seen so far, partition max repeated on every row.
Running total and 7-day moving average โ€” the two patterns youโ€™ll write a hundred times.
SELECT
  order_date,
  amount,
  SUM(amount) OVER (
    ORDER BY order_date
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  ) AS running_total,
  AVG(amount) OVER (
    ORDER BY order_date
    ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
  ) AS moving_avg_7d
FROM Orders;

๐Ÿ‘‰ Full guide: Aggregate Window Functions.




๐Ÿฅ‡ Ranking Functions

Assign a position to each row inside its partition. The differences only matter when there are ties.

Function How it handles ties Output for values 700, 700, 600
ROW_NUMBER()Always unique โ€” ties broken arbitrarily1, 2, 3
RANK()Same rank for ties, then skip1, 1, 3
DENSE_RANK()Same rank for ties, no skip1, 1, 2
NTILE(n)Splits rows into n roughly equal bucketse.g. NTILE(4) โ†’ 1โ€“4 quartile bucket
All four ranking functions side by side, partitioned by Region.
SELECT
  CustomerID,
  Region,
  OrderAmount,
  ROW_NUMBER() OVER (PARTITION BY Region ORDER BY OrderAmount DESC) AS rn,
  RANK()       OVER (PARTITION BY Region ORDER BY OrderAmount DESC) AS rnk,
  DENSE_RANK() OVER (PARTITION BY Region ORDER BY OrderAmount DESC) AS drnk,
  NTILE(4)     OVER (PARTITION BY Region ORDER BY OrderAmount DESC) AS quartile
FROM Orders;

๐Ÿ‘‰ Full guide: Ranking Functions.




๐Ÿ” Value & Navigation Functions

Reach into other rows of the partition without writing a self-join.

FunctionWhat it returnsCommon use
LAG(expr, offset, default) Value from offset rows before the current row Month-over-month change, time between events
LEAD(expr, offset, default) Value from offset rows after the current row "Days until next order", forecasting checks
FIRST_VALUE(expr) First row in the window frame Onboarding price, customerโ€™s first order date
LAST_VALUE(expr) Last row in the window frame โš ๏ธ Latest snapshot โ€” requires an explicit unbounded frame
NTH_VALUE(expr, n) The n-th row in the window frame "Second-highest", "third event", etc.
Month-over-month revenue change with LAG().
SELECT
  product_id,
  month,
  revenue,
  LAG(revenue) OVER (PARTITION BY product_id ORDER BY month) AS prev_month,
  revenue - LAG(revenue) OVER (PARTITION BY product_id ORDER BY month) AS mom_change
FROM monthly_revenue;

โš ๏ธ LAST_VALUE gotcha: with the default frame (RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) it returns the current row, not the partitionโ€™s last row. Always specify ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING.

๐Ÿ‘‰ Full guides: LEAD / LAG ยท FIRST / LAST / NTH.




๐Ÿ“Š Distribution Functions

Express where each row sits relative to its peers, on a 0โ€“1 scale.

FunctionFormulaUse it whenโ€ฆ
PERCENT_RANK() (rank - 1) / (rows - 1) You want a percentile rank โ€” first row is 0.0, last is 1.0.
CUME_DIST() rows โ‰ค current / total rows You want cumulative distribution โ€” "what fraction of rows โ‰ค this value".
Top 10% of orders per region using PERCENT_RANK and a CTE.
WITH Ranked AS (
  SELECT *,
         PERCENT_RANK() OVER (PARTITION BY Region ORDER BY Amount DESC) AS pr
  FROM Orders
)
SELECT * FROM Ranked WHERE pr <= 0.10;

๐Ÿ‘‰ Full guide: Percentile & Distribution.




๐Ÿ“ Frame Clauses: ROWS vs RANGE vs GROUPS

The frame clause decides which rows are visible to your function for each row's calculation. Most bugs in window queries trace back to this.

FrameWhat "current row" meansUse whenโ€ฆ
ROWS A specific number of rows before/after the current row You want exact row counts (e.g. last 7 rows for a 7-row moving avg)
RANGE All rows whose ORDER BY value matches the bound You want value-based bounds (e.g. all rows on the same date)
GROUPS A specific number of peer groups (rows with equal sort keys) Postgres 11+ โ€” when you want N distinct sort values, ties counted together

Frame Boundaries Quick Reference

BoundaryMeaning
UNBOUNDED PRECEDINGFrom the first row of the partition
n PRECEDINGn rows before the current row
CURRENT ROWThe current row itself
n FOLLOWINGn rows after the current row
UNBOUNDED FOLLOWINGThrough the last row of the partition

The Defaults That Bite

SituationDefault frameWhat it produces
SUM() OVER (ORDER BY ...) RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW A running total โœ…
SUM() OVER (PARTITION BY ...) (no ORDER BY) UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING Partition total on every row โœ…
LAST_VALUE() OVER (ORDER BY ...) RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW Returns the current row, not the last โŒ



๐Ÿงฐ Common Patterns (One-Liners You'll Reuse Forever)

The handful of patterns that cover ~80% of real analytics SQL.

PatternSQLPractice
Running total SUM(amount) OVER (ORDER BY order_date) #2
Partition total on every row SUM(amount) OVER (PARTITION BY customer_id) #1
7-day moving average AVG(amount) OVER (ORDER BY date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) #5
Top-N per group ROW_NUMBER() OVER (PARTITION BY group ORDER BY metric DESC) AS rn  (filter rn <= N) #3
Deduplication (keep latest) ROW_NUMBER() OVER (PARTITION BY key ORDER BY updated_at DESC)  (filter = 1) #22
Month-over-month change amount - LAG(amount) OVER (PARTITION BY id ORDER BY month) #6
Days since last event order_date - LAG(order_date) OVER (PARTITION BY customer_id ORDER BY order_date) #4
Percent of total amount / SUM(amount) OVER () * 100 #7
Percent of group amount / SUM(amount) OVER (PARTITION BY group) * 100 #7
First / last value per partition FIRST_VALUE(price) OVER (PARTITION BY product ORDER BY date) #21
Quartile bucketing NTILE(4) OVER (ORDER BY revenue DESC) #33
Gap between events date - LAG(date) OVER (PARTITION BY id ORDER BY date) Gap & Island



๐Ÿงญ Quick Decision Tree: Which Function Do I Need?

Match the question youโ€™re trying to answer to the function family.

If you need toโ€ฆReach for
Add an aggregate alongside detail rowsSUM/AVG/COUNT OVER (PARTITION BY ...)
Compute a running totalSUM OVER (ORDER BY ...)
Compute a moving / rolling windowAVG OVER (ORDER BY ... ROWS BETWEEN n PRECEDING AND CURRENT ROW)
Number every row uniquelyROW_NUMBER()
Rank with ties + gaps (1, 1, 3)RANK()
Rank with ties, no gaps (1, 1, 2)DENSE_RANK()
Bucket into N equal-sized groupsNTILE(n)
Compare against the previous rowLAG()
Compare against the next rowLEAD()
Get the first or last value in a groupFIRST_VALUE() / LAST_VALUE()
Express a row as a percentilePERCENT_RANK() / CUME_DIST()
Find streaks or gaps in datesROW_NUMBER() + LAG() โ€” see Gap & Island



โš ๏ธ Common Pitfalls (Quick Fixes)

  • โŒ Forgetting ORDER BY for ranking/value functions โ€” results are non-deterministic. Fix: always include ORDER BY, with a tie-breaker if values can repeat.
  • โŒ LAST_VALUE() returning the current row โ€” the default frame stops at CURRENT ROW. Fix: add ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING.
  • โŒ Using a window function in WHERE โ€” window functions run after WHERE. Fix: wrap the query in a CTE or subquery and filter outside.
  • โŒ Confusing RANK with DENSE_RANK โ€” RANK skips numbers after ties; DENSE_RANK doesnโ€™t. Fix: pick based on whether downstream consumers expect contiguous ranks.
  • โŒ ROW_NUMBER() changing across runs โ€” the engine breaks ties however it wants. Fix: add a stable secondary column to ORDER BY (e.g. an id).
  • โŒ Using ROWS when you needed RANGE (or vice versa) โ€” different semantics with duplicate ORDER BY values. Fix: ROWS = exact row count, RANGE = value-based.
  • โŒ Heavy PARTITION BY on huge tables โ€” sort spills kill performance. Fix: filter aggressively first; consider LATERAL joins for top-N at scale.



๐ŸŒ Database Differences (At a Glance)

Window function support is broadly consistent โ€” but the syntactic sugar differs. The Reference section has dedicated pages for each function in each database.

FeaturePostgreSQLMySQL 8+SQL ServerSnowflake / BigQuery
All standard window functions โœ…โœ…โœ…โœ…
QUALIFY clause (filter window results) โŒโŒโŒโœ… (huge readability win)
GROUPS frame โœ… (11+)โŒโŒSnowflake โœ… / BigQuery โŒ
EXCLUDE frame option โœ… (11+)โŒโŒโŒ
Named windows (WINDOW w AS (...)) โœ…โœ…โŒโœ…

๐Ÿ‘‰ For exact syntax in your dialect, browse the Reference section โ€” every function has a dedicated page per database (Postgres, MySQL, SQL Server, MariaDB, SQLite, Snowflake, BigQuery, and more).







๐Ÿ”— Related Learning Pages

This cheat sheet is a quick reference. The deep dives live here:

More Resources by Function and Dialect

Want to explore all window functions across every SQL dialect? Visit the full SQL Dialects & Window Functions Documentation.