¿Total acumulado con conteo?

34

Como sugiere el título, necesito ayuda para obtener un total acumulado en T-SQL. El problema es que la suma que necesito hacer es la suma de un conteo:

sum(count (distinct (customers))) 

Digamos que si ejecuté el conteo solo, el resultado sería:

Day | CountCustomers
----------------------
5/1  |      1
5/2  |      0
5/3  |      5

Necesito salida con la suma para ser:

Day | RunningTotalCustomers
----------------------
5/1  |      1
5/2  |      1
5/3  |      6

He hecho totales antes de usar el coalescemétodo, pero nunca con un conteo. No estoy seguro de cómo hacerlo ahora que tengo la cuenta.

Aaron Bertrand
fuente
2
¿Qué versión de SQL Server por favor? ¿Puede compartir el alcance de los datos? ¿Estamos hablando de 1000 filas, un millón, un billón? ¿Es realmente solo estas dos columnas, o ha simplificado el esquema para nosotros? Finalmente, ¿es Dayuna clave y los valores son contiguos?
Aaron Bertrand
Hice un blog completo sobre el total acumulado (actualización peculiar vs CTE híbrido recursivo vs cursor): ienablemuch.com/2012/05/… No incluí el total acumulado que utiliza un enfoque basado en conjuntos, el rendimiento no es nada para ser deseado: sqlblog.com/blogs/adam_machanic/archive/2006/07/12/…
Michael Buen

Respuestas:

53

Aquí hay algunos métodos que puede comparar. Primero, configuremos una tabla con algunos datos ficticios. Estoy completando esto con un montón de datos aleatorios de sys.all_columns. Bueno, es algo aleatorio: me aseguro de que las fechas sean contiguas (lo que en realidad solo es importante para una de las respuestas).

CREATE TABLE dbo.Hits(Day SMALLDATETIME, CustomerID INT);

CREATE CLUSTERED INDEX x ON dbo.Hits([Day]);

INSERT dbo.Hits SELECT TOP (5000) DATEADD(DAY, r, '20120501'),
  COALESCE(ASCII(SUBSTRING(name, s, 1)), 86)
FROM (SELECT name, r = ROW_NUMBER() OVER (ORDER BY name)/10,
       s = CONVERT(INT, RIGHT(CONVERT(VARCHAR(20), [object_id]), 1))
FROM sys.all_columns) AS x;

SELECT 
  Earliest_Day   = MIN([Day]), 
  Latest_Day     = MAX([Day]), 
  Unique_Days    = DATEDIFF(DAY, MIN([Day]), MAX([Day])) + 1, 
  Total_Rows     = COUNT(*)
FROM dbo.Hits;

Resultados:

Earliest_Day         Latest_Day           Unique_Days  Total_Days
-------------------  -------------------  -----------  ----------
2012-05-01 00:00:00  2013-09-13 00:00:00  501          5000

Los datos se ven así (5000 filas), pero se verán ligeramente diferentes en su sistema según la versión y el número de compilación:

Day                  CustomerID
-------------------  ---
2012-05-01 00:00:00  95
2012-05-01 00:00:00  97
2012-05-01 00:00:00  97
2012-05-01 00:00:00  117
2012-05-01 00:00:00  100
...
2012-05-02 00:00:00  110
2012-05-02 00:00:00  110
2012-05-02 00:00:00  95
...

Y los resultados de los totales acumulados deberían verse así (501 filas):

Day                  c   rt
-------------------  --  --
2012-05-01 00:00:00  6   6
2012-05-02 00:00:00  5   11
2012-05-03 00:00:00  4   15
2012-05-04 00:00:00  7   22
2012-05-05 00:00:00  6   28
...

Entonces, los métodos que voy a comparar son:

  • "auto-unión" - el enfoque purista basado en conjuntos
  • "CTE recursivo con fechas": esto se basa en fechas contiguas (sin espacios)
  • "CTE recursivo con row_number" - similar al anterior pero más lento, confiando en ROW_NUMBER
  • "CTE recursivo con #temp table" - robado de la respuesta de Mikael como se sugiere
  • "actualización peculiar" que, aunque no es compatible y no promete un comportamiento definido, parece ser bastante popular
  • "cursor"
  • SQL Server 2012 con la nueva funcionalidad de ventanas

auto-unirse

Esta es la forma en que la gente le dirá que lo haga cuando le adviertan que se mantenga alejado de los cursores, porque "basado en conjuntos siempre es más rápido". En algunos experimentos recientes, descubrí que el cursor supera esta solución.

;WITH g AS 
(
  SELECT [Day], c = COUNT(DISTINCT CustomerID) 
    FROM dbo.Hits
    GROUP BY [Day]
)
SELECT g.[Day], g.c, rt = SUM(g2.c)
  FROM g INNER JOIN g AS g2
  ON g.[Day] >= g2.[Day]
GROUP BY g.[Day], g.c
ORDER BY g.[Day];

cte recursivo con fechas

Recordatorio: esto se basa en fechas contiguas (sin espacios), hasta 10000 niveles de recursión, y que conoce la fecha de inicio del rango que le interesa (para establecer el ancla). Podría establecer el ancla dinámicamente utilizando una subconsulta, por supuesto, pero quería mantener las cosas simples.

;WITH g AS 
(
  SELECT [Day], c = COUNT(DISTINCT CustomerID) 
    FROM dbo.Hits
    GROUP BY [Day]
), x AS
(
    SELECT [Day], c, rt = c
        FROM g
        WHERE [Day] = '20120501'
    UNION ALL
    SELECT g.[Day], g.c, x.rt + g.c
        FROM x INNER JOIN g
        ON g.[Day] = DATEADD(DAY, 1, x.[Day])
)
SELECT [Day], c, rt
    FROM x
    ORDER BY [Day]
    OPTION (MAXRECURSION 10000);

cte recursivo con row_number

El cálculo del número de fila es un poco caro aquí. Nuevamente, esto admite un nivel máximo de recursión de 10000, pero no es necesario asignar el ancla.

;WITH g AS 
(
  SELECT [Day], rn = ROW_NUMBER() OVER (ORDER BY DAY), 
    c = COUNT(DISTINCT CustomerID) 
    FROM dbo.Hits
    GROUP BY [Day]
), x AS
(
    SELECT [Day], rn, c, rt = c
        FROM g
        WHERE rn = 1
    UNION ALL
    SELECT g.[Day], g.rn, g.c, x.rt + g.c
        FROM x INNER JOIN g
        ON g.rn = x.rn + 1
)
SELECT [Day], c, rt
    FROM x
    ORDER BY [Day]
    OPTION (MAXRECURSION 10000);

cte recursivo con tabla temporal

Robando la respuesta de Mikael, como se sugiere, para incluir esto en las pruebas.

CREATE TABLE #Hits
(
  rn INT PRIMARY KEY,
  c INT,
  [Day] SMALLDATETIME
);

INSERT INTO #Hits (rn, c, Day)
SELECT ROW_NUMBER() OVER (ORDER BY DAY),
       COUNT(DISTINCT CustomerID),
       [Day]
FROM dbo.Hits
GROUP BY [Day];

WITH x AS
(
    SELECT [Day], rn, c, rt = c
        FROM #Hits as c
        WHERE rn = 1
    UNION ALL
    SELECT g.[Day], g.rn, g.c, x.rt + g.c
        FROM x INNER JOIN #Hits as g
        ON g.rn = x.rn + 1
)
SELECT [Day], c, rt
    FROM x
    ORDER BY [Day]
    OPTION (MAXRECURSION 10000);

DROP TABLE #Hits;

actualización peculiar

Nuevamente, solo incluyo esto para completar; Personalmente, no confiaría en esta solución ya que, como mencioné en otra respuesta, no se garantiza que este método funcione en absoluto, y puede romperse por completo en una versión futura de SQL Server. (Estoy haciendo todo lo posible para obligar a SQL Server a obedecer el orden que quiero, usando una pista para la elección del índice).

CREATE TABLE #x([Day] SMALLDATETIME, c INT, rt INT);
CREATE UNIQUE CLUSTERED INDEX x ON #x([Day]);

INSERT #x([Day], c) 
    SELECT [Day], c = COUNT(DISTINCT CustomerID) 
    FROM dbo.Hits
    GROUP BY [Day]
    ORDER BY [Day];

DECLARE @rt1 INT;
SET @rt1 = 0;

UPDATE #x
SET @rt1 = rt = @rt1 + c
FROM #x WITH (INDEX = x);

SELECT [Day], c, rt FROM #x ORDER BY [Day];

DROP TABLE #x;

cursor

"¡Cuidado, hay cursores aquí! ¡Los cursores son malvados! ¡Debes evitar los cursores a toda costa!" No, no soy yo quien habla, son cosas que escucho mucho. Contrariamente a la opinión popular, hay algunos casos en que los cursores son apropiados.

CREATE TABLE #x2([Day] SMALLDATETIME, c INT, rt INT);
CREATE UNIQUE CLUSTERED INDEX x ON #x2([Day]);

INSERT #x2([Day], c) 
    SELECT [Day], COUNT(DISTINCT CustomerID) 
    FROM dbo.Hits
    GROUP BY [Day]
    ORDER BY [Day];

DECLARE @rt2 INT, @d SMALLDATETIME, @c INT;
SET @rt2 = 0;

DECLARE c CURSOR LOCAL STATIC READ_ONLY FORWARD_ONLY
  FOR SELECT [Day], c FROM #x2 ORDER BY [Day];

OPEN c;

FETCH NEXT FROM c INTO @d, @c;

WHILE @@FETCH_STATUS = 0
BEGIN
  SET @rt2 = @rt2 + @c;
  UPDATE #x2 SET rt = @rt2 WHERE [Day] = @d;
  FETCH NEXT FROM c INTO @d, @c;
END

SELECT [Day], c, rt FROM #x2 ORDER BY [Day];

DROP TABLE #x2;

SQL Server 2012

Si está utilizando la versión más reciente de SQL Server, las mejoras en la funcionalidad de ventanas nos permiten calcular fácilmente los totales acumulados sin el costo exponencial de la unión automática (el SUM se calcula en una pasada), la complejidad de los CTE (incluido el requisito de filas contiguas para el mejor rendimiento de CTE), la actualización peculiar no compatible y el cursor prohibido. Solo tenga cuidado con la diferencia entre usar RANGEy ROWS, o no especificar, solo ROWSevita un carrete en el disco, lo que obstaculizará significativamente el rendimiento de lo contrario.

;WITH g AS 
(
  SELECT [Day], c = COUNT(DISTINCT CustomerID) 
    FROM dbo.Hits
    GROUP BY [Day]
)
SELECT g.[Day], c, 
  rt = SUM(c) OVER (ORDER BY [Day] ROWS UNBOUNDED PRECEDING)
FROM g
ORDER BY g.[Day];

comparaciones de rendimiento

Tomé cada enfoque y lo envolví un lote usando lo siguiente:

SELECT SYSUTCDATETIME();
GO
DBCC DROPCLEANBUFFERS;DBCC FREEPROCCACHE;
-- query here
GO 10
SELECT SYSUTCDATETIME();

Estos son los resultados de la duración total, en milisegundos (recuerde que esto también incluye los comandos DBCC cada vez):

method                          run 1     run 2
-----------------------------   --------  --------
self-join                        1296 ms   1357 ms -- "supported" non-SQL 2012 winner
recursive cte with dates         1655 ms   1516 ms
recursive cte with row_number   19747 ms  19630 ms
recursive cte with #temp table   1624 ms   1329 ms
quirky update                     880 ms   1030 ms -- non-SQL 2012 winner
cursor                           1962 ms   1850 ms
SQL Server 2012                   847 ms    917 ms -- winner if SQL 2012 available

Y lo hice nuevamente sin los comandos DBCC:

method                          run 1     run 2
-----------------------------   --------  --------
self-join                        1272 ms   1309 ms -- "supported" non-SQL 2012 winner
recursive cte with dates         1247 ms   1593 ms
recursive cte with row_number   18646 ms  18803 ms
recursive cte with #temp table   1340 ms   1564 ms
quirky update                    1024 ms   1116 ms -- non-SQL 2012 winner
cursor                           1969 ms   1835 ms
SQL Server 2012                   600 ms    569 ms -- winner if SQL 2012 available

Eliminando tanto el DBCC como los bucles, solo mide una iteración sin procesar:

method                          run 1     run 2
-----------------------------   --------  --------
self-join                         313 ms    242 ms
recursive cte with dates          217 ms    217 ms
recursive cte with row_number    2114 ms   1976 ms
recursive cte with #temp table     83 ms    116 ms -- "supported" non-SQL 2012 winner
quirky update                      86 ms     85 ms -- non-SQL 2012 winner
cursor                           1060 ms    983 ms
SQL Server 2012                    68 ms     40 ms -- winner if SQL 2012 available

Por último, he multiplicado el número de filas en la tabla de origen por 10 (cambiando superior a 50.000 y la adición de otra tabla como una combinación cruzada). Los resultados de esto, una sola iteración sin comandos DBCC (simplemente en interés del tiempo):

method                           run 1      run 2
-----------------------------    --------   --------
self-join                         2401 ms    2520 ms
recursive cte with dates           442 ms     473 ms
recursive cte with row_number   144548 ms  147716 ms
recursive cte with #temp table     245 ms     236 ms -- "supported" non-SQL 2012 winner
quirky update                      150 ms     148 ms -- non-SQL 2012 winner
cursor                            1453 ms    1395 ms
SQL Server 2012                    131 ms     133 ms -- winner

Sólo mide la duración - Lo dejo como ejercicio para el lector comparar estos enfoques de sus datos, comparando otras métricas que pueden ser importantes (o pueden variar con su esquema / datos). Antes de sacar conclusiones a partir de esta respuesta, que será hasta usted para probarlo en contra de sus datos y su esquema ... estos resultados es casi seguro que cambian a medida que los recuentos de filas de llegar más alto.


manifestación

He agregado un sqlfiddle . Resultados:

ingrese la descripción de la imagen aquí


conclusión

En mis pruebas, la elección sería:

  1. Método SQL Server 2012, si tengo SQL Server 2012 disponible.
  2. Si SQL Server 2012 no está disponible, y mis fechas son contiguas, iría con el método recursivo cte con fechas.
  3. Si ni 1. ni 2. son aplicables, iría con la autounión sobre la peculiar actualización, a pesar de que el rendimiento fue cercano, solo porque el comportamiento está documentado y garantizado. Estoy menos preocupado por la compatibilidad futura porque espero que si la actualización peculiar se rompe después de que ya haya convertido todo mi código a 1. :-)

Pero de nuevo, debe probarlos contra su esquema y datos. Dado que esta fue una prueba artificial con recuentos de filas relativamente bajos, también podría ser un pedo en el viento. He realizado otras pruebas con diferentes esquemas y recuentos de filas, y las heurísticas de rendimiento fueron bastante diferentes ... por eso hice tantas preguntas de seguimiento a su pregunta original.


ACTUALIZAR

He blogueado más sobre esto aquí:

Mejores enfoques para ejecutar totales: actualizado para SQL Server 2012

Aaron Bertrand
fuente
1

Esta es, aparentemente, la solución óptima.

DECLARE @dailyCustomers TABLE (day smalldatetime, CountCustomers int, RunningTotal int)

DECLARE @RunningTotal int

SET @RunningTotal = 0

INSERT INTO @dailyCustomers 
SELECT day, CountCustomers, null
FROM Sales
ORDER BY day

UPDATE @dailyCustomers
SET @RunningTotal = RunningTotal = @RunningTotal + CountCustomers
FROM @dailyCustomers

SELECT * FROM @dailyCustomers
Código mago
fuente
¿Alguna idea sin implementar una tabla temporal (mi proceso ya está forzando valores a través de varias tablas temporales por necesidad, por lo que estoy tratando de encontrar una manera de evitar usar otra tabla temporal)? Si no es así, voy a utilizar este método. Creo que va a trabajar
También se puede hacer con una autounión o una subconsulta anidada, pero estas opciones no funcionan tan bien. Además, es probable que de todas formas golpees tempdb con estas alternativas con algunas bobinas o tablas de trabajo.
3
Sólo ten en cuenta que este método de "actualización peculiar" no se garantiza que el trabajo - esta sintaxis no es compatible y su comportamiento no está definido, y se puede romper en un paquete versión futura, solución o servicio caliente. Entonces, si bien es más rápido que algunas alternativas compatibles, eso tiene un costo potencial de compatibilidad futuro.
Aaron Bertrand
66
Hay muchas advertencias sobre este enfoque que Jeff Moden ha escrito en alguna parte. Debe tener un índice agrupado, daypor ejemplo.
Martin Smith
2
@MartinSmith Es un artículo MUY GRANDE en sqlservercentral.com (vaya a la página del Autor y encuentre sus artículos sobre actualizaciones de quirck).
Fabricio Araujo
-2

Solo otra forma, costosa, pero independiente de la versión. No usa tablas temporales o variables.

select T.dday, T.CustomersByDay + 
    (select count(A.customer) from NewCustomersByDate A 
      where A.dday < T.dday) as TotalCustomerTillNow 
from (select dday, count(customer) as CustomersByDay 
        from NewCustomersByDate group by dday) T 

fuente
2
Eso no es bueno, eso es muy lento. Incluso si solo tiene 100 filas, hará una lectura de ping-pong entre las mesas a 5.050 veces. 200 filas, es 20,100 veces. Con solo 1,000 filas, salta exponencialmente a 500,500 lecturas sqlblog.com/blogs/adam_machanic/archive/2006/07/12/…
Michael Buen
Vi el enlace a tu blog después de publicar esto, ahora veo que es una muy mala idea, ¡gracias!