Um ótimo recurso para calcular totais em execução no SQL Server é este documento por Itzik Ben Gan, que foi enviado à equipe do SQL Server como parte de sua campanha para ter o
OVER
cláusula estendida de sua implementação inicial do SQL Server 2005. Nele, ele mostra como, uma vez que você entra em dezenas de milhares de linhas, os cursores executam soluções baseadas em conjuntos. O SQL Server 2012 realmente estendeu o OVER
cláusula tornando este tipo de consulta muito mais fácil. SELECT col1,
SUM(col1) OVER (ORDER BY ind ROWS UNBOUNDED PRECEDING)
FROM @tmp
Como você está no SQL Server 2005, no entanto, isso não está disponível para você.
Adam Machanic mostra aqui como o CLR pode ser usado para melhorar o desempenho de cursores TSQL padrão.
Para esta definição de tabela
CREATE TABLE RunningTotals
(
ind int identity(1,1) primary key,
col1 int
)
Eu crio tabelas com 2.000 e 10.000 linhas em um banco de dados com
ALLOW_SNAPSHOT_ISOLATION ON
e um com essa configuração desativada (a razão para isso é porque meus resultados iniciais estavam em um banco de dados com a configuração que levou a um aspecto intrigante dos resultados). Os índices clusterizados para todas as tabelas tinham apenas 1 página raiz. O número de páginas de folha para cada um é mostrado abaixo.
+-------------------------------+-----------+------------+
| | 2,000 row | 10,000 row |
+-------------------------------+-----------+------------+
| ALLOW_SNAPSHOT_ISOLATION OFF | 5 | 22 |
| ALLOW_SNAPSHOT_ISOLATION ON | 8 | 39 |
+-------------------------------+-----------+------------+
Testei os seguintes casos (os links mostram os planos de execução)
- Partir à esquerda e agrupar por
- Subconsulta correlacionada Plano de 2.000 linhas ,plano de 10.000 linhas
- CTE da resposta de Mikael (atualizada)
- CTE abaixo
O motivo da inclusão da opção CTE adicional foi para fornecer uma solução CTE que ainda funcionaria se o
ind
coluna não foi garantida sequencial. 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 as consultas tinham um
CAST(col1 AS BIGINT)
adicionados para evitar erros de estouro em tempo de execução. Além disso, para todos eles, atribuí os resultados às variáveis acima, a fim de eliminar o tempo gasto no envio de resultados da consideração. 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 a subconsulta correlacionada quanto o
GROUP BY
versão use junções de loop aninhadas "triangulares" conduzidas por uma varredura de índice clusterizado no RunningTotals
tabela (T1
) e, para cada linha retornada por essa varredura, buscando de volta na tabela (T2
) auto-ingressando em T2.ind<=T1.ind
. Isso significa que as mesmas linhas são processadas repetidamente. Quando o
T1.ind=1000
linha é processada, a autojunção recupera e soma todas as linhas com um ind <= 1000
, em seguida, para a próxima linha em que T1.ind=1001
as mesmas 1.000 linhas são recuperadas novamente e somado com uma linha adicional e assim por diante. O número total de tais operações para uma tabela de 2.000 linhas é 2.001.000, para 10.000 linhas 50.005.000 ou mais geralmente
(n² + n) / 2
que claramente cresce exponencialmente. No caso de 2.000 linhas, a principal diferença entre o
GROUP BY
e as versões de subconsulta é que a primeira tem a agregação de fluxo após a junção e, portanto, tem três colunas alimentando-a (T1.ind
, T2.col1
, T2.col1
) e um GROUP BY
propriedade de T1.ind
enquanto o último é calculado como um agregado escalar, com o agregado de fluxo antes da junção, tem apenas T2.col1
alimentando-o e não tem GROUP BY
propriedade definida em tudo. Este arranjo mais simples pode ser visto como tendo um benefício mensurável em termos de tempo de CPU reduzido. Para o caso de 10.000 linhas, há uma diferença adicional no plano de subconsulta. Ele adiciona um carretel ansioso que copia todos os
ind,cast(col1 as bigint)
valores em tempdb
. No caso em que o isolamento de instantâneo está ativado, isso funciona mais compacto do que a estrutura de índice clusterizado e o efeito líquido é reduzir o número de leituras em cerca de 25% (já que a tabela base preserva bastante espaço vazio para informações de versão), quando esta opção está desativada ela funciona menos compacta (presumivelmente devido ao bigint
vs int
diferença) e mais lê o resultado. Isso reduz a lacuna entre a subconsulta e o grupo por versões, mas a subconsulta ainda vence. O vencedor claro, no entanto, foi o CTE Recursivo. Para a versão "sem lacunas", as leituras lógicas da tabela base agora são
2 x (n + 1)
refletindo o n
index procura no índice de 2 níveis para recuperar todas as linhas mais a linha adicional no final que não retorna nada e encerra a recursão. Isso ainda significava 20.002 leituras para processar uma tabela de 22 páginas! As leituras da tabela de trabalho lógica para a versão CTE recursiva são muito altas. Parece funcionar em 6 leituras de tabela de trabalho por linha de origem. Estes vêm do carretel de índice que armazena a saída da linha anterior e é lido novamente na próxima iteração (boa explicação disso por Umachandar Jayachandran aqui ). Apesar do número elevado, este ainda é o melhor desempenho.