¿Hay alguna forma de acceder al valor de la "fila anterior" en una instrucción SELECT?

93

Necesito calcular la diferencia de una columna entre dos líneas de una tabla. ¿Hay alguna forma de que pueda hacer esto directamente en SQL? Estoy usando Microsoft SQL Server 2008.

Estoy buscando algo como esto:

SELECT value - (previous.value) FROM table

Imaginando que la variable "anterior" hace referencia a la última fila seleccionada. Por supuesto, con una selección como esa, terminaré con n-1 filas seleccionadas en una tabla con n filas, eso no es probable, en realidad es exactamente lo que necesito.

¿Es eso posible de alguna manera?

Edwin Jarvis
fuente
6
Bueno, solo agrego un comentario útil para nuevos espectadores. SQL 2012 tiene LAG y LEAD ahora :) Consulte este enlace blog.sqlauthority.com/2013/09/22/…
KD

Respuestas:

61

SQL no tiene una noción incorporada de orden, por lo que debe ordenar por alguna columna para que esto sea significativo. Algo como esto:

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

Si sabe cómo ordenar las cosas pero no cómo obtener el valor anterior dado el actual (por ejemplo, desea ordenar alfabéticamente), entonces no conozco una forma de hacerlo en SQL estándar, pero la mayoría de las implementaciones de SQL tendrán extensiones para hacerlo.

Aquí hay una forma para el servidor SQL que funciona si puede ordenar filas de manera que cada una sea 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

Si necesita romper los lazos, puede agregar tantas columnas como sea necesario al ORDER BY.

RossFabricante
fuente
Está bien, el orden no es un problema, simplemente lo eliminé del ejemplo para hacerlo más simple, lo intentaré.
Edwin Jarvis
7
que asume que las claves primarias se generan secuencialmente y las filas nunca se eliminan y la selección no tiene ninguna otra cláusula de orden y y y ...
MartinStettner
Martin tiene razón. Aunque esto podría funcionar en algunos casos, realmente necesita definir exactamente lo que quiere decir con "anterior" en un sentido comercial, preferiblemente sin depender de una identificación generada.
Tom H
Tienes razón, agregué una mejora usando una extensión de SQL Server.
RossFabricant
2
En respuesta a "Está bien, el orden no es un problema" ... Entonces, ¿por qué no restas un valor arbitrario en tu consulta, ya que eso es lo que estás haciendo si no consideras el orden?
JohnFx
79

Utilice la función de retraso :

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

Las secuencias utilizadas para los ID pueden omitir valores, por lo que Id-1 no siempre funciona.

Hans Ginzel
fuente
1
Esta es la solución PostgreSQL. La pregunta es sobre MSSQL. MSSQL tiene dicha función en las versiones 2012+ ( msdn.microsoft.com/en-us/en-en/library/hh231256(v=sql.120).aspx )
Kromster
10
@KromStern No solo la solución PostgreSQL. Las funciones de la ventana SQL se introdujeron en el estándar SQL: 2003 .
Hans Ginzel
La función LAG puede tomar tres parámetros: LAG(ExpressionToSelect, NumberOfRowsToLag, DefaultValue). El número predeterminado de filas para retrasar es 1, pero puede especificar eso y el valor predeterminado para seleccionar cuando no es posible retrasar ya que está al comienzo del conjunto.
vaindil
29

Oracle, PostgreSQL, SQL Server y muchos más motores RDBMS tienen funciones analíticas llamadas LAGy LEADque hacen esto mismo.

En SQL Server antes de 2012, deberá hacer lo siguiente:

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

, dónde COL1 está la columna por la que está ordenando.

Tener un índice (COL1, PK)activado mejorará enormemente esta consulta.

Quassnoi
fuente
14
SQL Server 2012 ahora también tiene LAG y LEAD.
ErikE
El script Hana SQL también es compatible con LAG y LEAD.
mik
Solo para agregar otro comentario a los espectadores que llegaron aquí buscando hacer eso en Hive. También tiene funciones LAG y LEAD. Documentación aquí: 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
fuente
Funciona correctamente si no hay agrupación en la consulta, pero ¿qué pasa si queremos restar valores del valor anterior solo dentro de un grupo, digamos el mismo ID de empleado, entonces cómo podemos hacer eso? Porque ejecutar esto funciona solo para las 2 filas superiores de cada grupo y no para el resto de las filas de ese grupo. Para esto, usé ejecutar este código en un bucle while, pero parece ser muy lento. ¿Algún otro enfoque que pudiéramos en este escenario? ¿Y eso también solo en SQL Server 2008?
Hemant Sisodia
10

LEFT JOIN la tabla consigo misma, con la condición de unión resuelta de modo que la fila coincidente en la versión unida de la tabla sea una fila anterior, para su definición particular de "anterior".

Actualización: Al principio estaba pensando que querría mantener todas las filas, con NULL para la condición donde no había una fila anterior. Al volver a leerlo, solo desea que se eliminen las filas, por lo que debe usar una combinación interna en lugar de una combinación izquierda.


Actualizar:

Las versiones más nuevas de Sql Server también tienen las funciones LAG y LEAD Windowing que también se pueden usar para esto.

Joel Coehoorn
fuente
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
fuente
2

La respuesta seleccionada solo funcionará si no hay espacios en la secuencia. Sin embargo, si está utilizando una identificación generada automáticamente, es probable que haya espacios en la secuencia debido a inserciones que se revertieron.

Este método debería funcionar si tiene lagunas

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
fuente