Skip to content
Sample lesson · SQL for Analytics Engineering

Window functions, without the mystery.

A free preview of one hands-on lesson. Your full path starts from your diagnostic results and mixes concise instruction with real code, exercises, labs, and projects.

~6 min readIntermediate SQLFree preview

Why window functions matter for an analytics engineer

The first time an interviewer asks you to compute a running total of revenue by customer, in order of order date, you have two options. You can write a self-join and pray. Or you can use a window function and ship.

Window functions are the SQL feature that separates the analyst who can write a query from the analytics engineer who can build a metric layer. Every dbt project of any size leans on them: cohorts, rolling aggregates, deduplication, slowly-changing dimensions. If you only learn one advanced SQL feature, learn this one.

The anatomy of a window function

Every window function follows the same shape. Two parts.

SUM(amount) OVER ( PARTITION BY customer_id ORDER BY order_date ) AS running_total

On the left, an aggregate (here, SUM) — but it doesn't collapse rows. On the right, a window definition (OVER (...)) — which tells the aggregate how to group and order the rows it sees.

The output: one row per input row, with the aggregate computed over the matching window. Crucially, you keep the original rows. GROUP BY would have collapsed them.

Window function explorer
ordersRow 1 of 6
Input rows. The current row and the rows inside its window frame are marked in the last column.
order_dateregionamountframe
2024-01-03east120current row
2024-01-07east80outside frame
2024-01-12east150outside frame
2024-01-19east60outside frame
2024-02-02west200other partition
2024-02-09west90other partition
SQL
select
  order_date,
  region,
  amount,
  sum(amount) over (
    partition by region
    order by order_date
    rows between unbounded preceding
             and current row
  ) as running_total
from orders
Resultrunning_total = 120

The frame covers every earlier row in the same region, plus the current row. For 2024-01-03: 120 = 120.

Full result set. Rows after the current step are shown but not yet explained.
order_dateregionrunning_total
2024-01-03east120
2024-01-07east200
2024-01-12east350
2024-01-19east410
2024-02-02west200
2024-02-09west290

Nothing here animates on its own.

Step through one row at a time to see exactly which rows the frame covers. The totals are computed from the table above them, not written by hand.

Three patterns you'll use weekly

1. Running total

The textbook case. Cumulative revenue per customer, in order of purchase. Watch closely how PARTITION BY isolates each customer and ORDER BY defines the cumulative direction.

SELECT customer_id, order_date, amount, SUM(amount) OVER (PARTITION BY customer_id ORDER BY order_date) AS running_total FROM orders;

2. Ranked within a group

Top 3 products by revenue in each category. Add ROW_NUMBER() or RANK() and a WHERE on the result.

SELECT * FROM ( SELECT category, product_id, revenue, ROW_NUMBER() OVER (PARTITION BY category ORDER BY revenue DESC) AS rn FROM product_sales ) WHERErn <= 3;

3. Period-over-period change

LAG() lets you reach back to the previous row in the ordered window. Useful for week-over-week, month-over-month, cohort retention — anywhere you want the prior value next to the current one.

SELECT month, revenue, revenue - LAG(revenue) OVER (ORDER BY month) AS mom_change FROM monthly_revenue;

A common ORDER BY mistake

Forgetting ORDER BY inside the window.The database isn't obligated to give you rows in any particular order without it, and most window functions depend on order. SUM() OVER (PARTITION BY x) without ORDER BY is a grand total within each partition — not a running total. Check whether that is the intended calculation.

An interviewer may ask what a window expression without an ORDER BY computes and how to change it.

Checkpoint

Lessons end at a checkpoint — a graded exercise where one exists for the topic. Here's this one:

Checkpoint · 7 minutes

Top 5 products by revenue, per category.

Given a product_sales table with category, product_id, and revenue — return the top 5 products by revenue inside each category. Order ties by product_id ascending.

What you just learned

  • Window functions return one row per input row — they don't collapse like GROUP BY.
  • Every window has two parts: an aggregate or ranking function, and an OVER (...) clause.
  • PARTITION BY isolates groups; ORDER BY defines the row sequence inside each group.
  • Three patterns will carry most of your dbt work: running totals, ranked-within-group, and period-over-period change.
  • Always include ORDER BY in the window when the order matters. It almost always does.
One of 90 core lessons

See the full curriculum.

Follow the path that matches your current skills through SQL, modeling, dbt, APIs, data quality, and a human-reviewed BigQuery + dbt Cloud capstone.

Continue exploring

Want to see a sample project too?

Lessons point into practice challenges, applied labs, selected portfolio builds, and the capstone. See an applied project brief.