Inkorrekte Anzahl der 'tatsächlichen' Zeilen im parallelen Plan

17

Dies ist eine rein akademische Frage, die insofern kein Problem darstellt, als dass ich nur daran interessiert bin, Erklärungen für das Verhalten zu hören.

Nehmen Sie eine Standardausgabe Itzik Ben-Gan Cross-Join-CTE-Tabelle:

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

Geben Sie eine Abfrage aus, mit der eine Tabelle mit 1 Million Zeilennummern erstellt wird:

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

Sehen Sie sich den parallelen Ausführungsplan für diese Abfrage an:

Paralleler Ausführungsplan

Beachten Sie, dass die "tatsächliche" Zeilenanzahl vor dem Operator "Datenströme sammeln" 1.004.588 beträgt. Nach dem Operator "Datenströme sammeln" beträgt die erwartete Zeilenzahl 1.000.000. Seltsamerweise ist der Wert nicht konsistent und variiert von Lauf zu Lauf. Das Ergebnis der COUNT ist immer korrekt.

Setzen Sie die Abfrage erneut ab und erzwingen Sie einen nicht parallelen Plan:

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

Dieses Mal zeigen alle Operatoren die korrekten 'tatsächlichen' Zeilenzahlen an.

Nicht paralleler Ausführungsplan

Ich habe dies auf 2005SP3 und 2008R2 bis jetzt versucht, die gleichen Ergebnisse auf beiden. Irgendwelche Gedanken darüber, was dies verursachen könnte?

Mark Storey-Smith
quelle

Antworten:

12

Zeilen werden über den Austausch intern von Produzent zu Consumer-Thread in Paketen (daher CXPACKET - Class Exchange Packet) weitergeleitet und nicht zeilenweise. In der Vermittlungsstelle ist eine gewisse Pufferung vorhanden. Außerdem muss der Aufruf zum Herunterfahren der Pipeline von der Verbraucherseite der Gather-Streams in einem Steuerpaket an die Producer-Threads zurückgegeben werden. Aufgrund der Zeitplanung und anderer interner Überlegungen haben parallele Pläne immer einen bestimmten Bremsweg.

Infolgedessen werden Sie häufig einen solchen Unterschied in der Zeilenanzahl feststellen, wenn tatsächlich weniger als das gesamte potenzielle Rowset eines Unterbaums erforderlich ist. In diesem Fall beendet der TOP die Ausführung vorzeitig.

Mehr Informationen:

Paul White sagt GoFundMonica
quelle
10

Ich denke, ich habe eine teilweise Erklärung dafür, aber bitte zögern Sie nicht, es abzuschießen oder Alternativen zu posten. @MartinSmith hat definitiv etwas vor, indem es den Effekt von TOP im Ausführungsplan hervorhebt.

Einfach ausgedrückt, ist 'Actual Row Count' nicht die Anzahl der Zeilen, die ein Operator verarbeitet, sondern die Häufigkeit, mit der die GetNext () -Methode des Operators aufgerufen wird.

Entnommen aus BOL :

Die physischen Operatoren initialisieren, erfassen Daten und schließen sie. Insbesondere kann der physische Operator die folgenden drei Methodenaufrufe beantworten:

  • Init (): Die Init () -Methode veranlasst einen physischen Operator, sich selbst zu initialisieren und alle erforderlichen Datenstrukturen einzurichten. Der physische Operator empfängt möglicherweise viele Init () - Aufrufe, obwohl ein physischer Operator normalerweise nur einen empfängt.
  • GetNext (): Die GetNext () -Methode veranlasst einen physischen Operator, die erste oder nachfolgende Datenzeile abzurufen. Der physische Operator kann null oder viele GetNext () - Aufrufe empfangen.
  • Close (): Die Close () -Methode veranlasst einen physischen Operator, einige Bereinigungsvorgänge durchzuführen und sich selbst herunterzufahren. Ein physischer Operator erhält nur einen Close () -Aufruf.

Die GetNext () -Methode gibt eine Datenzeile zurück und die Häufigkeit, mit der sie aufgerufen wird, wird in der Showplan-Ausgabe, die mit SET STATISTICS PROFILE ON oder SET STATISTICS XML ON erstellt wird, als ActualRows angezeigt.

Der Vollständigkeit halber sind einige Hintergrundinformationen zu den Paralleloperatoren hilfreich. Die Arbeit wird in einem parallelen Plan durch den Neupartitions-Stream oder die Verteilungs-Stream-Operatoren auf mehrere Streams verteilt. Diese verteilen Zeilen oder Seiten zwischen Threads mithilfe eines von vier Mechanismen:

  • Hash verteilt Zeilen basierend auf einem Hash der Spalten in der Zeile
  • Round-Robin verteilt Zeilen, indem die Liste der Threads in einer Schleife durchlaufen wird
  • Broadcast verteilt alle Seiten oder Zeilen auf alle Threads
  • Die Bedarfspartitionierung wird nur für Scans verwendet. Die Themen drehen sich, fordern eine Seite mit Daten vom Bediener an, verarbeiten sie und fordern eine weitere Seite an, wenn sie fertig sind.

Der erste Verteilungsstrom-Operator (ganz rechts im Plan) verwendet die Bedarfsaufteilung für die Zeilen, die von einem konstanten Scan stammen. Es gibt drei Threads, die GetNext () 6, 4 und 0-mal für insgesamt 10 "Aktuelle Zeilen" aufrufen:

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

Beim nächsten Verteilungsoperator haben wir wieder drei Threads, diesmal mit 50, 50 und 0 Aufrufen von GetNext () für insgesamt 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>

Beim nächsten Paralleloperator erscheinen möglicherweise die Ursache und die Erklärung.

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

Daher haben wir jetzt 11 Aufrufe von GetNext (), bei denen wir erwartet hatten, 10 zu sehen.

Bearbeiten: 2011-11-13

An diesem Punkt festgefahren , habe ich nach Antworten mit den Kerlen im Clustered-Index gesucht und @MikeWalsh hat freundlicherweise @SQLKiwi hierher geleitet .

Mark Storey-Smith
quelle
7

1,004,588 ist eine Figur, die auch in meinen Tests häufig vorkommt.

Ich sehe dies auch für den etwas einfacheren Plan unten.

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

Planen

Weitere für den Ausführungsplan interessante Zahlen sind

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

Ich vermute nur, dass sich eine Aufgabe in der Mitte der Verarbeitungszeilen befindet, wenn die andere die millionste Zeile an den Operator für Sammelströme liefert, da die Aufgaben parallel verarbeitet werden, sodass zusätzliche Zeilen verarbeitet werden. Darüber hinaus werden die Zeilen in diesem Artikel gepuffert und stapelweise an diesen Iterator übergeben, sodass es sehr wahrscheinlich ist, dass die Anzahl der zu verarbeitenden Zeilen TOPin jedem Fall die Spezifikation überschreitet, anstatt sie genau zu treffen .

Bearbeiten

Ich schaue mir das nur etwas genauer an. Mir ist aufgefallen, dass ich mehr Abwechslung als nur die 1,004,588oben angegebene Zeilenzahl habe. Deshalb habe ich die obige Abfrage in einer Schleife für 1.000 Iterationen ausgeführt und die tatsächlichen Ausführungspläne erfasst. Das Verwerfen der 81 Ergebnisse, für die der Parallelitätsgrad Null war, ergab die folgenden Zahlen.

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

Es ist ersichtlich, dass 1.004.588 das mit Abstand häufigste Ergebnis war, dass jedoch in drei Fällen der schlimmste Fall eintrat und 100.000.000 Zeilen verarbeitet wurden. Der beste beobachtete Fall war eine Zeilenanzahl von 1.000.496, die 19-mal auftrat.

Das zu reproduzierende vollständige Skript befindet sich am Ende von Revision 2 dieser Antwort (es muss angepasst werden, wenn es auf einem System mit mehr als 2 Prozessoren ausgeführt wird).

Martin Smith
quelle
1

Ich glaube, dass das Problem von der Tatsache herrührt, dass mehrere Streams dieselbe Zeile verarbeiten können, abhängig davon, wie die Zeilen zwischen den Streams aufgeteilt sind.

mrdenny
quelle