SQL Window Functions and CTEs

Use CTEs, recursive queries, ranking, running totals, partitions, and lag/lead patterns for analytical SQL.

View
StandardDetailedCompact
Export
Copy the compact sheet, download it, or print it.
Download
`D` dense toggle · `C` copy all

Common Table Expressions

Write named subqueries with WITH.

Basic CTE

Create a named subquery for readability and reuse.

sqlANYctewith
sql
WITH recent_orders AS (
  SELECT *
  FROM orders
  WHERE created_at >= CURRENT_DATE - INTERVAL "30 days"
)
SELECT customer_id, COUNT(*) AS total_recent_orders
FROM recent_orders
GROUP BY customer_id;
Notes

CTEs make multi-step queries easier to read and maintain.

Multiple CTEs

Chain multiple named steps in one query.

sqlANYctewith
sql
WITH order_totals AS (
  SELECT customer_id, SUM(amount) AS total_spent
  FROM orders
  GROUP BY customer_id
),
ranked AS (
  SELECT customer_id, total_spent
  FROM order_totals
)
SELECT *
FROM ranked;
Notes

Multiple CTEs are useful for staged transformations.

Recursive CTE

Traverse hierarchical or graph-like data.

sqlANYcterecursive
sql
WITH RECURSIVE org_tree AS (
  SELECT employee_id, manager_id, name, 1 AS depth
  FROM employees
  WHERE manager_id IS NULL

  UNION ALL

  SELECT e.employee_id, e.manager_id, e.name, ot.depth + 1
  FROM employees e
  JOIN org_tree ot ON ot.employee_id = e.manager_id
)
SELECT *
FROM org_tree
ORDER BY depth, employee_id;
Notes

Recursive CTEs are powerful for trees, nested categories, and parent-child traversal.

Window Function Basics

Use OVER clauses without collapsing rows.

Assign row numbers

Number rows within an ordered partition.

sqlANYwindow-functionrow-number
sql
SELECT order_id, customer_id, created_at,
       ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY created_at DESC) AS rn
FROM orders;
Notes

ROW_NUMBER is useful for deduping and top-N-per-group queries.

Rank rows

Assign ordered rankings with or without gaps.

sqlANYwindow-functionrankdense-rank
sql
SELECT employee_id, salary,
       RANK() OVER (ORDER BY salary DESC) AS salary_rank,
       DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_salary_rank
FROM employees;
Notes

RANK leaves gaps after ties; DENSE_RANK does not.

Compute running total

Accumulate values across an ordered window.

sqlANYwindow-functionrunning-total
sql
SELECT order_id, created_at, amount,
       SUM(amount) OVER (ORDER BY created_at ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_total
FROM orders;
Notes

Running totals are common in finance and time-series analysis.

Average within partition

Compute metrics per partition while keeping each row.

sqlANYwindow-functionpartition
sql
SELECT employee_id, department_id, salary,
       AVG(salary) OVER (PARTITION BY department_id) AS dept_avg_salary
FROM employees;
Notes

Window functions differ from GROUP BY because they keep row-level detail.

Lag, Lead, and Value Navigation

Compare rows to earlier or later rows.

Use LAG

Read a value from the previous row in the same partition.

sqlANYwindow-functionlag
sql
SELECT day, total_sales,
       LAG(total_sales) OVER (ORDER BY day) AS previous_day_sales
FROM daily_sales;
Notes

LAG is great for deltas, trend analysis, and change detection.

Use LEAD

Read a value from the next row in the same partition.

sqlANYwindow-functionlead
sql
SELECT subscription_id, starts_at,
       LEAD(starts_at) OVER (PARTITION BY user_id ORDER BY starts_at) AS next_start
FROM subscriptions;
Notes

LEAD helps compare a row to the following event.

Use FIRST_VALUE and LAST_VALUE

Return boundary values within a window frame.

sqlANYwindow-functionfirst-valuelast-value
sql
SELECT customer_id, order_id, amount,
       FIRST_VALUE(amount) OVER (PARTITION BY customer_id ORDER BY created_at) AS first_order_amount
FROM orders;
Notes

Window frame details affect LAST_VALUE behavior in some dialects.

Buckets and Percentiles

Segment rows into bands or percentile-like groups.

Bucket rows with NTILE

Divide ordered rows into a fixed number of groups.

sqlANYwindow-functionntilepercentiles
sql
SELECT customer_id, total_spent,
       NTILE(4) OVER (ORDER BY total_spent DESC) AS quartile
FROM customer_lifetime_value;
Notes

NTILE is helpful for quartiles, deciles, and score bands.

Use PERCENT_RANK and CUME_DIST

Measure relative row position inside an ordered set.

sqlANYwindow-functionpercent-rankcume-dist
sql
SELECT employee_id, salary,
       PERCENT_RANK() OVER (ORDER BY salary) AS percent_rank,
       CUME_DIST() OVER (ORDER BY salary) AS cumulative_distribution
FROM employees;
Notes

Useful for reporting on distribution and percentile-like placement.

Recommended next

No recommendations yet.