Calcular un total acumulado en SQL Server


170

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-conjunto-declaración:

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 oído que hay problemas en torno a esto porque no necesariamente puede garantizar que la UPDATEinstrucción procesará las filas en el orden correcto. Tal vez podamos obtener algunas respuestas definitivas sobre ese tema.

¿Pero quizás 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


blogs.msdn.com/sqltips/archive/2005/07/20/441053.aspx Agregue un pedido a su actualización ... configúrelo y obtendrá una garantía.
Simon D

Pero Ordenar por no se puede aplicar a una declaración ACTUALIZAR ... ¿o sí?
codeulike 01 de

Consulte también sqlperformance.com/2012/07/t-sql-queries/running-totals, especialmente si está utilizando SQL Server 2012.
Aaron Bertrand

Respuestas:


133

Actualización , si está ejecutando SQL Server 2012, consulte: https://stackoverflow.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 ofrece una solución limpia a este problema. Mi instinto me dice que este es uno de esos casos raros en los que un cursor es el más rápido, aunque tendré que hacer algunos puntos de referencia para obtener grandes resultados.

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

Muestra de trabajo completo:

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 solicitó un punto de referencia, este es el detalle.

La forma SEGURA más rápida de hacer esto sería el cursor, es un orden de magnitud más rápido que la subconsulta correlacionada de unión cruzada.

La forma más rápida es el truco ACTUALIZAR. Mi única preocupación es que no estoy seguro de que, en todas las circunstancias, la actualización se realice 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

1
Gracias. Supongo que su ejemplo de código es para demostrar que se sumará en el orden de la clave primaria. Sería interesante saber si los cursores son aún más eficientes que las combinaciones para conjuntos de datos más grandes.
codeulike

1
Acabo de probar el CTE @Martin, nada se acerca al truco de actualización: el cursor parece más bajo en las lecturas. Aquí hay un seguimiento del perfilador i.stack.imgur.com/BbZq3.png
Sam Saffron

3
@Martin Denali tendrá una solución bastante buena para este msdn.microsoft.com/en-us/library/ms189461(v=SQL.110).aspx
Sam Saffron

1
+1 por todo el trabajo realizado en esta respuesta: me encanta la opción ACTUALIZAR; ¿Se puede construir una partición en este script de ACTUALIZACIÓN? por ejemplo, si hubiera un campo adicional "Color del automóvil", ¿podría este script devolver totales acumulados dentro de cada partición de "Color del automóvil"?
whytheq

2
la respuesta inicial (Oracle (y ANSI-SQL)) ahora funciona en SQL Server 2017. ¡Gracias, muy elegante!
DaniDev


40

Si bien Sam Saffron hizo un gran trabajo al respecto, aún no proporcionó un 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 ejecutar el total, 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 ordcolumna en la tabla y su número secuencial sin espacios, para un procesamiento rápido también debería haber una restricción única en 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

sql fiddle demo

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 siempre? bueno, aquí hay un pequeño truco (lo encontró aquí: http://www.sqlservercentral.com/Forums/Topic802558-203-21.aspx#bm981258 ): simplemente verifique la asignación actual y anterior ordy use la 1/0asignación en caso de que sean diferentes de lo que usted espera:

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 una clave primaria / índice agrupada adecuada en su tabla (en nuestro caso sería indexar por ord_id ), la actualización continuará de forma lineal todo el tiempo (nunca se encontró dividir por cero). Dicho esto, depende de usted decidir si desea usarlo en el código de producción :)

actualización 2 Estoy vinculando esta respuesta, porque incluye información útil sobre la falta de fiabilidad de la actualización peculiar: concatenación / índice / nvarchar (max) comportamiento inexplicable de nvarchar .


66
Esta respuesta merece más reconocimiento (o tal vez tiene algún defecto, que no veo?)
user1068352

debe haber un número secuencial para que pueda unirse en ord = ord + 1 y, a veces, necesita un poco más de trabajo. Pero de todos modos, en SQL 2008 R2 estoy usando esta solución
Roman Pekar

+1 En SQLServer2008R2 también prefiero el enfoque con CTE recursivo. Para su información, para encontrar el valor de las tablas, que permiten huecos, uso una subconsulta correlacionada. Agrega dos operaciones de búsqueda adicionales a la consulta sqlfiddle.com/#!3/d41d8/18967
Aleksandr Fedorenko

2
Para el caso en el que ya tiene un ordinal para sus datos y está buscando una solución concisa (sin cursor) basada en conjuntos en SQL 2008 R2, esto parece ser perfecto.
Nick.McDermaid

1
No todas las consultas totales en ejecución tendrán un campo ordinal contiguo. A veces, un campo de fecha y hora es lo que tiene, o los registros se han eliminado de la mitad del tipo. Esa podría ser la razón por la que no se usa con más frecuencia.
Rubén

28

El operador APLICAR 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

55
Funciona muy bien para conjuntos de datos más pequeños. Un inconveniente es que tendrá que tener cláusulas where idénticas en la consulta interna y externa.
Sire

Dado que algunas de mis fechas eran exactamente las mismas (hasta una fracción de segundo) tuve que agregar: row_number () over (ordenar por txndate) a la tabla interna y externa y algunos índices compuestos para que se ejecute. Solución ingeniosa / simple. Por cierto, la aplicación cruzada probada contra subconsulta ... es un poco más rápido.
pghcpa

esto es muy limpio y funciona bien con pequeños conjuntos de datos; más rápido que el CTE recursivo
jtate

esta es una buena solución también (para pequeños conjuntos de datos), pero también debe tener en cuenta que implica que una columna de fecha sea única
Roman Pekar

11
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.


1
Esto es realmente ineficiente ... pero, de nuevo, no hay una forma realmente limpia de hacer esto en el servidor SQL
Sam Saffron

Absolutamente es ineficiente, pero hace el trabajo y no hay duda de si hay algo para ejecutar en el orden correcto o incorrecto.
Sam Axe

gracias, es útil tener respuestas alternativas, y también es útil tener una crítica eficiente
codeulike

7

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

El código puede no ser exactamente correcto, pero estoy seguro de que la idea sí lo es.

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

Si no le importa ver fechas repetidas, 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

Gracias ... simple fue genial. Había un índice que agregar para el rendimiento, pero era bastante simple (tomando una de las recomendaciones del Asesor de ajuste de motor de base de datos;), y luego se ejecutó como un tiro.
Doug_Ivison


4

Suponiendo que las ventanas funcionan en SQL Server 2008 como lo hace en otros lugares (que he probado), intente esto:

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

MSDN 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 no agregando de la forma en que GROUP BY lo hace). Molesto: la referencia de sintaxis de MSDN sugiere que es opcional, pero solo tengo instancias de SqlServer 2000 en este momento.

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


2
Usar OVER con SUM no funcionará en este caso para dar un total acumulado. La cláusula OVER no acepta ORDER BY cuando se usa con SUM. Debe usar PARTITION BY, que no funcionará para ejecutar totales.
Sam Axe

gracias, es realmente útil saber por qué esto no funciona. araqnid tal vez podría editar su respuesta para explicar por qué no es una opción
codeulike


Esto realmente funciona para mí, porque necesito particionar, por lo que, aunque esta no es la respuesta más popular, es la solución más fácil para mi problema de RT en SQL.
William MB

No tengo MSSQL 2008 conmigo, pero creo que probablemente podría particionar (seleccione nulo) y solucionar el problema de particionamiento. O haga una subselección con 1 partitionmey partición por eso. Además, la partición por probablemente sea necesaria en situaciones de la vida real al hacer informes.
nurettin

4

Si está utilizando el servidor SQL Server 2008 R2 anterior. 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 más información.

[1]:


1
Creo que LAG solo existe en SQL Server 2012 y superior (no 2008)
AaA

1
Usar LAG () no mejora, lo SUM(somevalue) OVER(...) que me parece mucho más limpio
Used_By_Already

2

Creo que se puede lograr un total acumulado utilizando 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

Sí, creo que esto es equivalente a 'Prueba 3' en la respuesta de Sam Saffron.
codeulike

2

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.


@Dave Creo que esta pregunta está tratando de encontrar una manera eficiente de hacer esto, la unión cruzada va a ser realmente lenta para grandes sets
Sam Saffron

gracias, es útil tener respuestas alternativas, y también es útil tener una crítica eficiente
codeulike


2

Aunque la mejor manera de hacerlo es usando una función de ventana, también se puede hacer usando una simple subconsulta 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;

0
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

Probablemente debería dar alguna información sobre lo que está haciendo aquí, y tenga en cuenta las ventajas / desventajas de este método en particular.
TT.

0

Aquí hay 2 formas simples de calcular el total acumulado:

Enfoque 1 : se puede escribir de esta manera si su DBMS admite funciones analíticas

SELECT     id
           ,somedate
           ,somevalue
           ,runningtotal = SUM(somevalue) OVER (ORDER BY somedate ASC)
FROM       TestTable

Enfoque 2 : puede utilizar OUTER APPLY si la versión de su base de datos / DBMS en sí no admite funciones analíticas

SELECT     T.id
           ,T.somedate
           ,T.somevalue
           ,runningtotal = OA.runningtotal
FROM       TestTable T
           OUTER APPLY (
                           SELECT   runningtotal = SUM(TI.somevalue)
                           FROM     TestTable TI
                           WHERE    TI.somedate <= S.somedate
                       ) OA;

Nota: - Si tiene que calcular el total acumulado para diferentes particiones por separado, puede hacerlo tal como se publica aquí: Cálculo de totales acumulados en filas y agrupación por ID

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.