Introduction to the LAG() Function in SQL Server

In SQL Server, LAG() is a window function that enables us to access a value from a previous row in the same result set, without the need to perform a self-join.

We specify the previous row as an offset from the current row. An offset of 1 means the previous row, an offset of 2 means two rows back, and so on.

Syntax

The official syntax goes like this:

LAG (scalar_expression [ , offset ] [ , default ] ) [ IGNORE NULLS | RESPECT NULLS ]
    OVER ( [ partition_by_clause ] order_by_clause )

So scalar_expression can be the name of a column in the query, and we have the option of specifying the offset (offset) and a default value (default) to use in instances where offset exceeds the scope of the partition and there’s no value to be returned.

We must also provide an OVER clause, and that clause must have an ORDER BY clause.

Example

Here’s a basic example to demonstrate:

SELECT
    VendorId,
    ProductName,
    ProductPrice,
    LAG( ProductPrice, 1 ) OVER ( 
        ORDER BY ProductPrice
        ) AS LAG
FROM Products;

Result:

VendorId  ProductName                      ProductPrice  LAG  
--------  -------------------------------  ------------  -----
1004      Bottomless Coffee Mugs (4 Pack)  9.99          null 
1003      Hammock                          10            9.99 
1001      Long Weight (green)              11.99         10   
1004      Tea Pot                          12.45         11.99
1001      Long Weight (blue)               14.75         12.45
1001      Left handed screwdriver          25.99         14.75
1001      Right handed screwdriver         25.99         25.99
1002      Sledge Hammer                    33.49         25.99
1003      Straw Dog Box                    55.99         33.49
1003      Chainsaw                         245           55.99

Here, I specified that the LAG() function return its value from the ProductPrice column, and I specified an offset of 1.

We can see that the LAG column returns its value from the previous row. On the first row however, it returns null. That’s because there’s no previous row for which to retrieve a value.

Using the Default Offset

We can omit the second argument in order to use the default offset value of 1:

SELECT
    VendorId,
    ProductName,
    ProductPrice,
    LAG( ProductPrice ) OVER ( 
        ORDER BY ProductPrice
        ) AS LAG
FROM Products;

Result:

VendorId  ProductName                      ProductPrice  LAG  
--------  -------------------------------  ------------  -----
1004      Bottomless Coffee Mugs (4 Pack)  9.99          null 
1003      Hammock                          10            9.99 
1001      Long Weight (green)              11.99         10   
1004      Tea Pot                          12.45         11.99
1001      Long Weight (blue)               14.75         12.45
1001      Left handed screwdriver          25.99         14.75
1001      Right handed screwdriver         25.99         25.99
1002      Sledge Hammer                    33.49         25.99
1003      Straw Dog Box                    55.99         33.49
1003      Chainsaw                         245           55.99

Same result as the previous example. The only difference is in the code we used to produce the result. In the previous example we explicitly specified an offset of 1, whereas in this example we omitted the offset argument altogether in order to use the default offset of 1.

Changing the Offset

Here’s an example of using a different offset:

SELECT
    VendorId,
    ProductName,
    ProductPrice,
    LAG( ProductPrice, 3 ) OVER ( 
        ORDER BY ProductPrice
        ) AS LAG
FROM Products;

Result:

VendorId  ProductName                      ProductPrice  LAG  
--------  -------------------------------  ------------  -----
1004      Bottomless Coffee Mugs (4 Pack)  9.99          null 
1003      Hammock                          10            null 
1001      Long Weight (green)              11.99         null 
1004      Tea Pot                          12.45         9.99 
1001      Long Weight (blue)               14.75         10   
1001      Left handed screwdriver          25.99         11.99
1001      Right handed screwdriver         25.99         12.45
1002      Sledge Hammer                    33.49         14.75
1003      Straw Dog Box                    55.99         25.99
1003      Chainsaw                         245           25.99

Here I specified an offset of 3, and so we got the value from three rows back. We also got null in the first three rows of the LAG column as a result.

Change the Default Value

By default, LAG() returns null if the offset is beyond the scope of the partition and there’s no value to be returned (such as when we’re on the first row).

We can provide a third argument to change the default value:

SELECT
    VendorId,
    ProductName,
    ProductPrice,
    LAG( ProductPrice, 3, 0 ) OVER ( 
        ORDER BY ProductPrice
        ) AS LAG
FROM Products;

Result:

VendorId  ProductName                      ProductPrice  LAG  
--------  -------------------------------  ------------  -----
1004      Bottomless Coffee Mugs (4 Pack)  9.99          0    
1003      Hammock                          10            0    
1001      Long Weight (green)              11.99         0    
1004      Tea Pot                          12.45         9.99 
1001      Long Weight (blue)               14.75         10   
1001      Left handed screwdriver          25.99         11.99
1001      Right handed screwdriver         25.99         12.45
1002      Sledge Hammer                    33.49         14.75
1003      Straw Dog Box                    55.99         25.99
1003      Chainsaw                         245           25.99

In this case I specified a default value of 0, and so the first three rows return 0 instead of null.

Note that the third argument, if provided, must be type-compatible with the expression provided by the first argument.

Partitioning the Result Set

We can use a PARTITION BY clause with window functions to partition the result set by a given column. For example, we can partition by vendor:

SELECT
    v.VendorName,
    p.ProductName,
    p.ProductPrice,
    LAG( ProductPrice ) OVER ( 
        PARTITION BY p.VendorId
        ORDER BY p.ProductPrice
        ) AS LAG
FROM Products p 
INNER JOIN Vendors v
ON v.VendorId = p.VendorId;

Result:

VendorName     ProductName                      ProductPrice  LAG  
-------------  -------------------------------  ------------  -----
Mars Supplies  Long Weight (green)              11.99         null 
Mars Supplies  Long Weight (blue)               14.75         11.99
Mars Supplies  Left handed screwdriver          25.99         14.75
Mars Supplies  Right handed screwdriver         25.99         25.99
Randy Roofers  Sledge Hammer                    33.49         null 
Pedal Medals   Hammock                          10            null 
Pedal Medals   Straw Dog Box                    55.99         10   
Pedal Medals   Chainsaw                         245           55.99
Katty Kittens  Bottomless Coffee Mugs (4 Pack)  9.99          null 
Katty Kittens  Tea Pot                          12.45         9.99 

This time our LAG column resets with each new partition.

In this case I used a SQL join to join the Products and Vendors table so that we got the vendor names instead of just their IDs.

The OVER Clause is Required

As mentioned, we must provide the OVER clause when using window functions like LAG(). Here’s what happens if we remove it:

SELECT
    VendorId,
    ProductName,
    ProductPrice,
    LAG( ProductPrice )
FROM Products;

Result:

Msg 10753, Level 15, State 1, Line 5
The function 'LAG' must have an OVER clause.

The ORDER BY Clause is Required

And our OVER clause must include an ORDER BY clause. Here’s what happens when we use an OVER clause without an ORDER BY clause:

SELECT
    VendorId,
    ProductName,
    ProductPrice,
    LAG( ProductPrice ) OVER( )
FROM Products;

Result:

Msg 4112, Level 15, State 1, Line 5
The function 'LAG' must have an OVER clause with ORDER BY.

Documentation

For more information and examples, see Microsoft’s documentation for the LAG() function.