Niedokładne „rzeczywiste” wiersze liczą się w planie równoległym

17

To jest pytanie czysto akademickie, o tyle, że nie powoduje problemu, a ja po prostu chcę usłyszeć wyjaśnienia tego zachowania.

Weźmy standardowy numer tabeli wyników CTE Itzika Ben-Gana:

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

Wydaj zapytanie, które utworzy 1 milionową tabelę numerów wierszy:

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

Spójrz na równoległy plan wykonania dla tego zapytania:

Plan wykonania równoległego

Należy zauważyć, że „rzeczywista” liczba wierszy przed operatorem zbierania strumieni wynosi 1 004 588. Po operatorze zbierania strumieni liczba przewidywanych wierszy wynosi 1 000 000. Co dziwniejsze, wartość nie jest spójna i będzie się różnić w zależności od serii. Wynik COUNT jest zawsze poprawny.

Uruchom ponownie zapytanie, wymuszając plan nierównoległy:

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

Tym razem wszyscy operatorzy pokazują prawidłowe „rzeczywiste” liczby wierszy.

Nierównoległy plan wykonania

Próbowałem tego do tej pory na 2005SP3 i 2008R2, takie same wyniki na obu. Wszelkie przemyślenia, co może to powodować?

Mark Storey-Smith
źródło

Odpowiedzi:

12

Rzędy są przekazywane wewnętrznie między wymianami od producenta do wątku konsumenckiego w pakietach (stąd CXPACKET - pakiet wymiany klasy), a nie jeden po drugim. W ramach wymiany występuje pewna ilość buforowania. Ponadto wezwanie do zamknięcia rurociągu od strony konsumenta strumieni Gather Streams musi zostać przekazane w pakiecie kontrolnym z powrotem do wątków producenta. Planowanie i inne czynniki wewnętrzne oznaczają, że równoległe plany zawsze mają pewną „odległość zatrzymania”.

W rezultacie często widzisz tego rodzaju różnicę liczby wierszy, w której faktycznie wymagany jest mniej niż cały potencjalny zestaw wierszy poddrzewa. W tym przypadku TOP przenosi wykonanie na „wczesny koniec”.

Więcej informacji:

Paul White przywraca Monikę
źródło
10

Wydaje mi się, że mogę to częściowo wyjaśnić, ale prosimy o zastrzelenie go lub opublikowanie alternatyw. @MartinSmith zdecydowanie coś interesuje, podkreślając efekt TOP w planie wykonania.

Mówiąc prościej, „Rzeczywista liczba wierszy” nie jest liczbą wierszy przetwarzanych przez operatora, jest to liczba wywołań metody GetNext () operatora.

Zaczerpnięte z BOL :

Fizyczni operatorzy inicjują, zbierają dane i zamykają. W szczególności operator fizyczny może odbierać następujące trzy wywołania metod:

  • Init (): Metoda Init () powoduje, że operator fizyczny inicjuje się i konfiguruje wymagane struktury danych. Operator fizyczny może odbierać wiele wywołań Init (), chociaż zwykle operator fizyczny odbiera tylko jedno.
  • GetNext (): Metoda GetNext () powoduje, że operator fizyczny otrzymuje pierwszy lub kolejny wiersz danych. Operator fizyczny może odbierać zero lub wiele wywołań GetNext ().
  • Close (): Metoda Close () powoduje, że operator fizyczny wykonuje niektóre operacje czyszczenia i sam się wyłącza. Operator fizyczny odbiera tylko jedno wywołanie Close ().

Metoda GetNext () zwraca jeden wiersz danych, a liczba wywołań pojawia się jako ActualRows na wyjściu Showplan, który jest tworzony przy użyciu SET STATISTICS PROFILE ON lub SET STATISTICS XML ON.

Ze względu na kompletność przydatne jest małe tło operatorów równoległych. Praca jest dystrybuowana do wielu strumieni w planie równoległym przez strumień podziału lub operatorów dystrybucji. Dzielą one wiersze lub strony między wątkami za pomocą jednego z czterech mechanizmów:

  • Hash rozdziela wiersze na podstawie skrótu kolumn w wierszu
  • Round-robin dystrybuuje wiersze, iterując po liście wątków w pętli
  • Broadcast rozpowszechnia wszystkie strony lub wiersze we wszystkich wątkach
  • Partycjonowanie na żądanie jest używane tylko do skanowania. Wątki się rozwijają, żądają strony danych od operatora, przetwarzają ją i po zakończeniu żądają kolejnej strony.

Pierwszy operator strumienia dystrybucyjnego (najbardziej w planie) wykorzystuje partycjonowanie na żądanie w wierszach pochodzących ze stałego skanowania. Istnieją trzy wątki, które wywołują GetNext () 6, 4 i 0 razy w sumie dla 10 „Rzeczywistych wierszy”:

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

Przy kolejnym operatorze dystrybucji znów mamy trzy wątki, tym razem z 50, 50 i 0 wywołaniami GetNext (), co daje łącznie 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>

Prawdopodobnie pojawia się przyczyna i wyjaśnienie następnego operatora równoległego.

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

Mamy więc teraz 11 wywołań funkcji GetNext (), w której spodziewaliśmy się 10.

Edycja: 13.11.2011

Utknąłem w tym momencie i zacząłem szukać odpowiedzi z listami w indeksie klastrowym i @MikeWalsh uprzejmie skierował tutaj @SQLKiwi .

Mark Storey-Smith
źródło
7

1,004,588 jest liczbą, która często pojawia się w moich testach.

Widzę to również dla nieco prostszego planu poniżej.

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

Plan

Inne dane zainteresowania planem wykonania to

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

Sądzę tylko, że ponieważ zadania są przetwarzane równolegle, jedno zadanie znajduje się w środkowych rzędach przetwarzania lotu, podczas gdy drugie dostarcza operatorowi milionowy wiersz operatorowi gromadzenia strumieni, więc obsługiwane są dodatkowe wiersze. Dodatkowo z tego artykułu wiersze są buforowane i dostarczane partiami do tego iteratora, więc wydaje się całkiem prawdopodobne, że liczba przetwarzanych wierszy przekroczyłaby, a nie trafiła dokładnie w TOPspecyfikację.

Edytować

Wystarczy spojrzeć na to bardziej szczegółowo. Zauważyłem, że zyskuję większą różnorodność niż tylko 1,004,588podana powyżej liczba wierszy, więc uruchomiłem powyższe zapytanie w pętli dla 1000 iteracji i uchwyciłem rzeczywiste plany wykonania. Odrzucenie 81 wyników, dla których stopień równoległości wynosił zero, dało następujące liczby.

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

Można zauważyć, że 1 004 588 było zdecydowanie najczęstszym wynikiem, ale 3 razy wystąpił najgorszy możliwy przypadek i przetworzono 100 000 000 wierszy. Najlepszym zaobserwowanym przypadkiem była liczba 1 000 496 wierszy, co wystąpiło 19 razy.

Pełny skrypt do reprodukcji znajduje się na końcu wersji 2 tej odpowiedzi (będzie wymagał dostosowania, jeśli zostanie uruchomiony w systemie z więcej niż 2 procesorami).

Martin Smith
źródło
1

Uważam, że problem wynika z faktu, że wiele strumieni może przetwarzać ten sam wiersz w zależności od tego, jak wiersze są rzeźbione między strumieniami.

mrdenny
źródło