Fun with Learning Technology
LearnCoursesQuestionsTracksToolsNewsExplorePractice
Fun with Learning Technology

A new problem, explained clearly, every day.

Subscribe
Learn
  • Lessons
  • Topics
  • News
  • Tools
  • Courses
  • Career tracks
  • Everything
Site
  • About
  • Contact
  • Support
  • Privacy
  • Terms
Get the daily one

One email per new problem. No spam.

Request a tutorial

Requests shape what gets made next.

© 2026 Fun with Learning TechnologyRSS
Home›Courses›SQL›LAG, LEAD, FIRST_VALUE, LAST_VALUE

Advanced Queries

LAG, LEAD, FIRST_VALUE, LAST_VALUE

Window functions allow you to access data from other rows relative to the current row without collapsing the result set into a single group. This capability is essential for calculating period-over-period changes, identifying trends, and performing comparative analysis within a dataset. Reach for these functions whenever your calculation logic depends on the sequence or relative position of records rather than simple aggregate summaries.

The Logic of LAG and LEAD

The LAG and LEAD functions serve as navigational tools that allow a query to look backward or forward in the result set relative to the current row. Unlike a self-join, which often becomes computationally expensive as the table size grows, these functions operate during the windowing phase of the query execution plan. By defining an ORDER BY clause within the OVER partition, you establish a deterministic sequence. LAG accesses the value at a specified offset before the current record, while LEAD retrieves it from the subsequent records. These functions are fundamentally about identifying gaps or transitions. Because they maintain the granularity of the original rows, you can perform direct arithmetic on adjacent values, such as calculating daily price differences or identifying the time elapsed between sequential user logins. Understanding that these functions rely entirely on the sort order defined in the window specification is critical for producing accurate, reproducible analytical output.

-- Calculate daily sales growth by comparing today to yesterday
SELECT 
    sale_date, 
    amount, 
    amount - LAG(amount, 1) OVER (ORDER BY sale_date) AS growth
FROM daily_sales;

Handling Missing Data with Default Values

A common point of failure when using LAG and LEAD is the boundary condition: what happens when there is no preceding or subsequent row? By default, these functions return NULL, which can propagate through arithmetic operations, potentially invalidating your entire calculation. To handle this gracefully, both functions support an optional second and third argument. The second argument defines the offset (the number of rows to skip), while the third argument provides a default value to be used if the requested row falls outside the partition boundaries. Providing a default, such as zero or a placeholder value, ensures that downstream calculations remain robust and prevents accidental null-related logic errors. This is particularly useful in time-series data where the first record lacks a prior counterpart, allowing you to treat the 'starting' state as a distinct business event rather than a missing data point.

-- Replace NULL with 0 to prevent calculation errors on the first day
SELECT 
    sale_date, 
    LAG(amount, 1, 0) OVER (ORDER BY sale_date) AS prev_day_sales
FROM daily_sales;

Anchoring with FIRST_VALUE

While LAG and LEAD provide relative positioning, FIRST_VALUE acts as an anchor, allowing you to capture a reference point that persists across all rows in a defined frame. This function returns the value from the very first row of the window frame. By default, the window frame extends from the start of the partition to the current row, but you can override this using frame clauses like ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING. This is invaluable when you need to compare every row in a group against a baseline or a starting condition, such as comparing a product's current price to its initial launch price. The reasoning here is that by pinning the first value, you create a static reference point that does not shift as you iterate through subsequent rows, enabling normalized comparisons across the entire lifecycle of a specific category or identifier.

-- Calculate the percentage difference from the initial launch price
SELECT 
    product_name, 
    price, 
    FIRST_VALUE(price) OVER (PARTITION BY product_id ORDER BY change_date) AS launch_price
FROM product_history;

Closing the Range with LAST_VALUE

LAST_VALUE is conceptually similar to FIRST_VALUE but retrieves the final record in the frame. However, its usage requires a precise understanding of window frames, as the default frame is 'RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW'. Because of this default, if you use LAST_VALUE without an explicit frame clause, it will simply return the value of the current row, leading to confusing results. To capture the actual final value of a group, you must expand the window frame to include all rows up to the end of the partition. Once properly configured, LAST_VALUE allows you to reach forward and inspect the terminal state of a process, such as the final status of a support ticket or the most recent price update for a stock, while still keeping the context of the intermediate rows visible within the result set.

-- Capture the final price in the set using an explicit window frame
SELECT 
    product_id, 
    price,
    LAST_VALUE(price) OVER (
        PARTITION BY product_id 
        ORDER BY change_date 
        ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
    ) AS final_price
FROM product_history;

Combining Functions for Complex Analysis

Professional SQL development often involves chaining these functions to create multi-dimensional views of data. You might use LAG to see where a value was, FIRST_VALUE to see where it started, and LEAD to anticipate where it is going, all within a single SELECT statement. This is efficient because the database engine performs a single pass over the sorted data for each partition, optimizing performance significantly compared to correlated subqueries. The key to reasoning about these combined expressions is to visualize the partition as a fixed array: once you define the window, every row 'sees' the same set of boundaries. By layering these functions, you can derive complex metrics like moving averages, cumulative totals, or trend reversals. Mastering the composition of these tools allows you to solve advanced business logic problems without ever needing to export raw data into external processing tools.

-- Combining functions to get a full view of the price journey
SELECT 
    change_date, 
    price,
    LAG(price) OVER (ORDER BY change_date) as prev_price,
    FIRST_VALUE(price) OVER (ORDER BY change_date) as start_price,
    LEAD(price) OVER (ORDER BY change_date) as next_price
FROM product_history;

Key points

  • LAG and LEAD allow you to retrieve values from relative positions without performing expensive self-joins.
  • The ORDER BY clause inside the OVER specification is mandatory to define the sequence for relative functions.
  • Default values in LAG and LEAD help avoid NULL propagation in arithmetic operations.
  • FIRST_VALUE anchors your analysis by comparing every row against the first record in the partition.
  • LAST_VALUE requires an explicit frame clause to reach the end of the partition instead of the current row.
  • Window functions execute in a specific order that allows them to maintain row-level granularity while performing aggregates.
  • Chaining multiple window functions allows for sophisticated trend analysis within a single scan of the data.
  • Correctly defining window frames is the most important factor in preventing logical errors with aggregate-style navigation.

Common mistakes

  • Mistake: Forgetting the ORDER BY clause in window functions. Why it's wrong: Without an ordering, the results are non-deterministic, meaning LAG/LEAD could return random values. Fix: Always include an explicit ORDER BY inside the OVER clause.
  • Mistake: Misunderstanding the default frame for LAST_VALUE. Why it's wrong: By default, the frame is RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, so LAST_VALUE always returns the current row. Fix: Use 'ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING'.
  • Mistake: Using window functions in a WHERE clause. Why it's wrong: Logical processing order dictates that WHERE is evaluated before window functions. Fix: Wrap the query in a Common Table Expression (CTE) or subquery and filter in the outer query.
  • Mistake: Assuming LAG or LEAD will naturally fill gaps in sparse data. Why it's wrong: LAG and LEAD look at the previous/next row in the result set, not at temporal gaps in the data itself. Fix: Join against a calendar table or use time-series generation techniques before applying window functions.
  • Mistake: Incorrectly using window functions with GROUP BY. Why it's wrong: Window functions operate after the grouping has occurred, which can lead to unexpected aggregations of the aggregate results. Fix: Ensure window functions are used on the base dataset or compute the aggregation separately.

Interview questions

What is the primary purpose of the LAG and LEAD window functions in SQL?

The LAG and LEAD window functions are designed to access data from previous or subsequent rows in a result set without the need for a self-join. LAG allows you to retrieve the value from a preceding row, while LEAD retrieves it from a following row based on a specified offset. These are essential for calculating period-over-period changes, such as finding the difference between today's sales and yesterday's sales. By using an OVER clause with ORDER BY, you define the sequence of the data, which makes these functions incredibly performant compared to joining a table to itself multiple times, as they operate during a single scan of the data partition.

How do FIRST_VALUE and LAST_VALUE differ in terms of their functionality within window functions?

FIRST_VALUE and LAST_VALUE are analytic functions used to return the first or last value in an ordered set of rows within a window partition. FIRST_VALUE is straightforward, returning the value from the very first row of the window frame. LAST_VALUE, however, is often misunderstood because, by default, the window frame is defined as 'RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW.' This means that as the function iterates, it sees the current row as the last row. To get the true last value of a partition, you must explicitly expand your frame clause to 'RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING' to ensure the entire partition is considered.

Compare using a self-join versus using window functions like LAG for calculating row-to-row differences.

Using a self-join to compare adjacent rows requires joining a table to itself on a condition like 'T1.id = T2.id + 1', which is computationally expensive because it creates a Cartesian product subset that must be filtered, leading to high I/O and memory overhead on large tables. In contrast, LAG performs the same operation by accessing the physical buffer of the previous row directly within the window partition process. Window functions are generally much faster and result in cleaner, more readable code that avoids the complexity of managing aliases for the same table, making them the industry standard for sequential data analysis.

Why is the window frame clause crucial when using LAST_VALUE?

The window frame clause is crucial for LAST_VALUE because of how SQL defines the default frame of a window. When you specify an ORDER BY clause without a frame, SQL defaults to 'RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW.' Because the 'current row' is constantly changing as the engine iterates, the LAST_VALUE function will simply return the value of the current row, effectively mimicking the identity of that row. To actually capture the last item of the entire group, you must use 'ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING' so the function looks beyond the current position to the actual end of the set.

How would you handle NULL values when using LAG and LEAD functions?

Both LAG and LEAD accept an optional third argument that serves as a default value if the function does not find a preceding or subsequent row. If you do not provide this argument, the result of the function will simply be NULL for the first row (for LAG) or the last row (for LEAD). By providing a default value—for example, using 'LAG(sales, 1, 0)'—you can instruct the database to return 0 instead of NULL. This is particularly useful in reporting scenarios where you want to perform arithmetic on the returned values without NULLs causing your calculations to return NULL or leading to unexpected gaps in your trend analysis.

In a scenario where you have multiple partitions, how do window functions manage the boundaries of LAG, LEAD, and FIRST_VALUE?

Window functions use the PARTITION BY clause to define the boundaries of the analysis. When you include 'PARTITION BY category' in your window definition, the functions like LAG, LEAD, or FIRST_VALUE effectively reset their logic for every unique category. For instance, LAG will not pull data from the last row of a previous category into the first row of the current category; it treats each partition as an isolated, independent bucket. This is highly efficient for running separate calculations on distinct groups—like finding the first sale date per product—all within a single query pass, ensuring data integrity across distinct groupings without complex correlated subqueries.

All SQL interview questions →

Check yourself

1. You need to compare each employee's salary to their predecessor's salary in the same department. Which clause must be included in your OVER statement to ensure accuracy?

  • A.PARTITION BY department_id ORDER BY hire_date
  • B.GROUP BY department_id
  • C.ORDER BY salary DESC
  • D.ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
Show answer

A. PARTITION BY department_id ORDER BY hire_date
Partitioning by department isolates the scope, and ordering by hire_date defines what 'predecessor' means. The other options either fail to define the relative order or interfere with the window scope.

2. You attempt to get the maximum value of a column over the entire partition using LAST_VALUE, but you keep getting the value of the current row. What is the cause?

  • A.The table lacks a primary key.
  • B.The default frame clause limits the window to the current row.
  • C.LAST_VALUE only works with numeric data types.
  • D.The partition is not ordered.
Show answer

B. The default frame clause limits the window to the current row.
The default window frame is 'RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW', which excludes future rows. The other options do not dictate why LAST_VALUE stops at the current row.

3. If you want to filter for employees whose salary is lower than the person hired immediately before them, where should the LAG function be placed?

  • A.In the WHERE clause of the main query.
  • B.In the HAVING clause of the main query.
  • C.In an outer query that selects from a subquery or CTE where the LAG was calculated.
  • D.Directly in the SELECT list, then referenced by name in the WHERE clause.
Show answer

C. In an outer query that selects from a subquery or CTE where the LAG was calculated.
SQL processes WHERE clauses before window functions. Wrapping in a subquery/CTE allows the engine to calculate the LAG result first, making it available for filtering. The other options cause syntax errors.

4. What is the primary functional difference between using a self-join to compare adjacent rows versus using LAG/LEAD?

  • A.Self-joins are faster on large datasets.
  • B.LAG/LEAD can handle duplicates without extra logic, whereas joins cannot.
  • C.LAG/LEAD avoids Cartesian product issues and is more readable when maintaining partitioning.
  • D.Self-joins cannot be used for time-series data.
Show answer

C. LAG/LEAD avoids Cartesian product issues and is more readable when maintaining partitioning.
Window functions like LAG/LEAD were specifically designed to avoid the performance and logic complexities of self-joining. Joins don't have built-in partitioning and can easily result in duplicates if keys are not unique.

5. Consider a query with FIRST_VALUE(salary) OVER (ORDER BY salary). If two employees have the same minimum salary, what will the output look like?

  • A.The engine will return NULL for the second employee.
  • B.Both employees will show the same minimum salary.
  • C.The function will return the salary of the employee with the lower ID.
  • D.The query will throw an error due to non-deterministic results.
Show answer

B. Both employees will show the same minimum salary.
FIRST_VALUE retrieves the first value in the defined window. If both have the same salary, they are equal in the sort order, so the function returns the value regardless of the duplicate. It does not return NULL or error out.

Take the full SQL quiz →

← PreviousWindow Functions — ROW_NUMBER, RANK, DENSE_RANKNext →PARTITION BY with Window Functions

SQL

32 lessons, free to read.

All lessons →

Track your progress

Sign in to mark lessons done, score quizzes and keep notes.

Open in the app