LEAD() — Look Ahead to Calculate Growth and Differences
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
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.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 |
FIRST_VALUE() — Calculate the Gap from the Category Leader
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
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.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 |
Moving Average — Implement a Three-Day Sliding Window
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
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 |
|---|---|---|
| 04-01 | 30 | 30.00 |
| 04-02 | 50 | 40.00 |
| 04-03 | 40 | 40.00 |
| 04-04 | 60 | 50.00 |
| 04-05 | 80 | 60.00 |
WINDOW Clause — Write DRY SQL with a Shared Window Definition
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
FROM, WHERE, and GROUP BY, and before ORDER BY.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 |
Advanced Review — Implement Session Analysis with CTE + LAG + SUM
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;
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.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 |
| user_id | event_time | event_type | session_id |
|---|---|---|---|
| U1 | 600 | login | 1 |
| U1 | 605 | click | 1 |
| U1 | 645 | click | 2 |
| U1 | 650 | logout | 2 |
| U2 | 660 | login | 1 |
| U2 | 670 | click | 1 |