SQL Window Functions — Applied LEAD, FIRST_VALUE, WINDOW

ADVWindow FunctionsPractical PatternsSession AnalysisMoving AveragesPostgreSQL/MySQL 8.0+/BigQuery5 questions
1 / 5 · In progress 0 / 5 completed
QUESTION 6

LEAD() — Look Ahead to Calculate Growth and Differences

LEADORDER BYGrowth AnalysisThe Opposite Direction from LAG
Background

LEAD(expr, offset, default) returns the value offset rows ahead of the current row. The default offset is one row. When no next row exists, such as on the last row of a partition, it returns default, or NULL when the default is omitted.

LEAD(revenue)         -- revenue from 1 row ahead (default)
LEAD(revenue, 2)      -- revenue from 2 rows ahead
LEAD(revenue, 1, 0)  -- 1 row ahead; return 0 when that row does not exist
How it differs from LAG: While LAG() refers to the past—the preceding row—LEAD() refers to the future—the following row. For both, ORDER BY decides which row counts as next or previous. They are commonly used for month-over-month or week-over-week comparisons and growth-rate calculations.
Problem

From the monthly_revenue table below, return each month’s revenue, the following month’s revenue as next_revenue, and the difference from the following month as diff = next_revenue − revenue. Return NULL when no following month exists.

Input Table
▸ monthly_revenue
monthrevenue
2026-01100
2026-02120
2026-0390
2026-04150
Expected Output
monthrevenuenext_revenuediff
2026-01100120+20
2026-0212090-30
2026-0390150+60
2026-04150NULLNULL
QUESTION 7

FIRST_VALUE() — Calculate the Gap from the Category Leader

FIRST_VALUEPARTITION BYCategory ComparisonThe LAST_VALUE Trap
Background

FIRST_VALUE(expr) returns the value from the first row of the window frame. Combined with PARTITION BY category ORDER BY score DESC, it consistently retrieves the highest score—the leader—in each category.

FIRST_VALUE(score) OVER(
  PARTITION BY category
  ORDER BY score DESC
) AS top_score  -- Highest score in the category
The default frame and tied rows: When ORDER BY is present, the default frame—normally RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW—always includes the first row, so every current row receives the value from that first row. If values tie, rows sharing the same ordering value are peers in the same frame, but the value FIRST_VALUE returns does not change.
Problem

From the product_scores table below, return each product’s score, the highest score in the same category as top_score, and the gap from the leader as gap = top_score − score. Sort products by descending score within each category.

Input Table
▸ product_scores
productcategoryscore
P1A85
P2A92
P3A78
P4B70
P5B88
Expected Output
productcategoryscoretop_scoregap
P2A92920
P1A85927
P3A789214
P5B88880
P4B708818
QUESTION 8

Moving Average — Implement a Three-Day Sliding Window

ROWS BETWEENAVG + PRECEDINGMoving AverageSliding Window
Background

A moving average calculates the average over the latest N periods and is widely used to track trends in stock prices, sales, traffic, and other time-series data. With window functions, define the frame using ROWS BETWEEN.

AVG(amount) OVER(
  ORDER BY sale_date
  ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
)  -- Average over the latest 3 days: 2 preceding rows through the current row
Moving average vs. cumulative average: Because the frame slides along the ordered rows, it always references only the latest N records as data grows. That range differs fundamentally from a cumulative average, which averages every row from the beginning through the current row. On the earliest rows, where the frame cannot reach far enough back, only the rows that exist are included.
Problem

From the daily_sales table below, calculate each day’s sales and the three-day moving average through that day as moving_avg_3. This table contains exactly one row per day with no missing dates. Return rows by ascending date; for the initial rows with fewer than three days of data, average only the rows that exist.

Input Table
▸ daily_sales
sale_dateamount
04-0130
04-0250
04-0340
04-0460
04-0580
Expected Output
sale_dateamountmoving_avg_3
04-013030.00
04-025040.00
04-034040.00
04-046050.00
04-058060.00
QUESTION 9

WINDOW Clause — Write DRY SQL with a Shared Window Definition

WINDOW ClauseDRY PrincipleMaintainabilityPostgreSQL/MySQL 8+/BigQuery
Background

When several window functions repeat the same PARTITION BY ... ORDER BY ..., a WINDOW clause lets you define a named window once and reuse it, following the DRY: Don't Repeat Yourself principle.

SELECT
  ROW_NUMBER() OVER(w) AS rank,   -- Refer to the same window w
  SUM(sales)   OVER(w) AS running,
  LAG(sales)   OVER(w) AS prev
FROM t
WINDOW w AS (PARTITION BY emp_id ORDER BY month); -- Define it in one place
Why the WINDOW clause helps: Without it, the same window definition is copied once per function. A named window is more maintainable because changing the definition means editing only one place. The WINDOW clause goes after FROM, WHERE, and GROUP BY, and before ORDER BY.
Problem

From the employee_sales table below, attach the following three metrics to every employee-month. Use a WINDOW clause to define PARTITION BY emp_id, dept ORDER BY month in one place.

  1. rank: which numbered month this is for the employee, using ROW_NUMBER
  2. running: the employee’s cumulative sales through that point, using SUM
  3. prev_sales: the employee’s sales in the preceding month, using LAG; NULL for the first month
Input Table
▸ employee_sales
emp_iddeptmonthsales
E1Sales01100
E1Sales02120
E2Sales0180
E2Sales0290
E3Tech01150
Expected Output
emp_iddeptmonthsalesrankrunningprev_sales
E1Sales011001100NULL
E1Sales021202220100
E2Sales0180180NULL
E2Sales0290217080
E3Tech011501150NULL
QUESTION 10

Advanced Review — Implement Session Analysis with CTE + LAG + SUM

CTELAG + CASE WHENSession AnalysisCommon Production Pattern
Background

Session analysis is a fundamental product-analytics technique for identifying each session, or sequence of continuous actions, in a user’s event log. Use LAG + CASE WHEN to detect the rule “start a new session when more than 30 minutes have elapsed since the preceding event,” then cumulatively SUM the flags to assign session numbers. A gap of exactly 30 minutes remains in the same session.

WITH flagged AS (
  SELECT ...,
    CASE WHEN LAG(ts_col) OVER(...) IS NULL
            OR (ts_col - LAG(ts_col) OVER(...)) > gap_limit
         THEN 1 ELSE 0 END AS is_new_session
  FROM table_name
)
SELECT ...,
  SUM(is_new_session) OVER(PARTITION BY key_col ORDER BY ts_col) AS session_id
FROM flagged;
Units of event_time: event_time is an integer number of minutes since midnight: 600 means 10:00 and 660 means 11:00. Time differences are therefore plain subtraction. In a production database, calculate them with a function or expression such as DATEDIFF or EXTRACT(EPOCH FROM ...), according to the database dialect.
Problem

From the user_events table below, assign a session number as session_id for each user under this rule: start a new session when the gap from the preceding event exceeds 30 minutes, meaning the difference in event_time is greater than 30. Each user’s first event starts session 1.

Input Table (event_time is minutes since midnight: 600=10:00, 605=10:05, and so on)
▸ user_events
user_idevent_timeevent_type
U1600login
U1605click
U1645click
U1650logout
U2660login
U2670click
Expected Output
user_idevent_timeevent_typesession_id
U1600login1
U1605click1
U1645click2
U1650logout2
U2660login1
U2670click1