A linha 'real' imprecisa conta no plano paralelo

17

Esta é uma questão puramente acadêmica, tanto que não está causando problemas, e só estou interessado em ouvir explicações para o comportamento.

Faça uma edição padrão da tabela de registro CTE de junção cruzada Itzik Ben-Gan:

USE [master]
GO

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

CREATE FUNCTION [dbo].[TallyTable] 
(   
    @N INT
)
RETURNS TABLE WITH SCHEMABINDING AS
RETURN 
(
    WITH 
    E1(N) AS 
    (
        SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL 
        SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL 
        SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
    )                                       -- 1*10^1 or 10 rows
    , E2(N) AS (SELECT 1 FROM E1 a, E1 b)   -- 1*10^2 or 100 rows
    , E4(N) AS (SELECT 1 FROM E2 a, E2 b)   -- 1*10^4 or 10,000 rows
    , E8(N) AS (SELECT 1 FROM E4 a, E4 b)   -- 1*10^8 or 100,000,000 rows

    SELECT TOP (@N) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS N FROM E8 
)
GO

Emita uma consulta que criará uma tabela de número de 1 milhão de linhas:

SELECT
    COUNT(N)
FROM
    dbo.TallyTable(1000000) tt

Dê uma olhada no plano de execução paralelo para esta consulta:

Plano de execução paralela

Observe que a contagem de linhas 'real' antes do operador de coleta de fluxos é 1.004.588. Após o operador de fluxos de coleta, a contagem de linhas é a 1.000.000 esperada. Mais estranho ainda, o valor não é consistente e varia de uma corrida para outra. O resultado da COUNT está sempre correto.

Emita a consulta novamente, forçando o plano não paralelo:

SELECT
    COUNT(N)
FROM
    dbo.TallyTable(1000000) tt
OPTION (MAXDOP 1)

Desta vez, todos os operadores mostram as contagens de linha 'reais' corretas.

Plano de execução não paralelo

Eu tentei isso em 2005SP3 e 2008R2 até agora, os mesmos resultados em ambos. Alguma idéia sobre o que pode causar isso?

Mark Storey-Smith
fonte

Respostas:

12

As linhas são passadas pelas trocas internamente do encadeamento do produtor para o consumidor em pacotes (daí o CXPACKET - pacote de trocas de classe), em vez de uma linha por vez. Há uma certa quantidade de buffer dentro da troca. Além disso, a chamada para desligar o pipeline do lado do consumidor do Gather Streams deve ser passada em um pacote de controle de volta aos encadeamentos do produtor. A programação e outras considerações internas significam que os planos paralelos sempre têm uma certa 'distância de parada'.

Como conseqüência, você frequentemente verá esse tipo de diferença de contagem de linhas, onde menos do que todo o conjunto de linhas potencial de uma subárvore é realmente necessário. Nesse caso, o TOP leva a execução a um 'fim inicial'.

Mais Informações:

Paul White restabelece Monica
fonte
10

Acho que posso ter uma explicação parcial para isso, mas sinta-se à vontade para derrubá-la ou postar quaisquer alternativas. O @MartinSmith está definitivamente focado em algo destacando o efeito do TOP no plano de execução.

Simplificando, 'Contagem Real de Linhas' não é uma contagem das linhas que um operador processa, é o número de vezes que o método GetNext () do operador é chamado.

Retirado do BOL :

Os operadores físicos inicializam, coletam dados e fecham. Especificamente, o operador físico pode atender as três chamadas de método a seguir:

  • Init (): O método Init () faz com que um operador físico se inicialize e configure quaisquer estruturas de dados necessárias. O operador físico pode receber muitas chamadas Init (), embora normalmente um operador físico receba apenas uma.
  • GetNext (): O método GetNext () faz com que um operador físico obtenha a primeira linha ou a linha de dados subsequente. O operador físico pode receber zero ou muitas chamadas GetNext ().
  • Close (): o método Close () faz com que um operador físico execute algumas operações de limpeza e se desligue. Um operador físico recebe apenas uma chamada Close ().

O método GetNext () retorna uma linha de dados e o número de vezes que é chamado aparece como ActualRows na saída do Showplan que é produzida usando SET STATISTICS PROFILE ON ou SET STATISTICS XML ON.

Por uma questão de integridade, é útil um pouco de experiência nos operadores paralelos. O trabalho é distribuído para vários fluxos em um plano paralelo pelo fluxo de repartição ou pelos operadores de fluxo de distribuição. Eles distribuem linhas ou páginas entre os segmentos usando um dos quatro mecanismos:

  • Hash distribui linhas com base em um hash das colunas na linha
  • Round-robin distribui linhas iterando pela lista de threads em um loop
  • Broadcast distribui todas as páginas ou linhas para todos os threads
  • O particionamento de demanda é usado apenas para varreduras. Os threads giram, solicitam uma página de dados ao operador, os processam e solicitam uma página adicional quando concluídos.

O primeiro operador de fluxo de distribuição (à direita no plano) usa o particionamento de demanda nas linhas originárias de uma varredura constante. Há três threads que chamam GetNext () 6, 4 e 0 vezes para um total de 10 'Linhas reais':

<RunTimeInformation>
       <RunTimeCountersPerThread Thread="2" ActualRows="6" ActualEndOfScans="1" ActualExecutions="1" />
       <RunTimeCountersPerThread Thread="1" ActualRows="4" ActualEndOfScans="1" ActualExecutions="1" />
       <RunTimeCountersPerThread Thread="0" ActualRows="0" ActualEndOfScans="0" ActualExecutions="0" />
 </RunTimeInformation>

No próximo operador de distribuição, temos três threads novamente, desta vez com 50, 50 e 0 chamadas para GetNext () para um total de 100:

<RunTimeInformation>
    <RunTimeCountersPerThread Thread="2" ActualRows="50" ActualEndOfScans="1" ActualExecutions="1" />
    <RunTimeCountersPerThread Thread="1" ActualRows="50" ActualEndOfScans="1" ActualExecutions="1" />
    <RunTimeCountersPerThread Thread="0" ActualRows="0" ActualEndOfScans="0" ActualExecutions="0" />
</RunTimeInformation>

É no próximo operador paralelo que a causa e a explicação possivelmente aparecem.

<RunTimeInformation>
    <RunTimeCountersPerThread Thread="2" ActualRows="1" ActualEndOfScans="0" ActualExecutions="1" />
    <RunTimeCountersPerThread Thread="1" ActualRows="10" ActualEndOfScans="0" ActualExecutions="1" />
    <RunTimeCountersPerThread Thread="0" ActualRows="0" ActualEndOfScans="0" ActualExecutions="0" />
</RunTimeInformation>

Agora, temos 11 chamadas para GetNext (), onde esperávamos ver 10.

Edit: 2011-11-13

Preso neste momento, procurei respostas com os chaps no índice clusterizado e o @MikeWalsh gentilmente dirigiu o @SQLKiwi aqui .

Mark Storey-Smith
fonte
7

1,004,588 é uma figura que aparece muito nos meus testes também.

Também vejo isso no plano um pouco mais simples abaixo.

WITH 
E1(N) AS 
(
    SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL 
    SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL 
    SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
)                                       -- 1*10^1 or 10 rows
, E2(N) AS (SELECT 1 FROM E1 a, E1 b)   -- 1*10^2 or 100 rows
, E4(N) AS (SELECT 1 FROM E2 a, E2 b)   -- 1*10^4 or 10,000 rows
SELECT * INTO #E4 FROM E4;

WITH E8(N) AS (SELECT 1 FROM #E4 a, #E4 b),
Nums(N) AS (SELECT  TOP (1000000) ROW_NUMBER() OVER (ORDER BY (SELECT 0)) FROM E8 )
SELECT COUNT(N) FROM Nums

DROP TABLE #E4

Plano

Outras figuras de interesse no plano de execução são

+----------------------------------+--------------+--------------+-----------------+
|                                  | Table Scan A | Table Scan B | Row Count Spool |
+----------------------------------+--------------+--------------+-----------------+
| Number Of Executions             | 2            |            2 |             101 |
| Actual Number Of Rows - Total    | 101          |        20000 |         1004588 |
| Actual Number Of Rows - Thread 0 | -            |              |                 |
| Actual Number Of Rows - Thread 1 | 95           |        10000 |          945253 |
| Actual Number Of Rows - Thread 2 | 6            |        10000 |           59335 |
| Actual Rebinds                   | 0            |            0 |               2 |
| Actual Rewinds                   | 0            |            0 |              99 |
+----------------------------------+--------------+--------------+-----------------+

Meu palpite é que, como as tarefas estão sendo processadas em paralelo, uma tarefa está no meio do processamento de linhas de vôo, quando a outra entrega a milionésima linha ao operador de fluxos de coleta, para que linhas adicionais sejam manipuladas. Além disso, neste artigo, as linhas são armazenadas em buffer e entregues em lotes para esse iterador, portanto, parece bastante provável que o número de linhas sendo processadas exceda em vez de atingir exatamente a TOPespecificação em qualquer evento.

Editar

Basta olhar para isso com mais detalhes. Percebi que estava obtendo mais variedade do que apenas a 1,004,588contagem de linhas citada acima, então executei a consulta acima em um loop por 1.000 iterações e capturei os planos de execução reais. Descartar os 81 resultados para os quais o grau de paralelismo foi zero deu os seguintes números.

count       Table Scan A: Total Actual Row Spool - Total Actual Rows
----------- ------------------------------ ------------------------------
352         101                            1004588
323         102                            1004588
72          101                            1003565
37          101                            1002542
35          102                            1003565
29          101                            1001519
18          101                            1000496
13          102                            1002542
5           9964                           99634323
5           102                            1001519
4           9963                           99628185
3           10000                          100000000
3           9965                           99642507
2           9964                           99633300
2           9966                           99658875
2           9965                           99641484
1           9984                           99837989
1           102                            1000496
1           9964                           99637392
1           9968                           99671151
1           9966                           99656829
1           9972                           99714117
1           9963                           99629208
1           9985                           99847196
1           9967                           99665013
1           9965                           99644553
1           9963                           99623626
1           9965                           99647622
1           9966                           99654783
1           9963                           99625116

Pode-se observar que 1.004.588 foi de longe o resultado mais comum, mas que em três ocasiões ocorreu o pior caso possível e 100.000.000 de linhas foram processadas. O melhor caso observado foi a contagem de 1.000.496 linhas, que ocorreu 19 vezes.

O script completo a ser reproduzido está na parte inferior da revisão 2 desta resposta (ele precisará de ajustes se for executado em um sistema com mais de 2 processadores).

Martin Smith
fonte
1

Acredito que o problema vem do fato de que vários fluxos podem processar a mesma linha, dependendo de como as linhas são divididas entre os fluxos.

mrdenny
fonte