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
While LAG() refers to the past—the preceding row—LEAD() refers to the future—the following row. It is commonly used for month-over-month or week-over-week comparisons and growth-rate calculations.
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.
| month | revenue |
|---|---|
| 2026-01 | 100 |
| 2026-02 | 120 |
| 2026-03 | 90 |
| 2026-04 | 150 |
| month | revenue | next_revenue | diff |
|---|---|---|---|
| 2026-01 | 100 | 120 | +20 |
| 2026-02 | 120 | 90 | -30 |
| 2026-03 | 90 | 150 | +60 |
| 2026-04 | 150 | NULL | NULL |
The last row, 2026-04, has no following month, so both next_revenue and diff are NULL. Any arithmetic operation involving NULL also returns NULL.
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free
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
When ORDER BY is present, the default frame—normally RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW—always includes the first row, which has the highest score, so every current row receives that top value. If scores tie, rows with the same ordering value are peers in the same frame, but the highest score returned by FIRST_VALUE does not change.
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.
| product | category | score |
|---|---|---|
| P1 | A | 85 |
| P2 | A | 92 |
| P3 | A | 78 |
| P4 | B | 70 |
| P5 | B | 88 |
| product | category | score | top_score | gap |
|---|---|---|---|---|
| P2 | A | 92 | 92 | 0 |
| P1 | A | 85 | 92 | 7 |
| P3 | A | 78 | 92 | 14 |
| P5 | B | 88 | 88 | 0 |
| P4 | B | 70 | 88 | 18 |
Every row in category A has top_score 92 from P2, while every row in category B has top_score 88 from P5. A row with gap=0 is the leader of its category.
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free
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
Because the frame slides along the ordered rows, it automatically continues to reference the latest N records as data grows. This differs fundamentally from a cumulative average, which averages every row from the beginning through the current row.
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.
| sale_date | amount |
|---|---|
| 04-01 | 30 |
| 04-02 | 50 |
| 04-03 | 40 |
| 04-04 | 60 |
| 04-05 | 80 |
| sale_date | amount | moving_avg_3 | Frame contents |
|---|---|---|---|
| 04-01 | 30 | 30.00 | [30] ÷ 1 |
| 04-02 | 50 | 40.00 | [30, 50] ÷ 2 |
| 04-03 | 40 | 40.00 | [30, 50, 40] ÷ 3 |
| 04-04 | 60 | 50.00 | [50, 40, 60] ÷ 3 ← 04-01 leaves the frame |
| 04-05 | 80 | 60.00 | [40, 60, 80] ÷ 3 ← slides again |
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free
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
Without a WINDOW clause, the same query would copy and paste PARTITION BY emp_id ORDER BY month three times. A named window is more maintainable because changing the definition requires editing only one place.
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.
- rank: which numbered month this is for the employee, using ROW_NUMBER
- running: the employee’s cumulative sales through that point, using SUM
- prev_sales: the employee’s sales in the preceding month, using LAG; NULL for the first month
| emp_id | dept | month | sales |
|---|---|---|---|
| E1 | Sales | 01 | 100 |
| E1 | Sales | 02 | 120 |
| E2 | Sales | 01 | 80 |
| E2 | Sales | 02 | 90 |
| E3 | Tech | 01 | 150 |
| emp_id | dept | month | sales | rank | running | prev_sales |
|---|---|---|---|---|---|---|
| E1 | Sales | 01 | 100 | 1 | 100 | NULL |
| E1 | Sales | 02 | 120 | 2 | 220 | 100 |
| E2 | Sales | 01 | 80 | 1 | 80 | NULL |
| E2 | Sales | 02 | 90 | 2 | 170 | 80 |
| E3 | Tech | 01 | 150 | 1 | 150 | NULL |
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free
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 gap_flagged AS ( SELECT ..., CASE WHEN LAG(event_time) OVER(...) IS NULL OR (event_time - LAG(event_time) OVER(...)) > 30 THEN 1 ELSE 0 END AS is_new_session FROM user_events ) SELECT ..., SUM(is_new_session) OVER(PARTITION BY user_id ORDER BY event_time) AS session_id FROM gap_flagged;
event_time is represented as an integer number of minutes since midnight: for example, 600 means 10:00 and 660 means 11:00. In a production database, calculate the time difference with a function or expression such as DATEDIFF or EXTRACT(EPOCH FROM ...), according to the database dialect.
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.
| user_id | event_time | event_type |
|---|---|---|
| U1 | 600 | login |
| U1 | 605 | click |
| U1 | 645 | click |
| U1 | 650 | logout |
| U2 | 660 | login |
| U2 | 670 | click |
U1: 600→605 (gap 5 min) →645 (gap 40 min → new session) →650 (gap 5 min) / U2: 660→670 (gap 10 min)
| user_id | event_time | event_type | session_id |
|---|---|---|---|
| U1 | 600 | login | 1 |
| U1 | 605 | click | 1 |
| U1 | 645 | click | 2 ← new session after a 40-minute gap |
| U1 | 650 | logout | 2 |
| U2 | 660 | login | 1 |
| U2 | 670 | click | 1 |
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free