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

subconsulta ou leftjoin com grupo por qual é mais rápido?


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)
  1. Partir à esquerda e agrupar por
  2. Subconsulta correlacionada Plano de 2.000 linhas ,plano de 10.000 linhas
  3. CTE da resposta de Mikael (atualizada)
  4. 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.