Existe uma maneira de acessar o valor da “linha anterior” em uma instrução SELECT?

93

Preciso calcular a diferença de uma coluna entre duas linhas de uma tabela. Existe alguma maneira de fazer isso diretamente no SQL? Estou usando o Microsoft SQL Server 2008.

Estou procurando algo assim:

SELECT value - (previous.value) FROM table

Imaginando que a variável "anterior" referencia a última linha selecionada. É claro que com um select como esse vou acabar com n-1 linhas selecionadas em uma tabela com n linhas, isso não é provável, na verdade é exatamente o que preciso.

Isso é possível de alguma forma?

Edwin Jarvis
fonte
6
Bem, apenas adicionando um comentário útil para novos visualizadores ainda. SQL 2012 tem LAG e LEAD agora :) Consulte este link blog.sqlauthority.com/2013/09/22/…
KD

Respostas:

61

O SQL não tem uma noção de ordem embutida, então você precisa ordenar por alguma coluna para que isso seja significativo. Algo assim:

select t1.value - t2.value from table t1, table t2 
where t1.primaryKey = t2.primaryKey - 1

Se você sabe como ordenar as coisas, mas não como obter o valor anterior dado o atual (por exemplo, você quer ordenar alfabeticamente), então não conheço uma maneira de fazer isso no SQL padrão, mas a maioria das implementações de SQL terá extensões para fazer isso.

Esta é uma maneira de o servidor SQL funcionar se você puder ordenar as linhas de forma que cada uma seja distinta:

select  rank() OVER (ORDER BY id) as 'Rank', value into temp1 from t

select t1.value - t2.value from temp1 t1, temp1 t2 
where t1.Rank = t2.Rank - 1

drop table temp1

Se você precisar quebrar empate, você pode adicionar quantas colunas forem necessárias ao ORDER BY.

RossFabricant
fonte
Tudo bem, a ordem não é um problema, apenas removi do exemplo para torná-lo mais simples, vou tentar isso.
Edwin Jarvis,
7
que assume que as chaves primárias são geradas sequencialmente e as linhas nunca são excluídas e o select não tem nenhuma outra cláusula de pedido
eee
Martin está correto. Embora isso possa funcionar em alguns casos, você realmente precisa definir exatamente o que entende por "anterior" no sentido comercial, de preferência sem depender de um ID gerado.
Tom H,
Você está certo, adicionei uma melhoria usando uma extensão do SQL Server.
RossFabricant,
2
Em resposta a "Tudo bem, a ordem não é um problema" ... Então por que você simplesmente não subtrai um valor arbitrário em sua consulta, já que é isso que você está fazendo se não considera a ordem?
JohnFx,
79

Use a função lag :

SELECT value - lag(value) OVER (ORDER BY Id) FROM table

As sequências usadas para Ids podem ignorar valores, portanto, Id-1 nem sempre funciona.

Hans Ginzel
fonte
1
Esta é a solução PostgreSQL. A questão é sobre MSSQL. MSSQL tem essa função nas versões 2012+ ( msdn.microsoft.com/en-us/en-en/library/hh231256(v=sql.120).aspx )
Kromster
10
@KromStern Não apenas a solução PostgreSQL. As funções da janela SQL foram introduzidas no padrão SQL: 2003 .
Hans Ginzel
A função LAG pode tomar três parâmetros: LAG(ExpressionToSelect, NumberOfRowsToLag, DefaultValue). O número padrão de linhas para atrasar é 1, mas você pode especificar isso e o valor padrão para selecionar quando não for possível atrasar, pois você está no início do conjunto.
vaindil
29

Oracle, PostgreSQL, SQL Server e muitos outros motores RDBMS têm funções analíticas chamadas LAGe LEADque fazem exatamente isso.

No SQL Server antes de 2012, você precisaria fazer o seguinte:

SELECT  value - (
        SELECT  TOP 1 value
        FROM    mytable m2
        WHERE   m2.col1 < m1.col1 OR (m2.col1 = m1.col1 AND m2.pk < m1.pk)
        ORDER BY 
                col1, pk
        )
FROM mytable m1
ORDER BY
      col1, pk

, onde COL1é a coluna pela qual você está ordenando.

Ter um índice (COL1, PK)melhorará muito esta consulta.

Quassnoi
fonte
14
O SQL Server 2012 agora tem LAG e LEAD também.
ErikE
O script Hana SQL também oferece suporte a LAG e LEAD.
mik
Só para adicionar outro comentário aos visualizadores que chegaram aqui procurando fazer isso no Hive. Também possui funções LAG e LEAD. Documentação aqui: cwiki.apache.org/confluence/display/Hive/…
Jaime Caffarel
27
WITH CTE AS (
  SELECT
    rownum = ROW_NUMBER() OVER (ORDER BY columns_to_order_by),
    value
  FROM table
)
SELECT
  curr.value - prev.value
FROM CTE cur
INNER JOIN CTE prev on prev.rownum = cur.rownum - 1
Jeremy Stein
fonte
Funciona corretamente se não houver agrupamento na consulta, mas e se quisermos subtrair valores do valor anterior apenas dentro de um grupo, digamos mesmo EmployeeID, então como podemos fazer isso? Porque executar isso funciona apenas para as 2 primeiras linhas de cada grupo e não para o resto das linhas desse grupo. Para isso, usei a execução desse código em loop while, mas parece ser muito lento. Qualquer outra abordagem que poderíamos neste cenário? E isso também apenas no SQL Server 2008?
Hemant Sisodia
10

LEFT JOIN a tabela para si mesma, com a condição de junção resolvida de forma que a linha correspondida na versão junta da tabela esteja uma linha anterior, para sua definição particular de "anterior".

Update: A princípio, pensei que você gostaria de manter todas as linhas, com NULLs para a condição em que não havia linha anterior. Ao lê-lo novamente, você só deseja que as linhas sejam eliminadas, então você deve uma junção interna em vez de uma junção à esquerda.


Atualizar:

As versões mais recentes do Sql Server também têm as funções LAG e LEAD Windowing que podem ser usadas para isso também.

Joel Coehoorn
fonte
3
select t2.col from (
select col,MAX(ID) id from 
(
select ROW_NUMBER() over(PARTITION by col order by col) id ,col from testtab t1) as t1
group by col) as t2
user1920851
fonte
2

A resposta selecionada só funcionará se não houver lacunas na sequência. No entanto, se você estiver usando um id gerado automaticamente, é provável que haja lacunas na sequência devido a inserções que foram revertidas.

Este método deve funcionar se você tiver lacunas

declare @temp (value int, primaryKey int, tempid int identity)
insert value, primarykey from mytable order by  primarykey

select t1.value - t2.value from @temp  t1
join @temp  t2 
on t1.tempid = t2.tempid - 1
HLGEM
fonte