Sqlserver
 sql >> Base de Dados >  >> RDS >> Sqlserver

Calcular total de corrida / saldo de corrida


Para aqueles que não usam o SQL Server 2012 ou superior, um cursor é provavelmente o suportado mais eficiente e garantido método fora do CLR. Existem outras abordagens, como a "atualização peculiar", que pode ser marginalmente mais rápida, mas não garantida para funcionar no futuro, e, claro, abordagens baseadas em conjuntos com perfis de desempenho hiperbólicos à medida que a tabela fica maior e métodos CTE recursivos que geralmente exigem #tempdb E/S ou resultar em derramamentos que produzem aproximadamente o mesmo impacto.

INNER JOIN - não faça isso:


A abordagem lenta e baseada em conjuntos é da 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;

A razão pela qual isso é lento? À medida que a tabela aumenta, cada linha incremental requer a leitura de n-1 linhas na tabela. Isso é exponencial e destinado a falhas, tempos limite ou apenas usuários irritados.

Subconsulta correlacionada - também não faça isso:


O formulário de subconsulta é igualmente doloroso por razões 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;

Atualização peculiar - faça isso por sua conta e risco:


O método de "atualização peculiar" é mais eficiente que o acima, mas o comportamento não está documentado, não há garantias sobre a ordem e o comportamento pode funcionar hoje, mas pode falhar no futuro. Estou incluindo isso porque é um método popular e eficiente, mas isso não significa que eu o endosse. A principal razão pela qual eu respondi essa pergunta em vez de fechá-la como uma duplicata é porque a outra pergunta tem uma atualização peculiar como a resposta aceita.
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 primeiro depende do TID para ser contíguo, sem lacunas:
;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);

Se você não pode confiar nisso, então você pode usar esta variação, que simplesmente cria uma sequência contígua 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);

Dependendo do tamanho dos dados (por exemplo, colunas que não conhecemos), você pode encontrar um desempenho geral melhor preenchendo as colunas relevantes apenas em uma tabela #temp primeiro e processando-a em vez da tabela 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;

Apenas o primeiro método CTE fornecerá desempenho que rivaliza com a atualização peculiar, mas faz uma grande suposição sobre a natureza dos dados (sem lacunas). Os outros dois métodos retornarão e, nesses casos, você também poderá usar um cursor (se não puder usar o CLR e ainda não estiver no SQL Server 2012 ou superior).

Cursor


Todo mundo é informado de que os cursores são maus e que devem ser evitados a todo custo, mas isso realmente supera o desempenho da maioria dos outros métodos suportados e é mais seguro do que a atualização peculiar. Os únicos que prefiro sobre a solução do cursor são os métodos 2012 e CLR (abaixo):
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;

SQL Server 2012 ou superior


Novas funções de janela introduzidas no SQL Server 2012 tornam essa tarefa muito mais fácil (e também tem um desempenho melhor do que todos os métodos acima):
SELECT TID, amt, 
  RunningTotal = SUM(amt) OVER (ORDER BY TID ROWS UNBOUNDED PRECEDING)
FROM dbo.Transactions
ORDER BY TID;

Observe que em conjuntos de dados maiores, você descobrirá que o desempenho acima é muito melhor do que qualquer uma das duas opções a seguir, pois RANGE usa um spool em disco (e o padrão usa RANGE). No entanto, também é importante observar que o comportamento e os resultados podem diferir, portanto, certifique-se de que ambos retornem resultados corretos antes de decidir entre eles com base nessa diferença.
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, estou oferecendo um link para o método CLR de Pavel Pawlowski, que é de longe o método preferível em versões anteriores ao SQL Server 2012 (mas não 2000, obviamente).

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

Conclusão


Se você estiver no SQL Server 2012 ou superior, a escolha é óbvia - use o novo SUM() OVER() construção (com ROWS vs. RANGE ). Para versões anteriores, você desejará comparar o desempenho das abordagens alternativas em seu esquema, dados e - levando em consideração fatores não relacionados ao desempenho - determinar qual abordagem é a certa para você. Pode muito bem ser a abordagem CLR. Aqui estão minhas recomendações, em ordem de preferência:
  1. SUM() OVER() ... ROWS , se em 2012 ou superior
  2. Método CLR, se possível
  3. Primeiro método CTE recursivo, se possível
  4. Cursor
  5. Os outros métodos CTE recursivos
  6. Atualização peculiar
  7. Juntar e/ou subconsulta correlacionada

Para obter mais informações com comparações de desempenho desses métodos, consulte esta pergunta em http://dba.stackexchange.com:

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

Eu também postei mais detalhes sobre essas comparações aqui:

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

Também para totais de execução agrupados/particionados, consulte as seguintes postagens:

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

O particionamento resulta em uma consulta de totais em execução

Múltiplos Totais Executados com Agrupar Por