Conteggio delle righe "effettive" imprecise nel piano parallelo


17

Questa è una domanda puramente accademica, al punto che non sta causando un problema e sono solo interessato a ricevere spiegazioni sul comportamento.

Prendi un problema standard Itzik Ben-Gan cross-join tabella di conteggio CTE:

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

Emettere una query che creerà una tabella dei numeri di 1 milione di righe:

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

Dai un'occhiata al piano di esecuzione parallelo per questa query:

Piano di esecuzione parallela

Si noti che il conteggio delle righe "effettive" prima dell'operatore di flussi di raccolta è 1.004.588. Dopo l'operatore di flussi di raccolta, il conteggio delle righe è previsto 1.000.000. Ancora più strano, il valore non è coerente e varierà da una corsa all'altra. Il risultato di COUNT è sempre corretto.

Emettere nuovamente la query, forzando un piano non parallelo:

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

Questa volta tutti gli operatori mostrano i conteggi delle righe "effettivi" corretti.

Piano di esecuzione non parallelo

Ho provato questo su 2005SP3 e 2008R2 finora, stessi risultati su entrambi. Qualche idea su cosa potrebbe causare questo?

Risposte:


12

Le righe vengono trasferite internamente agli scambi dal produttore al thread del consumatore in pacchetti (da cui CXPACKET - pacchetto di scambio di classe), anziché una riga alla volta. C'è una certa quantità di buffering all'interno dello scambio. Inoltre, la chiamata per chiudere la pipeline dal lato consumer di Gather Streams deve essere passata in un pacchetto di controllo ai thread del produttore. La pianificazione e altre considerazioni interne indicano che i piani paralleli hanno sempre una certa "distanza di arresto".

Di conseguenza, vedrai spesso questo tipo di differenza nel conteggio delle righe in cui è effettivamente richiesto meno dell'intero set di righe potenziale di un sottoalbero. In questo caso, il TOP porta l'esecuzione a un termine precoce.

Maggiori informazioni:


10

Penso che potrei avere una spiegazione parziale per questo, ma sentiti libero di abbatterlo o pubblicare eventuali alternative. @MartinSmith è decisamente interessato a qualcosa evidenziando l'effetto di TOP nel piano di esecuzione.

In parole povere, "Conteggio righe effettive" non è un conteggio delle righe elaborate da un operatore, è il numero di volte in cui viene chiamato il metodo GetNext () dell'operatore.

Tratto da BOL :

Gli operatori fisici inizializzano, raccolgono dati e chiudono. In particolare, l'operatore fisico può rispondere alle seguenti tre chiamate di metodo:

  • Init (): il metodo Init () consente a un operatore fisico di inizializzare se stesso e impostare le strutture di dati richieste. L'operatore fisico può ricevere molte chiamate Init (), sebbene in genere un operatore fisico ne riceva solo una.
  • GetNext (): il metodo GetNext () fa sì che un operatore fisico ottenga la prima o la successiva riga di dati. L'operatore fisico può ricevere zero o più chiamate GetNext ().
  • Close (): il metodo Close () fa sì che un operatore fisico esegua alcune operazioni di pulizia e si spenga. Un operatore fisico riceve solo una chiamata Close ().

Il metodo GetNext () restituisce una riga di dati e il numero di volte che viene chiamato viene visualizzato come ActualRows nell'output Showplan prodotto utilizzando SET STATISTICS PROFILE ON o SET STATISTICS XML ON.

Per completezza, è utile un piccolo background sugli operatori paralleli. Il lavoro viene distribuito a più flussi in un piano parallelo dal flusso di ripartizione o distribuisce operatori di flusso. Distribuiscono righe o pagine tra i thread utilizzando uno dei quattro meccanismi:

  • L'hash distribuisce le righe in base a un hash delle colonne nella riga
  • Round-robin distribuisce le righe ripetendo l'elenco dei thread in un ciclo
  • Broadcast distribuisce tutte le pagine o righe su tutti i thread
  • Il partizionamento della domanda viene utilizzato solo per le scansioni. I thread si girano, richiedono una pagina di dati dall'operatore, li elabora e al termine richiede un'altra pagina.

Il primo operatore di flusso di distribuzione (più a destra nel piano) utilizza il partizionamento della domanda sulle righe provenienti da una scansione costante. Esistono tre thread che chiamano GetNext () 6, 4 e 0 volte per un totale di 10 "Righe effettive":

<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>

Al prossimo operatore di distribuzione abbiamo di nuovo tre thread, questa volta con 50, 50 e 0 chiamate a GetNext () per un totale di 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>

È al prossimo operatore parallelo che probabilmente appaiono la causa e la spiegazione.

<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>

Quindi ora abbiamo 11 chiamate a GetNext (), dove ci aspettavamo di vederne 10.

Modifica: 13/11/2011

Bloccato a questo punto, sono andato a cercare risposte con i chap nell'indice cluster e @MikeWalsh ha gentilmente diretto @SQLKiwi qui .


7

1,004,588 è una figura che cresce molto anche nei miei test.

Vedo anche questo per il piano un po 'più semplice di seguito.

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

Piano

Altre figure di interesse nel piano di esecuzione sono

+----------------------------------+--------------+--------------+-----------------+
|                                  | 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 |
+----------------------------------+--------------+--------------+-----------------+

La mia ipotesi è solo che, poiché le attività vengono elaborate in parallelo, un'attività si trova nelle file di elaborazione a metà volo quando l'altra consegna la milionesima riga all'operatore di flussi di raccolta in modo da gestire ulteriori righe. Inoltre da questo articolo le righe vengono memorizzate in buffer e consegnate in batch a questo iteratore, quindi sembra abbastanza probabile che il numero di righe elaborate superi piuttosto che colpire esattamente la TOPspecifica in ogni caso.

modificare

Solo guardando questo in un po 'più in dettaglio. Ho notato che stavo ottenendo più varietà rispetto al solo 1,004,588conteggio delle righe sopra citato, quindi ho eseguito la query sopra in un ciclo per 1.000 iterazioni e acquisito i piani di esecuzione effettivi. Scartando gli 81 risultati per i quali il grado di parallelismo era zero, si ottenevano le seguenti cifre.

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

Si può vedere che 1.004.588 è stato di gran lunga il risultato più comune ma che in 3 occasioni si è verificato il caso peggiore e sono state elaborate 100.000.000 di righe. Il caso migliore osservato è stato il conteggio di 1.000.496 righe, che si è verificato 19 volte.

Lo script completo da riprodurre è in fondo alla revisione 2 di questa risposta (dovrà essere modificato se eseguito su un sistema con più di 2 processori).


1

Ritengo che il problema derivi dal fatto che più flussi possono elaborare la stessa riga in base al modo in cui le righe vengono incise tra i flussi.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.