SQL LAG Function Example With Real Dataset

SQL LAG Function Example With Real Dataset

If you have ever needed to compare a value in one row to the value in the previous row i.e. calculating month-over-month sales growth, finding the difference between consecutive readings, or identifying how long something took between events — you have encountered exactly the problem the SQL LAG function was built to solve.

Before LAG existed, analysts had to write complex self-joins or correlated subqueries to access data from a previous row. LAG makes this elegant, readable, and efficient in a single function call.

In this guide, we will break down the SQL LAG function with a real dataset — walking through every parameter, building up from simple to complex examples, and covering the most common real-world use cases.

What Is the LAG Function?

LAG is a window function that gives you access to a value from a previous row in the same result set without needing a self-join.

It looks back a specified number of rows and returns the value of a column from that earlier row, based on the ordering you define.

Simple Analogy

Imagine you are reading a spreadsheet of monthly sales figures. You want to know how much sales changed compared to last month. You look at the current row’s value, then glance up one row to see last month’s value, and subtract.

LAG does exactly that. It automatically “glances up” however many rows you specify and hands you the value from that row.

LAG Function Syntax

sql

LAG(column, offset, default_value)
    OVER (PARTITION BY partition_column ORDER BY order_column)
  • column — The column whose previous value you want to retrieve
  • offset — How many rows back to look (default is 1)
  • default_value — What to return when there is no previous row (default is NULL)
  • PARTITION BY — Optional. Restarts the window for each group
  • ORDER BY — Required. Defines what “previous” means

Setting Up the Real Dataset

We will use a realistic monthly sales dataset throughout this guide.

sql

CREATE TABLE monthly_sales (
    sale_id     INT,
    region      VARCHAR(50),
    month_date  DATE,
    revenue     DECIMAL(12, 2),
    units_sold  INT
);

INSERT INTO monthly_sales VALUES
(1,  'North', '2024-01-01', 125000, 520),
(2,  'North', '2024-02-01', 138000, 580),
(3,  'North', '2024-03-01', 142000, 610),
(4,  'North', '2024-04-01', 131000, 545),
(5,  'North', '2024-05-01', 155000, 650),
(6,  'North', '2024-06-01', 162000, 680),
(7,  'South', '2024-01-01', 98000,  410),
(8,  'South', '2024-02-01', 104000, 435),
(9,  'South', '2024-03-01', 97000,  405),
(10, 'South', '2024-04-01', 112000, 468),
(11, 'South', '2024-05-01', 119000, 497),
(12, 'South', '2024-06-01', 128000, 535);

Table Preview:

sale_idregionmonth_daterevenueunits_sold
1North2024-01-01125000520
2North2024-02-01138000580
3North2024-03-01142000610
4North2024-04-01131000545
5North2024-05-01155000650
6North2024-06-01162000680
7South2024-01-0198000410

Example 1: Basic LAG — Previous Month Revenue

The simplest use — retrieve last month’s revenue alongside the current month.

sql

SELECT
    region,
    month_date,
    revenue,
    LAG(revenue) OVER (ORDER BY month_date) AS prev_month_revenue
FROM monthly_sales
WHERE region = 'North'
ORDER BY month_date;

Result:

regionmonth_daterevenueprev_month_revenue
North2024-01-01125000NULL
North2024-02-01138000125000
North2024-03-01142000138000
North2024-04-01131000142000
North2024-05-01155000131000
North2024-06-01162000155000

January returns NULL because there is no previous row. It is the first month in the dataset.

Example 2: Replacing NULL With a Default Value

The third argument of LAG provides a default value when no previous row exists.

sql

SELECT
    region,
    month_date,
    revenue,
    LAG(revenue, 1, 0) OVER (ORDER BY month_date) AS prev_month_revenue
FROM monthly_sales
WHERE region = 'North'
ORDER BY month_date;

Result:

regionmonth_daterevenueprev_month_revenue
North2024-01-011250000
North2024-02-01138000125000
North2024-03-01142000138000

January now shows 0 instead of NULL. This is useful when you need to perform arithmetic on the result without NULL propagation breaking your calculations.

Example 3: Calculating Month-Over-Month Change

The most common real-world LAG pattern — calculating the absolute and percentage change from the previous period.

sql

SELECT
    region,
    month_date,
    revenue,
    LAG(revenue) OVER (ORDER BY month_date) AS prev_revenue,
    revenue - LAG(revenue) OVER (ORDER BY month_date) AS revenue_change,
    ROUND(
        (revenue - LAG(revenue) OVER (ORDER BY month_date)) * 100.0
        / LAG(revenue) OVER (ORDER BY month_date),
        2
    ) AS pct_change
FROM monthly_sales
WHERE region = 'North'
ORDER BY month_date;

Result:

regionmonth_daterevenueprev_revenuerevenue_changepct_change
North2024-01-01125000NULLNULLNULL
North2024-02-011380001250001300010.40
North2024-03-0114200013800040002.90
North2024-04-01131000142000-11000-7.75
North2024-05-011550001310002400018.32
North2024-06-0116200015500070004.52

April shows a revenue drop of $11,000 (-7.75%) compared to March which is immediately visible from the LAG calculation.

Example 4: PARTITION BY — LAG Within Each Region

Without PARTITION BY, LAG compares rows across all regions which produces incorrect cross-region comparisons. PARTITION BY restarts the LAG window for each region independently.

sql

SELECT
    region,
    month_date,
    revenue,
    LAG(revenue) OVER (
        PARTITION BY region
        ORDER BY month_date
    ) AS prev_month_revenue,
    revenue - LAG(revenue) OVER (
        PARTITION BY region
        ORDER BY month_date
    ) AS mom_change
FROM monthly_sales
ORDER BY region, month_date;

Result:

regionmonth_daterevenueprev_month_revenuemom_change
North2024-01-01125000NULLNULL
North2024-02-0113800012500013000
North2024-03-011420001380004000
North2024-04-01131000142000-11000
North2024-05-0115500013100024000
North2024-06-011620001550007000
South2024-01-0198000NULLNULL
South2024-02-01104000980006000
South2024-03-0197000104000-7000
South2024-04-011120009700015000
South2024-05-011190001120007000
South2024-06-011280001190009000

Each region now resets to NULL at its own January.

Example 5: LAG With Offset — Look Back Multiple Rows

The second argument controls how many rows back to look. Set it to 3 to compare to three months ago.

sql

SELECT
    region,
    month_date,
    revenue,
    LAG(revenue, 3) OVER (
        PARTITION BY region
        ORDER BY month_date
    ) AS revenue_3_months_ago,
    revenue - LAG(revenue, 3) OVER (
        PARTITION BY region
        ORDER BY month_date
    ) AS qtr_change
FROM monthly_sales
ORDER BY region, month_date;

Result (North region):

regionmonth_daterevenuerevenue_3_months_agoqtr_change
North2024-01-01125000NULLNULL
North2024-02-01138000NULLNULL
North2024-03-01142000NULLNULL
North2024-04-011310001250006000
North2024-05-0115500013800017000
North2024-06-0116200014200020000

April through June can now compare to January through March giving you a quarter-over-quarter view within the same query.

Example 6: Using CTE to Clean Up NULL Rows

Wrap the LAG query in a CTE to filter out rows where the previous value is NULL. This is useful for cleaner reporting.

sql

WITH sales_with_lag AS (
    SELECT
        region,
        month_date,
        revenue,
        LAG(revenue) OVER (
            PARTITION BY region
            ORDER BY month_date
        ) AS prev_revenue
    FROM monthly_sales
)
SELECT
    region,
    month_date,
    revenue,
    prev_revenue,
    revenue - prev_revenue AS mom_change,
    ROUND((revenue - prev_revenue) * 100.0 / prev_revenue, 2) AS pct_change
FROM sales_with_lag
WHERE prev_revenue IS NOT NULL
ORDER BY region, month_date;

The WHERE clause removes the first row of each region where prev_revenue is NULL giving you a clean result with no NULL rows and no division-by-zero risk.

Example 7: Identifying Consecutive Drops

Find months where revenue dropped compared to the previous month. This is useful for flagging concerning trends.

sql

WITH sales_with_lag AS (
    SELECT
        region,
        month_date,
        revenue,
        LAG(revenue) OVER (
            PARTITION BY region
            ORDER BY month_date
        ) AS prev_revenue
    FROM monthly_sales
)
SELECT
    region,
    month_date,
    revenue,
    prev_revenue,
    revenue - prev_revenue AS revenue_change,
    'Revenue Drop' AS flag
FROM sales_with_lag
WHERE revenue < prev_revenue
ORDER BY region, month_date;

Result:

regionmonth_daterevenueprev_revenuerevenue_changeflag
North2024-04-01131000142000-11000Revenue Drop
South2024-03-0197000104000-7000Revenue Drop

Two months across both regions showed revenue declines. This kind of alert query is extremely valuable in automated monitoring pipelines.

Example 8: LAG on Units Sold — Multiple Columns

LAG can be applied to multiple columns in the same query — each with its own OVER clause.

sql

SELECT
    region,
    month_date,
    revenue,
    units_sold,
    LAG(revenue) OVER (PARTITION BY region ORDER BY month_date) AS prev_revenue,
    LAG(units_sold) OVER (PARTITION BY region ORDER BY month_date) AS prev_units,
    revenue - LAG(revenue) OVER (PARTITION BY region ORDER BY month_date) AS rev_change,
    units_sold - LAG(units_sold) OVER (PARTITION BY region ORDER BY month_date) AS units_change
FROM monthly_sales
ORDER BY region, month_date;

Now you have month-over-month change for both revenue and units sold in a single clean result set.

LAG vs LEAD — What Is the Difference?

LAG and LEAD are mirror functions. LAG looks backward to previous rows. LEAD looks forward to future rows.

sql

SELECT
    region,
    month_date,
    revenue,
    LAG(revenue) OVER (PARTITION BY region ORDER BY month_date) AS prev_month,
    LEAD(revenue) OVER (PARTITION BY region ORDER BY month_date) AS next_month
FROM monthly_sales
WHERE region = 'North'
ORDER BY month_date;

Result:

regionmonth_daterevenueprev_monthnext_month
North2024-01-01125000NULL138000
North2024-02-01138000125000142000
North2024-03-01142000138000131000
North2024-04-01131000142000155000
North2024-05-01155000131000162000
North2024-06-01162000155000NULL

Use LAG when you need to compare to what happened before. Use LEAD when you need to look ahead at what comes next.

LAG vs LEAD

FeatureLAGLEAD
DirectionLooks backwardLooks forward
ReturnsPrevious row valueNext row value
First row resultNULL (no previous)Value of next row
Last row resultValue of previous rowNULL (no next)
Common useMonth-over-month changeDays until next event
Same syntax Yes Yes

Database Compatibility

DatabaseLAG Support
SQL Server 2012+ Yes
PostgreSQL 8.4+ Yes
MySQL 8.0+ Yes
Oracle 11g+ Yes
SQLite 3.25+ Yes
BigQuery Yes
Snowflake Yes

LAG is now supported in all major modern SQL databases.

Real-World Use Cases

Sales and Revenue Analysis — Month-over-month, quarter-over-quarter, and year-over-year growth calculations across product lines, regions, and sales teams.

Financial Reporting — Stock price change from previous close, balance changes between accounting periods, running comparison of actuals vs prior period budget.

User Behavior Analytics — Time between sessions for the same user, identifying churn signals when gaps between logins increase, measuring feature adoption over time.

Operations and Logistics — Comparing delivery times between consecutive shipments, identifying slowdowns in production throughput, monitoring SLA compliance trends.

IoT and Sensor Data — Detecting anomalous readings by comparing each sensor reading to the previous one, identifying equipment degradation patterns.

Common Mistakes to Avoid

  • Forgetting ORDER BY — LAG requires ORDER BY inside the OVER clause to define what “previous” means. Without it, the result is undefined and databases will throw an error
  • Not using PARTITION BY for grouped data — Without PARTITION BY, LAG compares across all rows regardless of grouping. The last row of Region A bleeds into the first row of Region B — producing incorrect cross-group comparisons
  • Arithmetic with NULL results — The first row of each partition returns NULL from LAG. Any arithmetic involving NULL produces NULL. Use COALESCE or the default value argument to handle this: LAG(revenue, 1, 0)
  • Using wrong offset — LAG(revenue, 2) looks back two rows — not two months. If your data has gaps (missing months), the offset counts rows, not time periods. Always verify your data has no gaps when using LAG for time-based analysis
  • Confusing LAG and LEAD direction — LAG goes backward (previous rows). LEAD goes forward (next rows). Getting this wrong produces results that are the mirror image of what you intended
  • Not wrapping in a CTE for further filtering — You cannot filter on a window function result directly in the same WHERE clause. Wrap the LAG query in a CTE or subquery first, then filter on the result

Quick Reference Cheat Sheet

TaskQuery Pattern
Previous row valueLAG(col) OVER (ORDER BY date_col)
Previous row with defaultLAG(col, 1, 0) OVER (ORDER BY date_col)
Previous row per groupLAG(col) OVER (PARTITION BY grp ORDER BY date_col)
Look back N rowsLAG(col, N) OVER (ORDER BY date_col)
MoM changerevenue - LAG(revenue) OVER (PARTITION BY region ORDER BY month)
MoM percent change(revenue - LAG(revenue)OVER(...)) * 100.0 / LAG(revenue)OVER(...)
Filter NULL rowsWrap in CTE → WHERE prev_col IS NOT NULL
Flag dropsWHERE revenue < LAG(revenue) OVER (…)

The SQL LAG function is one of the most practically useful window functions in data analysis. Once you understand how it works, you will find yourself reaching for it constantly — in sales reporting, financial analysis, operations monitoring, and user behavior analysis.

Here is a quick recap of everything we covered:

  • LAG retrieves the value from a previous row without a self-join
  • The three arguments are: column, offset (default 1), and default value (default NULL)
  • ORDER BY is required — it defines what “previous” means
  • PARTITION BY restarts the window for each group — essential for grouped data
  • Wrap LAG queries in a CTE to filter or further transform the results
  • LEAD is the forward-looking counterpart to LAG

Start with the basic single-partition example, then add PARTITION BY for grouped data, and finally combine with CTE filtering for production-ready analytical queries.

FAQs

What does the SQL LAG function do?

LAG returns the value of a column from a previous row in the result set based on a specified ordering without requiring a self-join. It is a window function used for comparing values across consecutive rows.

What is the difference between LAG and LEAD in SQL?

LAG looks backward and returns a value from a previous row. LEAD looks forward and returns a value from a future row. Both use identical syntax — only the direction differs.

How do I avoid NULL in LAG results?

Use the third argument of LAG to specify a default value: LAG(revenue, 1, 0) returns 0 instead of NULL when no previous row exists. Alternatively, wrap the query in a CTE and filter with WHERE prev_value IS NOT NULL.

Why do I need PARTITION BY in a LAG function?

PARTITION BY restarts the LAG window for each group — like each region or each product. Without it, LAG compares across all rows regardless of group, causing incorrect cross-group comparisons where the last row of one group bleeds into the first row of the next.

Can LAG look back more than one row?

Yes. The second argument controls the offset. LAG(revenue, 3) looks back three rows. This is useful for quarter-over-quarter comparisons when monthly data is stored row by row.

Does LAG work in MySQL?

Yes, but only in MySQL 8.0 and later. Earlier versions of MySQL do not support window functions. All other major databases — PostgreSQL, SQL Server, Oracle, Snowflake, and BigQuery — support LAG as well.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top