Window Functions
Window Functions
Section titled “Window Functions”Window functions let you calculate across related rows while keeping each row intact. Unlike GROUP BY which collapses rows, window functions keep all rows.
Real-World Analogy
Section titled “Real-World Analogy”Think of students taking a test:
- GROUP BY: “Show me the average score per class” → collapses all students into one row per class
- Window Function: “Show me each student’s score AND their class average on the same row” → keeps all students, adds extra info
How Window Functions Work
Section titled “How Window Functions Work”flowchart TB subgraph Input[Original Rows] I1["Alice | Engineering | 90000"] I2["Bob | Engineering | 80000"] I3["Carol | Engineering | 80000"] I4["David | Marketing | 70000"] I5["Eve | Marketing | 60000"] end
subgraph Window[PARTITION BY department] W1["🏷️ Engineering (3 rows)"] W2["🏷️ Marketing (2 rows)"] end
subgraph Result[Result — Rows Stay, Window Calculated] R1["Alice | Engineering | 90000 | ROW_NUM: 1 | AVG: 83333"] R2["Bob | Engineering | 80000 | ROW_NUM: 2 | AVG: 83333"] R3["Carol | Engineering | 80000 | ROW_NUM: 3 | AVG: 83333"] R4["David | Marketing | 70000 | ROW_NUM: 1 | AVG: 65000"] R5["Eve | Marketing | 60000 | ROW_NUM: 2 | AVG: 65000"] end
Input --> Window Window --> Result
style Input fill:#3b82f6,color:#fff style W1 fill:#7c3aed,color:#fff style W2 fill:#059669,color:#fff style Result fill:#10b981,color:#fffBasic Syntax
Section titled “Basic Syntax”SELECT name, department, salary, window_function() OVER ( PARTITION BY column -- group into windows (like GROUP BY) ORDER BY column -- order within each window frame_clause -- which rows to include ) AS aliasFROM employees;ROW_NUMBER, RANK, DENSE_RANK
Section titled “ROW_NUMBER, RANK, DENSE_RANK”SELECT name, department, salary, ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS row_num, RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS rank_num, DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dense_rank_numFROM employees;Salaries per department: 90000, 80000, 80000, 70000
ROW_NUMBER: 1, 2, 3, 4 ← always unique, no tiesRANK: 1, 2, 2, 4 ← tie gets same rank, then SKIPDENSE_RANK: 1, 2, 2, 3 ← tie gets same rank, no SKIPLAG and LEAD
Section titled “LAG and LEAD”Compare a row with the previous or next row:
SELECT name, salary, hire_date, LAG(salary, 1) OVER (ORDER BY hire_date) AS prev_salary, LEAD(salary, 1) OVER (ORDER BY hire_date) AS next_salary, salary - LAG(salary, 1) OVER (ORDER BY hire_date) AS salary_changeFROM employees;┌────────┬────────┬──────────────┬──────────────┬──────────────┐│ name │ salary │ prev_salary │ next_salary │ change │├────────┼────────┼──────────────┼──────────────┼──────────────┤│ Alice │ 60000 │ NULL │ 70000 │ NULL ││ Bob │ 70000 │ 60000 │ 80000 │ +10000 ││ Carol │ 80000 │ 70000 │ 90000 │ +10000 ││ David │ 90000 │ 80000 │ NULL │ +10000 │└────────┴────────┴──────────────┴──────────────┴──────────────┘Running Totals
Section titled “Running Totals”SELECT name, department, salary, SUM(salary) OVER ( ORDER BY salary ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS running_total, AVG(salary) OVER (PARTITION BY department) AS dept_avgFROM employees;NTILE — Divide into Buckets
Section titled “NTILE — Divide into Buckets”-- Split employees into 4 salary quartilesSELECT name, salary, NTILE(4) OVER (ORDER BY salary DESC) AS quartileFROM employees;FIRST_VALUE and LAST_VALUE
Section titled “FIRST_VALUE and LAST_VALUE”SELECT name, department, salary, FIRST_VALUE(name) OVER (PARTITION BY department ORDER BY salary DESC) AS highest_paid, LAST_VALUE(name) OVER (PARTITION BY department ORDER BY salary DESC RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS lowest_paidFROM employees;Real-World Examples
Section titled “Real-World Examples”Top-N per group (e.g., top 2 salaries per department):
SELECT name, department, salaryFROM ( SELECT name, department, salary, DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dr FROM employees) rankedWHERE dr <= 2;Year-over-year comparison:
SELECT year, revenue, LAG(revenue, 1) OVER (ORDER BY year) AS prev_year_revenue, (revenue - LAG(revenue, 1) OVER (ORDER BY year)) / LAG(revenue, 1) OVER (ORDER BY year) * 100 AS yoy_growth_pctFROM yearly_sales;Cumulative percentage (running total / total):
SELECT name, salary, SUM(salary) OVER (ORDER BY salary) AS running_total, ROUND(SUM(salary) OVER (ORDER BY salary) / SUM(salary) OVER () * 100, 2) AS cum_pctFROM employees;In Simple Words
Section titled “In Simple Words”- Window functions don’t collapse rows — each original row stays in the result
PARTITION BYcreates groups (likeGROUP BYbut doesn’t collapse)ORDER BYinsideOVER()defines the order within each window- ROW_NUMBER = unique number per row; RANK = ties share rank, then skip; DENSE_RANK = ties share rank, no skip
- LAG/LEAD = compare with previous/next row
- Very common in interview questions — practice writing them!