Lab 01: Window Functions

Time: 40 minutes | Level: Practitioner | DB: PostgreSQL 15

Window functions perform calculations across a set of table rows related to the current row — without collapsing the result set. Unlike GROUP BY, every row is preserved.


Step 1 — Setup: Create Sales Data

CREATE TABLE sales (
  id         SERIAL PRIMARY KEY,
  salesperson VARCHAR(50),
  region      VARCHAR(50),
  amount      NUMERIC(10,2),
  sale_date   DATE
);

INSERT INTO sales (salesperson, region, amount, sale_date) VALUES
  ('Alice', 'East', 1200.00, '2024-01-05'),
  ('Bob',   'East',  950.00, '2024-01-10'),
  ('Carol', 'West', 1500.00, '2024-01-03'),
  ('Dave',  'West',  800.00, '2024-01-15'),
  ('Eve',   'East', 1200.00, '2024-01-20'),
  ('Frank', 'West', 2000.00, '2024-01-22'),
  ('Grace', 'East',  600.00, '2024-01-25'),
  ('Heidi', 'West', 1800.00, '2024-01-28');

💡 The OVER() clause is what transforms an aggregate function into a window function. Without it, you'd get a single-row aggregate.

📸 Verified Output:


Step 2 — ROW_NUMBER, RANK, DENSE_RANK, NTILE

  • ROW_NUMBER: always unique, even for ties

  • RANK: skips numbers after ties (1,1,3)

  • DENSE_RANK: no gaps after ties (1,1,2)

  • NTILE(n): divides into n buckets

📸 Verified Output:


Step 3 — LAG and LEAD: Row Navigation

💡 LAG(col, n, default) and LEAD(col, n, default) accept offset and default-value arguments. LAG(amount, 2, 0) looks back two rows, returning 0 if none.

📸 Verified Output (partial):


Step 4 — Running Totals and Moving Averages

Frame clauses:

Frame
Meaning

ROWS UNBOUNDED PRECEDING

from start to current row

ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING

3-row moving window

ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING

entire partition

📸 Verified Output:


Step 5 — FIRST_VALUE and LAST_VALUE

💡 LAST_VALUE requires an explicit ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING frame — the default frame only goes to the current row, which would return the current row's value.

📸 Verified Output:


Step 6 — Named Windows with WINDOW Clause

💡 The WINDOW clause lets you define a window frame once and reference it by name — avoids repetition and keeps queries readable.


Step 7 — Practical: Top-N per Group

📸 Verified Output:

💡 Use RANK() instead of ROW_NUMBER() when you want to include all tied rows at position N.


Step 8 — Capstone: Sales Performance Dashboard

Build a complete sales report combining multiple window functions:


Summary

Function
Purpose
Ties Handling

ROW_NUMBER()

Unique sequential number

Each tie gets different number

RANK()

Rank with gaps

Ties share rank; next rank skips

DENSE_RANK()

Rank without gaps

Ties share rank; next rank consecutive

NTILE(n)

Divide into n buckets

Even distribution

LAG(col, n)

Access n rows before current

N/A

LEAD(col, n)

Access n rows after current

N/A

FIRST_VALUE(col)

First value in window frame

N/A

LAST_VALUE(col)

Last value in window frame

Needs explicit frame

SUM/AVG OVER

Running aggregate

N/A

Last updated