unbounded preceding over functions ejemplos ejemplo datos consultas complejas sql sql-server tsql running-total

preceding - sum over sql server



Calcular un total acumulado en SQL Server (14)

Aunque la mejor forma de hacerlo es usar una función de ventana, también se puede hacer usando una sub consulta simple correlacionada .

Select id, someday, somevalue, (select sum(somevalue) from testtable as t2 where t2.id = t1.id and t2.someday <= t1.someday) as runningtotal from testtable as t1 order by id,someday;

Imagine la siguiente tabla (llamada TestTable ):

id somedate somevalue -- -------- --------- 45 01/Jan/09 3 23 08/Jan/09 5 12 02/Feb/09 0 77 14/Feb/09 7 39 20/Feb/09 34 33 02/Mar/09 6

Me gustaría una consulta que devuelva un total acumulado en orden de fecha, como:

id somedate somevalue runningtotal -- -------- --------- ------------ 45 01/Jan/09 3 3 23 08/Jan/09 5 8 12 02/Feb/09 0 8 77 14/Feb/09 7 15 39 20/Feb/09 34 49 33 02/Mar/09 6 55

Sé que hay varias formas de hacerlo en SQL Server 2000/2005/2008.

Estoy particularmente interesado en este tipo de método que utiliza el truco de agregación de conjunto de declaraciones:

INSERT INTO @AnotherTbl(id, somedate, somevalue, runningtotal) SELECT id, somedate, somevalue, null FROM TestTable ORDER BY somedate DECLARE @RunningTotal int SET @RunningTotal = 0 UPDATE @AnotherTbl SET @RunningTotal = runningtotal = @RunningTotal + somevalue FROM @AnotherTbl

... esto es muy eficiente, pero he escuchado que hay problemas al respecto porque no necesariamente se puede garantizar que la instrucción UPDATE procesará las filas en el orden correcto. Tal vez podamos obtener algunas respuestas definitivas sobre ese tema.

Pero tal vez hay otras formas en que la gente puede sugerir?

editar: Ahora con un SqlFiddle con la configuración y el ejemplo de ''truco de actualización'' anterior


Creo que se puede lograr un total acumulado usando la simple operación INNER JOIN a continuación.

SELECT ROW_NUMBER() OVER (ORDER BY SomeDate) AS OrderID ,rt.* INTO #tmp FROM ( SELECT 45 AS ID, CAST(''01-01-2009'' AS DATETIME) AS SomeDate, 3 AS SomeValue UNION ALL SELECT 23, CAST(''01-08-2009'' AS DATETIME), 5 UNION ALL SELECT 12, CAST(''02-02-2009'' AS DATETIME), 0 UNION ALL SELECT 77, CAST(''02-14-2009'' AS DATETIME), 7 UNION ALL SELECT 39, CAST(''02-20-2009'' AS DATETIME), 34 UNION ALL SELECT 33, CAST(''03-02-2009'' AS DATETIME), 6 ) rt SELECT t1.ID ,t1.SomeDate ,t1.SomeValue ,SUM(t2.SomeValue) AS RunningTotal FROM #tmp t1 JOIN #tmp t2 ON t2.OrderID <= t1.OrderID GROUP BY t1.OrderID ,t1.ID ,t1.SomeDate ,t1.SomeValue ORDER BY t1.OrderID DROP TABLE #tmp


El operador APPLY en SQL 2005 y superior funciona para esto:

select t.id , t.somedate , t.somevalue , rt.runningTotal from TestTable t cross apply (select sum(somevalue) as runningTotal from TestTable where somedate <= t.somedate ) as rt order by t.somedate


En SQL Server 2012 puede usar SUM() con la cláusula OVER() .

select id, somedate, somevalue, sum(somevalue) over(order by somedate rows unbounded preceding) as runningtotal from TestTable

SQL Fiddle


Lo siguiente producirá los resultados requeridos.

SELECT a.SomeDate, a.SomeValue, SUM(b.SomeValue) AS RunningTotal FROM TestTable a CROSS JOIN TestTable b WHERE (b.SomeDate <= a.SomeDate) GROUP BY a.SomeDate,a.SomeValue ORDER BY a.SomeDate,a.SomeValue

Tener un índice agrupado en SomeDate mejorará en gran medida el rendimiento.


Si bien Sam Saffron hizo un gran trabajo al respecto, aún no proporcionó el código de expresión de tabla común recursivo para este problema. Y para nosotros que trabajamos con SQL Server 2008 R2 y no con Denali, sigue siendo la forma más rápida de obtener un total acumulado, es aproximadamente 10 veces más rápido que el cursor en mi computadora de trabajo para 100000 filas, y también es una consulta en línea.
Entonces, aquí está (supongo que hay una columna ord en la tabla y es un número secuencial sin espacios, para un procesamiento rápido también debería haber una restricción única sobre este número):

;with CTE_RunningTotal as ( select T.ord, T.total, T.total as running_total from #t as T where T.ord = 0 union all select T.ord, T.total, T.total + C.running_total as running_total from CTE_RunningTotal as C inner join #t as T on T.ord = C.ord + 1 ) select C.ord, C.total, C.running_total from CTE_RunningTotal as C option (maxrecursion 0) -- CPU 140, Reads 110014, Duration 132

demostración fiddle sql

actualización También tenía curiosidad acerca de esta actualización con actualización variable o peculiar . Por lo general, funciona bien, pero ¿cómo podemos estar seguros de que funciona todo el tiempo? bueno, aquí hay un pequeño truco (lo encontré aquí - http://www.sqlservercentral.com/Forums/Topic802558-203-21.aspx#bm981258 ) - solo verifica el orden actual y anterior y usa la asignación 1/0 en caso de que son diferentes de lo que esperas:

declare @total int, @ord int select @total = 0, @ord = -1 update #t set @total = @total + total, @ord = case when ord <> @ord + 1 then 1/0 else ord end, ------------------------ running_total = @total select * from #t -- CPU 0, Reads 58, Duration 139

Por lo que he visto, si tiene la clave principal / índice agrupado adecuada en su tabla (en nuestro caso, sería índice por ord_id ), la actualización procederá de forma lineal todo el tiempo (nunca se ha encontrado dividir por cero). Dicho eso, depende de usted decidir si desea usarlo en el código de producción :)


Si está utilizando Sql server 2008 R2 arriba. Entonces, sería la forma más corta de hacerlo;

Select id ,somedate ,somevalue, LAG(runningtotal) OVER (ORDER BY somedate) + somevalue AS runningtotal From TestTable

LAG se usa para obtener el valor de fila anterior. Puedes hacer google para obtener más información.

[1]:


Suponiendo que las ventanas funcionen en SQL Server 2008 como lo hace en cualquier otro lugar (eso lo he intentado), pruebe esto:

select testtable.*, sum(somevalue) over(order by somedate) from testtable order by somedate;

OVER() dice que está disponible en SQL Server 2008 (¿y quizás también en 2005?) Pero no tengo una instancia a mano para probarlo.

EDITAR: bueno, aparentemente SQL Server no permite una especificación de ventana ("OVER (...)") sin especificar "PARTITION BY" (dividiendo el resultado en grupos pero sin agregar de forma bastante similar a GROUP BY). Molesto: la referencia de sintaxis de MSDN sugiere que es opcional, pero solo tengo instancias de SqlServer 2000 por el momento.

La consulta que proporcioné funciona tanto en Oracle 10.2.0.3.0 como en PostgreSQL 8.4-beta. Así que dile a MS que se ponga al día;)




Use una subconsulta correlacionada. Muy simple, aquí tienes:

SELECT somedate, (SELECT SUM(somevalue) FROM TestTable t2 WHERE t2.somedate<=t1.somedate) AS running_total FROM TestTable t1 GROUP BY somedate ORDER BY somedate

Es posible que el código no sea exactamente correcto, pero estoy seguro de que la idea es correcta.

GROUP BY está en caso de que una fecha aparezca más de una vez; solo querrá verla una vez en el conjunto de resultados.

Si no le importa ver las fechas de repetición, o si desea ver el valor original y la identificación, entonces lo siguiente es lo que desea:

SELECT id, somedate, somevalue, (SELECT SUM(somevalue) FROM TestTable t2 WHERE t2.somedate<=t1.somedate) AS running_total FROM TestTable t1 ORDER BY somedate


Actualización , si está ejecutando SQL Server 2012, consulte: https://.com/a/10309947

El problema es que la implementación de SQL Server de la cláusula Over es algo limitada .

Oracle (y ANSI-SQL) le permiten hacer cosas como:

SELECT somedate, somevalue, SUM(somevalue) OVER(ORDER BY somedate ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS RunningTotal FROM Table

SQL Server no le brinda una solución limpia a este problema. Mi instinto me dice que este es uno de esos raros casos en los que un cursor es el más rápido, aunque tendré que hacer una evaluación comparativa de grandes resultados.

El truco de actualización es útil, pero creo que es bastante frágil. Parece que si está actualizando una tabla completa, procederá en el orden de la clave principal. Entonces, si configura su fecha como una clave primaria ascendente, probably estará a salvo. Pero está confiando en un detalle de implementación de SQL Server no documentado (también, si la consulta termina siendo ejecutada por dos procesadores, me pregunto qué sucederá, consulte: MAXDOP):

Muestra de trabajo completa:

drop table #t create table #t ( ord int primary key, total int, running_total int) insert #t(ord,total) values (2,20) -- notice the malicious re-ordering insert #t(ord,total) values (1,10) insert #t(ord,total) values (3,10) insert #t(ord,total) values (4,1) declare @total int set @total = 0 update #t set running_total = @total, @total = @total + total select * from #t order by ord ord total running_total ----------- ----------- ------------- 1 10 10 2 20 30 3 10 40 4 1 41

Usted pidió un punto de referencia, esta es la verdad.

La forma más segura de hacer esto sería el Cursor, es un orden de magnitud más rápido que la subconsulta correlacionada de combinación cruzada.

La manera más rápida absoluta es el truco de ACTUALIZAR. Mi única preocupación es que no estoy seguro de que bajo ninguna circunstancia la actualización proceda de forma lineal. No hay nada en la consulta que lo diga explícitamente.

En pocas palabras, para el código de producción iría con el cursor.

Datos de prueba:

create table #t ( ord int primary key, total int, running_total int) set nocount on declare @i int set @i = 0 begin tran while @i < 10000 begin insert #t (ord, total) values (@i, rand() * 100) set @i = @i +1 end commit

Prueba 1:

SELECT ord,total, (SELECT SUM(total) FROM #t b WHERE b.ord <= a.ord) AS b FROM #t a -- CPU 11731, Reads 154934, Duration 11135

Prueba 2:

SELECT a.ord, a.total, SUM(b.total) AS RunningTotal FROM #t a CROSS JOIN #t b WHERE (b.ord <= a.ord) GROUP BY a.ord,a.total ORDER BY a.ord -- CPU 16053, Reads 154935, Duration 4647

Prueba 3:

DECLARE @TotalTable table(ord int primary key, total int, running_total int) DECLARE forward_cursor CURSOR FAST_FORWARD FOR SELECT ord, total FROM #t ORDER BY ord OPEN forward_cursor DECLARE @running_total int, @ord int, @total int SET @running_total = 0 FETCH NEXT FROM forward_cursor INTO @ord, @total WHILE (@@FETCH_STATUS = 0) BEGIN SET @running_total = @running_total + @total INSERT @TotalTable VALUES(@ord, @total, @running_total) FETCH NEXT FROM forward_cursor INTO @ord, @total END CLOSE forward_cursor DEALLOCATE forward_cursor SELECT * FROM @TotalTable -- CPU 359, Reads 30392, Duration 496

Prueba 4:

declare @total int set @total = 0 update #t set running_total = @total, @total = @total + total select * from #t -- CPU 0, Reads 58, Duration 139


BEGIN TRAN CREATE TABLE #Table (_Id INT IDENTITY(1,1) ,id INT , somedate VARCHAR(100) , somevalue INT) INSERT INTO #Table ( id , somedate , somevalue ) SELECT 45 , ''01/Jan/09'', 3 UNION ALL SELECT 23 , ''08/Jan/09'', 5 UNION ALL SELECT 12 , ''02/Feb/09'', 0 UNION ALL SELECT 77 , ''14/Feb/09'', 7 UNION ALL SELECT 39 , ''20/Feb/09'', 34 UNION ALL SELECT 33 , ''02/Mar/09'', 6 ;WITH CTE ( _Id, id , _somedate , _somevalue ,_totvalue ) AS ( SELECT _Id , id , somedate , somevalue ,somevalue FROM #Table WHERE _id = 1 UNION ALL SELECT #Table._Id , #Table.id , somedate , somevalue , somevalue + _totvalue FROM #Table,CTE WHERE #Table._id > 1 AND CTE._Id = ( #Table._id-1 ) ) SELECT * FROM CTE ROLLBACK TRAN


SELECT TOP 25 amount, (SELECT SUM(amount) FROM time_detail b WHERE b.time_detail_id <= a.time_detail_id) AS Total FROM time_detail a

También puede usar la función ROW_NUMBER () y una tabla temporal para crear una columna arbitraria para usar en la comparación en la instrucción SELECT interna.