sql >> Base de Datos >  >> RDS >> Sqlserver

Calcular total acumulado / saldo acumulado

Para aquellos que no usan SQL Server 2012 o superior, un cursor es probablemente el compatible más eficiente. y garantizado método fuera de CLR. Existen otros enfoques, como la "actualización peculiar", que puede ser un poco más rápido pero no se garantiza que funcione en el futuro y, por supuesto, enfoques basados ​​en conjuntos con perfiles de rendimiento hiperbólicos a medida que la tabla crece y métodos CTE recursivos que a menudo requieren #tempdb I/O o dar como resultado derrames que producen aproximadamente el mismo impacto.

INNER JOIN - no hagas esto:

El enfoque lento basado en conjuntos tiene la forma:

SELECT t1.TID, t1.amt, RunningTotal = SUM(t2.amt)
FROM dbo.Transactions AS t1
INNER JOIN dbo.Transactions AS t2
  ON t1.TID >= t2.TID
GROUP BY t1.TID, t1.amt
ORDER BY t1.TID;

¿La razón por la que esto es lento? A medida que la tabla crece, cada fila incremental requiere leer n-1 filas en la tabla. Esto es exponencial y está destinado a fallas, tiempos de espera o simplemente usuarios enojados.

Subconsulta correlacionada:tampoco haga esto:

El formulario de subconsulta es igualmente doloroso por razones igualmente dolorosas.

SELECT TID, amt, RunningTotal = amt + COALESCE(
(
  SELECT SUM(amt)
    FROM dbo.Transactions AS i
    WHERE i.TID < o.TID), 0
)
FROM dbo.Transactions AS o
ORDER BY TID;

Actualización peculiar:hazlo bajo tu propio riesgo:

El método de "actualización peculiar" es más eficiente que el anterior, pero el comportamiento no está documentado, no hay garantías sobre el orden y el comportamiento podría funcionar hoy pero podría fallar en el futuro. Incluyo esto porque es un método popular y eficiente, pero eso no significa que lo respalde. La razón principal por la que incluso respondí esta pregunta en lugar de cerrarla como un duplicado es porque la otra pregunta tiene una actualización peculiar como respuesta aceptada.

DECLARE @t TABLE
(
  TID INT PRIMARY KEY,
  amt INT,
  RunningTotal INT
);
 
DECLARE @RunningTotal INT = 0;
 
INSERT @t(TID, amt, RunningTotal)
  SELECT TID, amt, RunningTotal = 0
  FROM dbo.Transactions
  ORDER BY TID;
 
UPDATE @t
  SET @RunningTotal = RunningTotal = @RunningTotal + amt
  FROM @t;
 
SELECT TID, amt, RunningTotal
  FROM @t
  ORDER BY TID;

CTEs recursivos

Este primero depende de que TID sea contiguo, sin espacios:

;WITH x AS
(
  SELECT TID, amt, RunningTotal = amt
    FROM dbo.Transactions
    WHERE TID = 1
  UNION ALL
  SELECT y.TID, y.amt, x.RunningTotal + y.amt
   FROM x 
   INNER JOIN dbo.Transactions AS y
   ON y.TID = x.TID + 1
)
SELECT TID, amt, RunningTotal
  FROM x
  ORDER BY TID
  OPTION (MAXRECURSION 10000);

Si no puede confiar en esto, entonces puede usar esta variación, que simplemente construye una secuencia contigua usando ROW_NUMBER() :

;WITH y AS 
(
  SELECT TID, amt, rn = ROW_NUMBER() OVER (ORDER BY TID)
    FROM dbo.Transactions
), x AS
(
    SELECT TID, rn, amt, rt = amt
      FROM y
      WHERE rn = 1
    UNION ALL
    SELECT y.TID, y.rn, y.amt, x.rt + y.amt
      FROM x INNER JOIN y
      ON y.rn = x.rn + 1
)
SELECT TID, amt, RunningTotal = rt
  FROM x
  ORDER BY x.rn
  OPTION (MAXRECURSION 10000);

Dependiendo del tamaño de los datos (p. ej., columnas que no conocemos), puede encontrar un mejor rendimiento general rellenando primero las columnas relevantes solo en una tabla #temp y procesándola en lugar de la tabla base:

CREATE TABLE #x
(
  rn  INT PRIMARY KEY,
  TID INT,
  amt INT
);

INSERT INTO #x (rn, TID, amt)
SELECT ROW_NUMBER() OVER (ORDER BY TID),
  TID, amt
FROM dbo.Transactions;

;WITH x AS
(
  SELECT TID, rn, amt, rt = amt
    FROM #x
    WHERE rn = 1
  UNION ALL
  SELECT y.TID, y.rn, y.amt, x.rt + y.amt
    FROM x INNER JOIN #x AS y
    ON y.rn = x.rn + 1
)
SELECT TID, amt, RunningTotal = rt
  FROM x
  ORDER BY TID
  OPTION (MAXRECURSION 10000);

DROP TABLE #x;

Solo el primer método CTE proporcionará un rendimiento que rivaliza con la actualización peculiar, pero hace una gran suposición sobre la naturaleza de los datos (sin lagunas). Los otros dos métodos retrocederán y, en esos casos, también puede usar un cursor (si no puede usar CLR y aún no está en SQL Server 2012 o superior).

Cursores

A todo el mundo se le dice que los cursores son malos y que deben evitarse a toda costa, pero esto en realidad supera el rendimiento de la mayoría de los otros métodos compatibles y es más seguro que la peculiar actualización. Los únicos que prefiero sobre la solución del cursor son los métodos 2012 y CLR (abajo):

CREATE TABLE #x
(
  TID INT PRIMARY KEY, 
  amt INT, 
  rt INT
);

INSERT #x(TID, amt) 
  SELECT TID, amt
  FROM dbo.Transactions
  ORDER BY TID;

DECLARE @rt INT, @tid INT, @amt INT;
SET @rt = 0;

DECLARE c CURSOR LOCAL STATIC READ_ONLY FORWARD_ONLY
  FOR SELECT TID, amt FROM #x ORDER BY TID;

OPEN c;

FETCH c INTO @tid, @amt;

WHILE @@FETCH_STATUS = 0
BEGIN
  SET @rt = @rt + @amt;
  UPDATE #x SET rt = @rt WHERE TID = @tid;
  FETCH c INTO @tid, @amt;
END

CLOSE c; DEALLOCATE c;

SELECT TID, amt, RunningTotal = rt 
  FROM #x 
  ORDER BY TID;

DROP TABLE #x;

Servidor SQL 2012 o superior

Las nuevas funciones de ventana introducidas en SQL Server 2012 hacen que esta tarea sea mucho más fácil (y también funciona mejor que todos los métodos anteriores):

SELECT TID, amt, 
  RunningTotal = SUM(amt) OVER (ORDER BY TID ROWS UNBOUNDED PRECEDING)
FROM dbo.Transactions
ORDER BY TID;

Tenga en cuenta que en conjuntos de datos más grandes, encontrará que lo anterior funciona mucho mejor que cualquiera de las siguientes dos opciones, ya que RANGE usa un spool en disco (y el valor predeterminado usa RANGE). Sin embargo, también es importante tener en cuenta que el comportamiento y los resultados pueden diferir, así que asegúrese de que ambos arrojen resultados correctos antes de decidir entre ellos en función de esta diferencia.

SELECT TID, amt, 
  RunningTotal = SUM(amt) OVER (ORDER BY TID)
FROM dbo.Transactions
ORDER BY TID;

SELECT TID, amt, 
  RunningTotal = SUM(amt) OVER (ORDER BY TID RANGE UNBOUNDED PRECEDING)
FROM dbo.Transactions
ORDER BY TID;

CLR

Para completar, ofrezco un enlace al método CLR de Pavel Pawlowski, que es, con mucho, el método preferible en versiones anteriores a SQL Server 2012 (pero obviamente no 2000).

http://www.pawlowski.cz/2010/09/sql-server-and-fastest-running-totals-using-clr/

Conclusión

Si está en SQL Server 2012 o superior, la elección es obvia:use el nuevo SUM() OVER() construir (con ROWS frente a RANGE ). Para versiones anteriores, querrá comparar el rendimiento de los enfoques alternativos en su esquema, datos y, teniendo en cuenta factores no relacionados con el rendimiento, determinar qué enfoque es el adecuado para usted. Muy bien puede ser el enfoque CLR. Estas son mis recomendaciones, en orden de preferencia:

  1. SUM() OVER() ... ROWS , si es de 2012 o posterior
  2. método CLR, si es posible
  3. Primer método CTE recursivo, si es posible
  4. Cursores
  5. Los otros métodos CTE recursivos
  6. Actualización peculiar
  7. Unión y/o subconsulta correlacionada

Para obtener más información sobre las comparaciones de rendimiento de estos métodos, consulte esta pregunta en http://dba.stackexchange.com:

https://dba.stackexchange.com/questions/19507/running-total-with-count

También he publicado en el blog más detalles sobre estas comparaciones aquí:

http://www.sqlperformance.com/2012/07/t-sql-queries/running-totals

También para totales acumulados agrupados/particionados, consulte las siguientes publicaciones:

http://sqlperformance.com/2014/01/t-sql-queries/grouped-running-totals

La partición da como resultado una consulta de totales acumulados

Múltiples totales acumulados con Agrupar por