LAG is the SQL version of glancing at the row just behind you in line — interviewers love it because it checks whether you understand ordering, partitions, and NULL behavior together.
Question: What is LAG in SQL?
Answer: LAG is a window function that lets you read a value from a previous row in the same result set, without using a self-join. You choose an ordering, optionally split the data into partitions, and SQL gives you the value from N rows earlier. The first row in each partition has no previous row, so it returns NULL unless you provide a default.
Interview-Ready Answer: I use LAG when I need the prior row’s value in the same partition, usually for deltas like day-over-day revenue or status changes. It works by sorting rows with ORDER BY inside OVER, then looking back by an offset, which defaults to 1. One important detail is that the ordering must be deterministic, so if ties are possible I add a tie-breaker column.
LAG doesLAG(expr, offset, default) returns the value of expr from a row earlier in the same ordered window. Think of a window as a moving view over rows: PARTITION BY splits the data into separate mini-lists, and ORDER BY decides the row sequence inside each list.
FROM, WHERE, GROUP BY, and HAVING.OVER (...) ORDER BY list.LAG looks back offset rows; the default offset is 1 in most engines.NULL if no default is given.GROUP BY, which collapses rows.pending to paid.| Approach | Best for | Main trade-off |
|---|---|---|
LAG | Previous row | Needs stable order |
LEAD | Next row | Looks forward |
| Self-join | Custom matching | More verbose |
Most engines must sort each partition by the window order, so the main cost is usually the sort: roughly O(n log n) for the ordered rows, followed by a cheap linear pass. If the data is already stored or indexed in partition/order order, the engine may avoid a full sort or reduce work a lot. On large partitions, the sort can spill to disk when memory is tight, which is why a query over a few million wide rows can suddenly get much slower.
Three important gotchas: first, if the ORDER BY is not unique, results can be nondeterministic because SQL may pick different physical row orders. Second, LAG does not mean “previous non-null” — it means the previous row, even if that value is NULL. Third, the default value only applies when the offset row is missing; it does not replace a real NULL stored in the earlier row.
Real-World Example: In a checkout service, you might use LAG to compare each order total with the previous order for the same customer. That helps with fraud checks, pricing audits, and alerts when a discount engine suddenly changes behavior. A common bug is ordering only by timestamp when two events share the same second; then the previous amount can flip between runs. The symptom is flaky dashboards, confusing support tickets, and logs that show inconsistent day-over-day deltas for the same customer.
WITH sales (sale_id, customer_id, sale_date, amount) AS (
VALUES
(1, 'A', '2024-01-01', 100),
(2, 'A', '2024-01-03', 120),
(3, 'A', '2024-01-03', 130),
(4, 'A', '2024-01-05', NULL),
(5, 'B', '2024-01-02', 200),
(6, 'B', '2024-01-04', 180)
), lagged AS (
SELECT
sale_id,
customer_id,
sale_date,
amount,
-- The default value 0 applies only when there is no earlier row in the partition.
-- It does not replace a real NULL from the previous row.
LAG(amount, 1, 0) OVER (
PARTITION BY customer_id
ORDER BY sale_date, sale_id
) AS prev_amount
FROM sales
)
SELECT
sale_id,
customer_id,
sale_date,
amount,
prev_amount,
amount - prev_amount AS delta_from_prev
FROM lagged
ORDER BY customer_id, sale_date, sale_id;Follow-up & Tricky Questions:
LAG different from a self-join? LAG is shorter and usually easier to read because the engine handles the row lookup inside the window. A self-join can still work, but you must manually match each row to its predecessor, which is easier to get wrong and often more verbose.LAG(x, 2) means two rows earlier instead of one. If that row does not exist, SQL returns the default value or NULL.ORDER BY mandatory? Because “previous” only makes sense when rows are in a defined sequence. Without a stable order, the database cannot know which row counts as earlier.LAG after GROUP BY? Yes, but only on the grouped result set, not the original raw rows. That is useful when you want the previous day’s total sales, not the previous individual sale.LAG(value). This is the classic use case for trends and deltas.LAG skip NULLs? Not in the core standard behavior. It reads the previous row, even if that row’s value is NULL, so you may need extra logic if you want the previous non-null value.LAG? Usually no. LAG depends on partition and order; the frame is not what makes it work, so beginners should focus on the window order first.Common Mistakes:
sale_id when timestamps can repeat.LAG skips NULLs. Fix: it does not; if you need the previous non-null value, use engine-specific features or a different query pattern.LAG and LEAD. Fix: LAG looks backward, LEAD looks forward.Memory Hook: Think “LAG = look behind.” If the current row is a car in traffic, LAG tells you what the car just behind you was doing.
Cheat Sheet:
LAG(expr) returns the previous row’s value in the ordered window.PARTITION BY resets the sequence for each group.ORDER BY defines what “previous” means.1; missing rows return NULL unless you supply a default.Practice Tasks:
LAG.PARTITION BY customer_id and verify the first row in each customer group resets correctly.ORDER BY and observe why the result becomes unreliable when timestamps repeat.