Un gran recurso para calcular totales acumulados en SQL Server es este documento
por Itzik Ben Gan que se envió al equipo de SQL Server como parte de su campaña para tener el OVER
La cláusula se extendió más allá de su implementación inicial de SQL Server 2005. En él, muestra cómo una vez que ingresa a decenas de miles de filas, los cursores realizan soluciones basadas en conjuntos. SQL Server 2012 efectivamente extendió el OVER
cláusula que hace que este tipo de consulta sea mucho más fácil.
SELECT col1,
SUM(col1) OVER (ORDER BY ind ROWS UNBOUNDED PRECEDING)
FROM @tmp
Como está en SQL Server 2005, sin embargo, esto no está disponible para usted.
Adam Machanic muestra aquí cómo se puede utilizar CLR para mejorar el rendimiento de los cursores TSQL estándar.
Para esta definición de tabla
CREATE TABLE RunningTotals
(
ind int identity(1,1) primary key,
col1 int
)
Creo tablas con 2000 y 10 000 filas en una base de datos con ALLOW_SNAPSHOT_ISOLATION ON
y uno con esta configuración desactivada (la razón de esto es que mis resultados iniciales estaban en una base de datos con la configuración activada que condujo a un aspecto desconcertante de los resultados).
Los índices agrupados para todas las tablas solo tenían 1 página raíz. El número de hojas de página para cada uno se muestra a continuación.
+-------------------------------+-----------+------------+
| | 2,000 row | 10,000 row |
+-------------------------------+-----------+------------+
| ALLOW_SNAPSHOT_ISOLATION OFF | 5 | 22 |
| ALLOW_SNAPSHOT_ISOLATION ON | 8 | 39 |
+-------------------------------+-----------+------------+
Probé los siguientes casos (los enlaces muestran planes de ejecución)
- Únete a la izquierda y agrupa por
- Subconsulta correlacionada Plan de 2000 filas ,plan de 10000 filas
- CTE de la respuesta (actualizada) de Mikael
- CTE abajo
El motivo de la inclusión de la opción CTE adicional fue para proporcionar una solución CTE que aún funcionaría si ind
la columna no estaba garantizada secuencial.
SET STATISTICS IO ON;
SET STATISTICS TIME ON;
DECLARE @col1 int, @sumcol1 bigint;
WITH RecursiveCTE
AS (
SELECT TOP 1 ind, col1, CAST(col1 AS BIGINT) AS Total
FROM RunningTotals
ORDER BY ind
UNION ALL
SELECT R.ind, R.col1, R.Total
FROM (
SELECT T.*,
T.col1 + Total AS Total,
rn = ROW_NUMBER() OVER (ORDER BY T.ind)
FROM RunningTotals T
JOIN RecursiveCTE R
ON R.ind < T.ind
) R
WHERE R.rn = 1
)
SELECT @col1 =col1, @sumcol1=Total
FROM RecursiveCTE
OPTION (MAXRECURSION 0);
Todas las consultas tenían un CAST(col1 AS BIGINT)
agregado para evitar errores de desbordamiento en tiempo de ejecución. Además, para todos ellos, asigné los resultados a las variables como se indicó anteriormente para eliminar el tiempo dedicado a devolver los resultados de la consideración.
Resultados
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
| | | | Base Table | Work Table | Time |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
| | Snapshot | Rows | Scan count | logical reads | Scan count | logical reads | cpu | elapsed |
| Group By | On | 2,000 | 2001 | 12709 | | | 1469 | 1250 |
| | On | 10,000 | 10001 | 216678 | | | 30906 | 30963 |
| | Off | 2,000 | 2001 | 9251 | | | 1140 | 1160 |
| | Off | 10,000 | 10001 | 130089 | | | 29906 | 28306 |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
| Sub Query | On | 2,000 | 2001 | 12709 | | | 844 | 823 |
| | On | 10,000 | 2 | 82 | 10000 | 165025 | 24672 | 24535 |
| | Off | 2,000 | 2001 | 9251 | | | 766 | 999 |
| | Off | 10,000 | 2 | 48 | 10000 | 165025 | 25188 | 23880 |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
| CTE No Gaps | On | 2,000 | 0 | 4002 | 2 | 12001 | 78 | 101 |
| | On | 10,000 | 0 | 20002 | 2 | 60001 | 344 | 342 |
| | Off | 2,000 | 0 | 4002 | 2 | 12001 | 62 | 253 |
| | Off | 10,000 | 0 | 20002 | 2 | 60001 | 281 | 326 |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
| CTE Alllows Gaps | On | 2,000 | 2001 | 4009 | 2 | 12001 | 47 | 75 |
| | On | 10,000 | 10001 | 20040 | 2 | 60001 | 312 | 413 |
| | Off | 2,000 | 2001 | 4006 | 2 | 12001 | 94 | 90 |
| | Off | 10,000 | 10001 | 20023 | 2 | 60001 | 313 | 349 |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
Tanto la subconsulta correlacionada como el GROUP BY
La versión utiliza uniones de bucle anidado "triangulares" impulsadas por un escaneo de índice agrupado en RunningTotals
tabla (T1
) y, para cada fila devuelta por ese escaneo, buscando en la tabla (T2
) autounión en T2.ind<=T1.ind
.
Esto significa que las mismas filas se procesan repetidamente. Cuando el T1.ind=1000
la fila se procesa, la autocombinación recupera y suma todas las filas con un ind <= 1000
, luego para la siguiente fila donde T1.ind=1001
se recuperan las mismas 1000 filas nuevamente y sumado junto con una fila adicional y así sucesivamente.
El número total de tales operaciones para una tabla de 2000 filas es 2001000, para 10k filas 50005000 o más generalmente (n² + n) / 2
que claramente crece exponencialmente.
En el caso de 2000 filas, la principal diferencia entre GROUP BY
y las versiones de la subconsulta es que la primera tiene el flujo agregado después de la unión y, por lo tanto, tiene tres columnas que lo alimentan (T1.ind
, T2.col1
, T2.col1
) y un GROUP BY
propiedad de T1.ind
mientras que el último se calcula como un agregado escalar, con el flujo agregado antes de la unión, solo tiene T2.col1
alimentándose y no tiene GROUP BY
conjunto de propiedades en absoluto. Se puede ver que este arreglo más simple tiene un beneficio medible en términos de tiempo de CPU reducido.
Para el caso de 10 000 filas, existe una diferencia adicional en el plan de subconsulta. Agrega un spool ansioso
que copia todo el ind,cast(col1 as bigint)
valores en tempdb
. En el caso de que el aislamiento de instantáneas esté activado, resulta más compacto que la estructura de índice agrupado y el efecto neto es reducir la cantidad de lecturas en aproximadamente un 25% (ya que la tabla base conserva bastante espacio vacío para la información de versiones), cuando esta opción está desactivada, resulta menos compacto (presumiblemente debido al bigint
vs int
diferencia) y más resultados de lecturas. Esto reduce la brecha entre la subconsulta y el grupo por versiones, pero la subconsulta sigue ganando.
Sin embargo, el claro ganador fue el CTE recursivo. Para la versión "sin espacios", las lecturas lógicas de la tabla base ahora son 2 x (n + 1)
reflejando el n
index busca en el índice de 2 niveles para recuperar todas las filas más la adicional al final que no devuelve nada y finaliza la recursividad. ¡Sin embargo, eso todavía significaba 20 002 lecturas para procesar una tabla de 22 páginas!
Las lecturas de la tabla de trabajo lógico para la versión CTE recursiva son muy altas. Parece funcionar con 6 lecturas de la mesa de trabajo por fila de origen. Estos provienen del carrete de índice que almacena la salida de la fila anterior y luego se lee nuevamente en la siguiente iteración (buena explicación de esto por Umachandar Jayachandran aquí ). A pesar del alto número, sigue siendo el de mejor desempeño.