SQL LAG e LEAD

Num SELECT comum, cada linha só enxerga a si mesma. Para saber "este pedido foi maior ou menor que o anterior?" ou "quantos dias o cliente demorou para voltar?", a linha precisa espiar a vizinha. É exatamente o que fazem duas funções de janela: LAG traz um valor da linha anterior, e LEAD, da linha seguinte. Nesta aula a Ana usa as duas para comparar pedidos, acompanhar as vendas mês a mês e medir, em dias, o intervalo entre as compras dos clientes.

Você vai aprender
  • LAG(coluna): o valor da linha anterior; LEAD(coluna): o da seguinte.
  • Comparar pedido a pedido e calcular a variação mês a mês.
  • Os dias entre um pedido e outro, com julianday.
  • Pular mais de uma linha (LAG(x, 2)) e trocar o NULL da primeira linha por um valor padrão.
  • LAG × subconsulta × self-join, e por que o self-join pelo id engana.
  • Os erros mais comuns, com a mensagem real do SQLite.

Esta aula continua as de Funções de Janela (OVER) e ROW_NUMBER, RANK e DENSE_RANK. Todos os exemplos rodam de verdade no seu navegador, no banco da Loja Aurora.


A ideia: o extrato do banco

Pense no extrato da sua conta: uma linha por dia, com o saldo. Para saber se você gastou mais hoje do que ontem, você olha a linha de cima. Para saber quanto tempo falta para o próximo depósito, olha a de baixo. Você não junta as linhas, não soma nada: só espia a vizinha.

No extratoNo SQLTradução
Olhar a linha de cimaLAG(coluna) OVER (ORDER BY …)lag = "atraso", "ficar para trás"
Olhar a linha de baixoLEAD(coluna) OVER (ORDER BY …)lead = "dianteira", "ir na frente"
A ordem do extrato (por data)o ORDER BY dentro do OVERsem ordem, "a de cima" não quer dizer nada

1. O pedido anterior e o seguinte

O José Ribamar Silva (cliente 1) fez 4 pedidos. Para cada um, a data do pedido anterior e a do seguinte dele:

SELECT id,
       data,
       LAG(data)  OVER (ORDER BY data) AS anterior,
       LEAD(data) OVER (ORDER BY data) AS seguinte
FROM pedidos
WHERE cliente_id = 1
ORDER BY data;
LAG(data)
"O valor de data na linha anterior". A coluna entre parênteses é o que buscar na vizinha.
LEAD(data)
O mesmo, na linha seguinte.
OVER (ORDER BY data)
Define quem é "anterior" e "seguinte": a ordem das datas. É a parte mais importante; sem ela, a vizinhança é a ordem que o SQLite quiser.
WHERE cliente_id = 1
O WHERE roda antes da janela (aula Funções de Janela): a janela só enxerga os 4 pedidos do José. Por isso o "anterior" do pedido 27 é o pedido 1 dele, e não o pedido 26 da loja.

Repare nas pontas: o primeiro pedido não tem anterior, e o último não tem seguinte. Nesses lugares a função devolve NULL. Você vai ver na seção 4 como trocar esse NULL por outra coisa.

Missão: agora a Ana Beatriz Sousa (cliente 3). Mostre os pedidos dela em ordem de data, com as colunas id, data e data_anterior (a data do pedido anterior dela).

SELECT id, data
FROM pedidos
WHERE cliente_id = 3
ORDER BY data;
SELECT id,
       data,
       LAG(data) OVER (ORDER BY data) AS data_anterior
FROM pedidos
WHERE cliente_id = 3
ORDER BY data;

2. Comparar com o anterior: a diferença

Com o valor anterior na mesma linha, é só fazer a conta. Os pedidos de julho em diante de 2026, cada um comparado com o pedido anterior da loja. O valor de cada pedido vem de uma CTE (aula CTE (WITH)):

WITH totais AS (
  SELECT p.id, p.data,
         ROUND(SUM(i.quantidade * i.preco_unitario), 2) AS valor
  FROM pedidos AS p
  JOIN itens_pedido AS i ON i.pedido_id = p.id
  WHERE p.data >= '2026-07-01'
  GROUP BY p.id
)
SELECT id,
       data,
       valor,
       LAG(valor) OVER (ORDER BY data) AS anterior,
       ROUND(valor - LAG(valor) OVER (ORDER BY data), 2) AS diferenca
FROM totais
ORDER BY data;
LAG(valor) OVER (ORDER BY data)
O valor do pedido anterior, em ordem de data.
valor - LAG(valor) OVER (…)
Positivo: este pedido foi maior que o anterior. Negativo: menor. Na primeira linha, qualquer conta com NULL dá NULL (aula O Valor NULL).

O pedido 88, de 5 de outubro, foi R$ 474,97 maior que o anterior: é um dos maiores pedidos da história da loja (R$ 619,47).

A variação mês a mês

O mesmo truque com as vendas de cada mês de 2025, agora em porcentagem: "quanto este mês subiu ou caiu em relação ao anterior". A mesma janela aparece três vezes, então ela ganha nome com WINDOW:

WITH mensal AS (
  SELECT strftime('%Y-%m', p.data) AS mes,
         ROUND(SUM(i.quantidade * i.preco_unitario), 2) AS vendas
  FROM pedidos AS p
  JOIN itens_pedido AS i ON i.pedido_id = p.id
  WHERE p.data LIKE '2025-%'
  GROUP BY mes
)
SELECT mes,
       vendas,
       LAG(vendas) OVER m AS mes_anterior,
       ROUND(100.0 * (vendas - LAG(vendas) OVER m) / LAG(vendas) OVER m, 1) AS variacao_pct
FROM mensal
WINDOW m AS (ORDER BY mes)
ORDER BY mes;
WINDOW m AS (ORDER BY mes)
A janela com nome, para não repetir OVER (ORDER BY mes) três vezes.
100.0 * (vendas - anterior) / anterior
A conta da porcentagem de variação: a diferença dividida pelo valor de antes.

Abril caiu 95,7% (de R$ 579,07 para R$ 25,13), e por isso maio aparece com +3119,6%: sair de quase nada para R$ 809,09 é muitas vezes mais. Variações em porcentagem enganam quando a base é pequena; mostre sempre o valor junto.


3. Os dias entre um pedido e outro: julianday

A pergunta da Ana agora é sobre tempo: "de quantos em quantos dias o José volta a comprar?". O primeiro impulso é subtrair as datas direto, mas isso não funciona (veja em Quando dá errado). As datas da Aurora são texto no formato AAAA-MM-DD; para fazer conta, transforme cada uma num número de dias com julianday (aula Funções de Data):

SELECT julianday('2024-01-05') AS dia_juliano,
       julianday('2024-10-29') - julianday('2024-01-05') AS diferenca;
julianday('2024-01-05')
O "dia juliano": quantos dias se passaram desde uma data muito antiga (no ano 4713 antes de Cristo), uma contagem usada na astronomia. O .5 no fim é porque o dia juliano começa ao meio-dia; a meia-noite de uma data fica no meio.
julianday(…) - julianday(…)
A diferença entre dois dias julianos é o número de dias entre as datas: 298 dias entre 5 de janeiro e 29 de outubro de 2024. Sai com .0 porque é um número real.

Agora junte com o LAG: em cada pedido, os dias desde o pedido anterior do mesmo cliente.

SELECT id,
       data,
       LAG(data) OVER (ORDER BY data) AS anterior,
       CAST(julianday(data) - julianday(LAG(data) OVER (ORDER BY data)) AS INTEGER) AS dias
FROM pedidos
WHERE cliente_id = 1
ORDER BY data;
julianday(LAG(data) OVER (…))
A data do pedido anterior, virada em número de dias. A janela vai dentro da função, sem problema.
CAST(… AS INTEGER)
Tira o .0: 298 em vez de 298.0 (aula Os Tipos no SQLite).

O José compra mais ou menos a cada 300 dias: uma vez por ano, quase certinho.

LEAD: quanto tempo até o próximo

Com LEAD, a conta olha para a frente. Os pedidos da loja desde setembro de 2026 e quantos dias se passaram até o próximo pedido chegar:

SELECT id,
       data,
       LEAD(data) OVER (ORDER BY data) AS proximo,
       CAST(julianday(LEAD(data) OVER (ORDER BY data)) - julianday(data) AS INTEGER) AS espera
FROM pedidos
WHERE data >= '2026-09-01'
ORDER BY data;

O último pedido (90, de 14 de outubro) ainda não tem próximo: NULL.

De quantos em quantos dias a loja recebe pedido?

Calcule o intervalo de cada pedido para o anterior (agora na loja inteira) numa CTE, e resuma fora dela. Não dá para pôr AVG(LAG(…)) direto: uma agregação não pode conter uma janela.

WITH intervalos AS (
  SELECT id,
         julianday(data) - julianday(LAG(data) OVER (ORDER BY data)) AS dias
  FROM pedidos
)
SELECT ROUND(AVG(dias), 1) AS media_de_dias,
       MIN(dias) AS menor,
       MAX(dias) AS maior
FROM intervalos;

Em média, um pedido a cada 11,4 dias; nunca menos de 4 dias de intervalo, nunca mais de 18. O AVG, o MIN e o MAX ignoram o NULL do primeiro pedido (aula Funções Agregadas).


4. Pular mais de uma linha e o valor padrão

LAG e LEAD aceitam mais dois valores entre os parênteses:

LAG(coluna, quantas_linhas, valor_se_nao_houver)
SELECT id,
       data,
       LAG(data, 1, 'primeiro pedido') OVER (ORDER BY data) AS anterior,
       LAG(data, 2) OVER (ORDER BY data) AS dois_antes
FROM pedidos
WHERE cliente_id = 1
ORDER BY data;
LAG(data, 1, 'primeiro pedido')
Uma linha para trás (o padrão é 1) e, quando não houver linha anterior, o texto 'primeiro pedido' no lugar do NULL.
LAG(data, 2)
Duas linhas para trás. Os dois primeiros pedidos não têm "dois antes", e ficam NULL.
Valor padrão ou COALESCE?

O terceiro valor do LAG só entra quando não existe linha anterior. Se a linha anterior existe mas a coluna nela é NULL, o LAG devolve esse NULL. Para trocar os dois casos, use COALESCE(LAG(x) OVER (…), 'valor') (aula IFNULL e COALESCE).


5. Formas diferentes de fazer a mesma coisa

Antes das funções de janela, havia dois jeitos de achar "o pedido anterior". Um funciona; o outro engana.

a) Subconsulta correlacionada

SELECT p.id,
       p.data,
       (SELECT MAX(p2.data)
        FROM pedidos AS p2
        WHERE p2.cliente_id = 1
          AND p2.data < p.data) AS anterior
FROM pedidos AS p
WHERE p.cliente_id = 1
ORDER BY p.data;

"A maior data do José que seja menor que a deste pedido." Dá o mesmo que o LAG, mas a condição do cliente aparece duas vezes, a subconsulta roda uma vez por linha, e buscar outra coluna do pedido anterior (o valor, o id) exige outra subconsulta.

b) Self-join pelo id: a armadilha

A ideia parece boa: "o pedido anterior é o de id um a menos" (aula Self Join):

SELECT p.id,
       p.data,
       a.data AS anterior
FROM pedidos AS p
LEFT JOIN pedidos AS a ON a.id = p.id - 1
WHERE p.cliente_id = 1
ORDER BY p.data;

Compare com o resultado da seção 1: as datas "anteriores" estão erradas. O pedido 26 é o anterior da loja, de outro cliente, e não o anterior do José. E mesmo para a loja inteira, isso só funciona enquanto os id estiverem em ordem de data e sem buracos (um pedido apagado já quebra a conta). O LAG não depende de nada disso: ele segue o ORDER BY que você mandar.

FormaFunciona?Quando usar
LAG / LEADSim, seguindo o ORDER BY da janelaSempre que o SQLite for 3.25 ou mais novo
Subconsulta com MAX(data) … WHERE data < …Sim, mas repete condições e roda por linhaNum banco sem funções de janela
Self-join com id - 1Só por sorte: depende dos ids em sequênciaEvite
E para todos os clientes de uma vez?

Nesta aula, o WHERE cliente_id = 1 separou um cliente. Para calcular "o pedido anterior do mesmo cliente" para todos os 26 clientes numa consulta só, a janela precisa recomeçar a cada cliente. É o assunto da próxima aula: PARTITION BY.


Quando dá errado

Subtrair as datas direto
SELECT id,
       data,
       data - LAG(data) OVER (ORDER BY data) AS dias
FROM pedidos
WHERE cliente_id = 1;

O que acontece: nenhum erro, e a coluna dias mostra 0, 1, 1. Para fazer a conta de menos, o SQLite transforma cada texto num número lendo só o começo dele, até onde ele parece número: '2024-10-29' vira 2024. Então '2024-10-29' - '2024-01-05' é 2024 - 2024 = 0, e '2025-09-03' - '2024-10-29' é 1. É a diferença em anos (e só do número do ano), não em dias.

Como consertar: julianday(data) - julianday(LAG(data) OVER (…)).

Data fora do formato ISO
SELECT julianday('05/01/2024') AS brasileiro,
       julianday('2024-1-5') AS sem_zeros,
       julianday('2024-01-05') AS iso;

O que acontece: as duas primeiras dão NULL, sem erro nenhum. O julianday só entende datas no formato AAAA-MM-DD, com os zeros. Num banco com datas em outro formato, todas as diferenças saem NULL.

Como consertar: guarde datas sempre como AAAA-MM-DD (aula Datas e Horas). As da Loja Aurora já estão assim.

Esqueceu o OVER
SELECT id, LAG(data) AS anterior
FROM pedidos
WHERE cliente_id = 1;

A mensagem: misuse of window function LAG(). LAG e LEAD só existem como janela.

Como consertar: LAG(data) OVER (ORDER BY data).

Esqueceu a coluna
SELECT id, LAG() OVER (ORDER BY data) AS anterior
FROM pedidos
WHERE cliente_id = 1;

A mensagem: wrong number of arguments to function LAG(). Ao contrário de ROW_NUMBER(), o LAG precisa saber o que buscar na linha vizinha.

Como consertar: diga a coluna: LAG(data), LAG(valor)…

LAG no WHERE

A Ana quis mostrar só os pedidos feitos com menos de 7 dias de intervalo:

SELECT id, data
FROM pedidos
WHERE julianday(data) - julianday(LAG(data) OVER (ORDER BY data)) < 7;

A mensagem: misuse of window function LAG(). Como toda janela, o LAG é calculado depois do WHERE.

Como consertar: calcule o intervalo numa CTE, como em "De quantos em quantos dias", e filtre WHERE dias < 7 fora dela.

Sem erro, mas sem garantia: esqueceu o ORDER BY da janela

LAG(data) OVER () roda, e muitas vezes até parece certo, porque o SQLite costuma ler a tabela na ordem do id. Mas "anterior" sem ordem quer dizer "a linha que o SQLite leu antes", e isso pode mudar com um índice novo ou um WHERE diferente.

Como consertar: diga sempre a ordem: OVER (ORDER BY data), e se houver datas repetidas, desempate: OVER (ORDER BY data, id).

Nos outros bancos
  • LAG e LEAD, com os mesmos três valores (coluna, quantas linhas, valor padrão), existem no PostgreSQL, no SQL Server (a partir do 2012) e no MySQL (a partir da 8.0).
  • A conta de dias é que muda, porque cada banco tem o seu tipo de data. No PostgreSQL, subtrair dois valores do tipo date já dá o número de dias. No MySQL, use DATEDIFF(data, anterior); no SQL Server, DATEDIFF(day, anterior, data) (repare que a ordem dos argumentos é diferente nos dois). O julianday é do SQLite.

Trechos só ilustrativos (não rodam no SQLite):

-- MySQL
SELECT id, DATEDIFF(data, LAG(data) OVER (ORDER BY data)) AS dias FROM pedidos;
-- SQL Server
SELECT id, DATEDIFF(day, LAG(data) OVER (ORDER BY data), data) AS dias FROM pedidos;
-- PostgreSQL (com a coluna data do tipo date)
SELECT id, data - LAG(data) OVER (ORDER BY data) AS dias FROM pedidos;

Referência rápida

EscritaO que faz
LAG(x) OVER (ORDER BY y)O valor de x na linha anterior (na ordem de y)
LEAD(x) OVER (ORDER BY y)O valor de x na linha seguinte
LAG(x, 2)Duas linhas para trás
LAG(x, 1, 'padrão')Valor quando não há linha anterior
x - LAG(x) OVER (…)A diferença para a linha anterior
julianday(data) - julianday(LAG(data) OVER (…))Dias desde a linha anterior
CAST(… AS INTEGER)Tira o .0 do número de dias
MensagemQuer dizer
misuse of window function LAG()Faltou o OVER, ou o LAG está no WHERE
wrong number of arguments to function LAG()Faltou a coluna: LAG(data)
(dias saindo 0 e 1)Subtraiu as datas como texto; use julianday
(dias saindo NULL em tudo)Datas fora do formato AAAA-MM-DD

Exercícios

1. A linha anterior

Complete a função que busca o valor da linha anterior:

SELECT data, (frete) OVER (ORDER BY data) AS frete_anterior FROM pedidos;

2. Os dias

Complete a função que transforma a data em número de dias, para a subtração funcionar:

SELECT (entregue_em) - julianday(data) AS dias_para_entregar FROM pedidos;

3. Num LEAD(data) OVER (ORDER BY data), o que aparece na última linha?

4. A Ana fez data - LAG(data) OVER (ORDER BY data) e recebeu 0 e 1. Por quê?

5. Por que LEFT JOIN pedidos AS a ON a.id = p.id - 1 não acha o pedido anterior de um cliente?

Missão final: o intervalo da Ana Beatriz

Missão: de quantos em quantos dias a Ana Beatriz Sousa (cliente 3) volta a comprar? Mostre a data de cada pedido dela e a coluna dias: os dias desde o pedido anterior dela, como número inteiro.

SELECT data
FROM pedidos
WHERE cliente_id = 3
ORDER BY data;
SELECT data,
       CAST(julianday(data) - julianday(LAG(data) OVER (ORDER BY data)) AS INTEGER) AS dias
FROM pedidos
WHERE cliente_id = 3
ORDER BY data;

299, 309 e 299 dias: como o José, a Ana Beatriz compra uma vez por ano. A primeira linha fica com NULL, porque não há pedido anterior a ela.


Resumo

  • LAG(x) traz o valor de x da linha anterior; LEAD(x), da seguinte. As duas são funções de janela: OVER (ORDER BY …) diz quem é vizinho de quem.
  • Nas pontas, sem vizinha, o resultado é NULL. LAG(x, 1, 'padrão') troca esse NULL; LAG(x, 2) pula duas linhas.
  • Com o valor vizinho na mesma linha, você calcula diferenças e variações: x - LAG(x) OVER (…).
  • Datas são texto: para contar dias, julianday(data) - julianday(LAG(data) OVER (…)), com CAST(… AS INTEGER) para tirar o .0. Subtrair o texto direto dá a diferença dos anos.
  • Para filtrar ou resumir pelo resultado do LAG, calcule numa CTE primeiro.
  • O self-join pelo id - 1 só acerta por sorte; o LAG segue a ordem que você pedir.
  • Próxima aula: a janela que recomeça em cada grupo, para fazer tudo isso por cliente e por categoria de uma vez: PARTITION BY.