SQL Server optimiert den parallelen Merge-Join für zwei entsprechend partitionierte Tabellen nicht

21

Entschuldigung im Voraus für die sehr detaillierte Frage. Ich habe Abfragen zum Generieren eines vollständigen Datensatzes zum Reproduzieren des Problems eingefügt, und ich führe SQL Server 2012 auf einem 32-Core-Computer aus. Ich glaube jedoch nicht, dass dies spezifisch für SQL Server 2012 ist, und ich habe für dieses Beispiel eine MAXDOP von 10 erzwungen.

Ich habe zwei Tabellen, die nach demselben Partitionsschema partitioniert sind. Als ich sie in der für die Partitionierung verwendeten Spalte zusammenfügte, bemerkte ich, dass SQL Server einen parallelen Merge-Join nicht so optimieren kann, wie es zu erwarten war, und entschied sich stattdessen für die Verwendung eines HASH JOIN. In diesem speziellen Fall kann ich einen viel optimaleren parallelen MERGE JOIN manuell simulieren, indem ich die Abfrage basierend auf der Partitionsfunktion in 10 disjunkte Bereiche aufteile und jede dieser Abfragen gleichzeitig in SSMS ausführe. Wenn Sie WAITFOR verwenden, um alle Abfragen genau zur gleichen Zeit auszuführen, werden alle Abfragen in ~ 40% der Gesamtzeit ausgeführt, die von der ursprünglichen parallelen HASH JOIN-Operation verwendet wird.

Gibt es eine Möglichkeit, SQL Server zu veranlassen, diese Optimierung bei entsprechend partitionierten Tabellen selbst durchzuführen? Ich verstehe, dass SQL Server im Allgemeinen viel Aufwand verursachen kann, um einen MERGE JOIN parallel zu machen, aber es scheint, als gäbe es in diesem Fall eine sehr natürliche Sharding-Methode mit minimalem Aufwand. Vielleicht handelt es sich nur um einen speziellen Fall, für den der Optimierer noch nicht klug genug ist, um ihn zu erkennen?

Hier ist die SQL zum Einrichten eines vereinfachten Datensatzes, um dieses Problem zu reproduzieren:

/* Create the first test data table */
CREATE TABLE test_transaction_properties 
    ( transactionID INT NOT NULL IDENTITY(1,1)
    , prop1 INT NULL
    , prop2 FLOAT NULL
    )

/* Populate table with pseudo-random data (the specific data doesn't matter too much for this example) */
;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
)
, E2(N) AS (SELECT 1 FROM E1 a CROSS JOIN E1 b)
, E4(N) AS (SELECT 1 FROM E2 a CROSS JOIN E2 b)
, E8(N) AS (SELECT 1 FROM E4 a CROSS JOIN E4 b)
INSERT INTO test_transaction_properties WITH (TABLOCK) (prop1, prop2)
SELECT TOP 10000000 (ABS(CAST(CAST(NEWID() AS VARBINARY) AS INT)) % 5) + 1 AS prop1
                , ABS(CAST(CAST(NEWID() AS VARBINARY) AS INT)) * rand() AS prop2
FROM E8

/* Create the second test data table */
CREATE TABLE test_transaction_item_detail
    ( transactionID INT NOT NULL
    , productID INT NOT NULL
    , sales FLOAT NULL
    , units INT NULL
    )

 /* Populate the second table such that each transaction has one or more items
     (again, the specific data doesn't matter too much for this example) */
INSERT INTO test_transaction_item_detail WITH (TABLOCK) (transactionID, productID, sales, units)
SELECT t.transactionID, p.productID, 100 AS sales, 1 AS units
FROM test_transaction_properties t
JOIN (
    SELECT 1 as productRank, 1 as productId
    UNION ALL SELECT 2 as productRank, 12 as productId
    UNION ALL SELECT 3 as productRank, 123 as productId
    UNION ALL SELECT 4 as productRank, 1234 as productId
    UNION ALL SELECT 5 as productRank, 12345 as productId
) p
    ON p.productRank <= t.prop1

/* Divides the transactions evenly into 10 partitions */
CREATE PARTITION FUNCTION [pf_test_transactionId] (INT)
AS RANGE RIGHT
FOR VALUES
(1,1000001,2000001,3000001,4000001,5000001,6000001,7000001,8000001,9000001)

CREATE PARTITION SCHEME [ps_test_transactionId]
AS PARTITION [pf_test_transactionId]
ALL TO ( [PRIMARY] )

/* Apply the same partition scheme to both test data tables */
ALTER TABLE test_transaction_properties
ADD CONSTRAINT PK_test_transaction_properties
PRIMARY KEY (transactionID)
ON ps_test_transactionId (transactionID)

ALTER TABLE test_transaction_item_detail
ADD CONSTRAINT PK_test_transaction_item_detail
PRIMARY KEY (transactionID, productID)
ON ps_test_transactionId (transactionID)

Jetzt können wir endlich die suboptimale Abfrage reproduzieren!

/* This query produces a HASH JOIN using 20 threads without the MAXDOP hint,
    and the same behavior holds in that case.
    For simplicity here, I have limited it to 10 threads. */
SELECT COUNT(*)
FROM test_transaction_item_detail i
JOIN test_transaction_properties t
    ON t.transactionID = i.transactionID
OPTION (MAXDOP 10)

Bildbeschreibung hier eingeben

Bildbeschreibung hier eingeben

Die Verwendung eines einzelnen Threads zur Verarbeitung jeder Partition (Beispiel für die erste Partition unten) würde jedoch zu einem viel effizienteren Plan führen. Ich habe dies getestet, indem ich eine Abfrage wie die folgende für jede der 10 Partitionen genau im selben Moment ausgeführt habe und alle 10 in etwas mehr als 1 Sekunde fertig waren:

SELECT COUNT(*)
FROM test_transaction_item_detail i
INNER MERGE JOIN test_transaction_properties t
    ON t.transactionID = i.transactionID
WHERE t.transactionID BETWEEN 1 AND 1000000
OPTION (MAXDOP 1)

Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben

Geoff Patterson
quelle

Antworten:

18

Sie haben Recht, dass das SQL Server-Optimierungsprogramm keine MERGEPläne für parallele Verknüpfungen generiert (dies kostet diese Alternative ziemlich viel). Parallel MERGEerfordert immer die Neupartitionierung der Austausche an beiden Join-Eingängen, und noch wichtiger ist, dass die Zeilenreihenfolge über diese Austausche hinweg beibehalten wird.

Die Parallelität ist am effizientesten, wenn jeder Thread unabhängig ausgeführt werden kann. Die Beibehaltung von Bestellungen führt häufig zu häufigen Synchronisationswarten und kann letztendlich dazu führen, dass der Datenaustausch überflüssig wird tempdb, um einen Deadlock-Zustand innerhalb der Abfrage zu beheben.

Diese Probleme können umgangen werden, indem mehrere Instanzen der gesamten Abfrage in jeweils einem Thread ausgeführt werden, wobei jeder Thread einen exklusiven Datenbereich verarbeitet. Dies ist jedoch keine Strategie, die der Optimierer nativ berücksichtigt. Das ursprüngliche SQL Server-Modell für Parallelität unterbricht die Abfrage beim Austausch und führt die Plansegmente, die durch diese Teilungen gebildet werden, auf mehreren Threads aus.

Es gibt Möglichkeiten, vollständige Abfragepläne für mehrere Threads über exklusive Datasetbereiche auszuführen, aber sie erfordern Tricks, mit denen nicht jeder zufrieden sein wird (und die von Microsoft nicht unterstützt werden oder die künftig garantiert nicht mehr funktionieren werden). Ein solcher Ansatz besteht darin, die Partitionen einer partitionierten Tabelle zu durchlaufen und jedem Thread die Aufgabe zuzuweisen, eine Zwischensumme zu erstellen. Das Ergebnis ist die SUMAnzahl der Zeilen, die von jedem unabhängigen Thread zurückgegeben werden:

Das Abrufen von Partitionsnummern aus Metadaten ist recht einfach:

DECLARE @P AS TABLE
(
    partition_number integer PRIMARY KEY
);

INSERT @P (partition_number)
SELECT
    p.partition_number
FROM sys.partitions AS p 
WHERE 
    p.[object_id] = OBJECT_ID(N'test_transaction_properties', N'U')
    AND p.index_id = 1;

Wir verwenden diese Zahlen dann, um einen korrelierten Join ( APPLY) $PARTITIONzu erzeugen , und die Funktion, um jeden Thread auf die aktuelle Partitionsnummer zu beschränken:

SELECT
    row_count = SUM(Subtotals.cnt)
FROM @P AS p
CROSS APPLY
(
    SELECT
        cnt = COUNT_BIG(*)
    FROM dbo.test_transaction_item_detail AS i
    JOIN dbo.test_transaction_properties AS t ON
        t.transactionID = i.transactionID
    WHERE 
        $PARTITION.pf_test_transactionId(t.transactionID) = p.partition_number
        AND $PARTITION.pf_test_transactionId(i.transactionID) = p.partition_number
) AS SubTotals;

Der Abfrageplan zeigt einen MERGEJoin, der für jede Zeile in der Tabelle ausgeführt wird @P. Die Eigenschaften der Clustered-Index-Suche bestätigen, dass bei jeder Iteration nur eine Partition verarbeitet wird:

Serienplan anwenden

Leider führt dies nur zu einer sequentiellen seriellen Verarbeitung von Partitionen. In dem von Ihnen angegebenen Datensatz gibt mein 4-Core-Laptop (Hyperthreading auf 8) das richtige Ergebnis in 7 Sekunden mit allen Daten im Speicher zurück.

Damit die MERGEUnterpläne gleichzeitig ausgeführt werden können, benötigen wir einen parallelen Plan, bei dem die Partitions-IDs auf die verfügbaren Threads ( MAXDOP) verteilt werden und jeder MERGEUnterplan unter Verwendung der Daten in einer Partition auf einem einzelnen Thread ausgeführt wird. Leider entscheidet sich der Optimierer häufig MERGEaus Kostengründen gegen Parallel und es gibt keine dokumentierte Möglichkeit, einen Parallelplan zu erzwingen. Es gibt eine undokumentierte (und nicht unterstützte) Möglichkeit, Trace-Flag 8649 zu verwenden :

SELECT
    row_count = SUM(Subtotals.cnt)
FROM @P AS p
CROSS APPLY
(
    SELECT
        cnt = COUNT_BIG(*)
    FROM dbo.test_transaction_item_detail AS i
    JOIN dbo.test_transaction_properties AS t ON
        t.transactionID = i.transactionID
    WHERE 
        $PARTITION.pf_test_transactionId(t.transactionID) = p.partition_number
        AND $PARTITION.pf_test_transactionId(i.transactionID) = p.partition_number
) AS SubTotals
OPTION (QUERYTRACEON 8649);

Der Abfrageplan zeigt nun Partitionsnummern an @P, die nicht im Round-Robin-Verfahren auf die Threads verteilt wurden. Jeder Thread führt die Innenseite des Join mit verschachtelten Schleifen für eine einzelne Partition aus, um unser Ziel zu erreichen, disjunkte Daten gleichzeitig zu verarbeiten. Das gleiche Ergebnis wird jetzt in 3 Sekunden auf meinen 8 Hyperkernen zurückgegeben, wobei alle acht bei 100% Auslastung sind.

Parallele ANWENDUNG

Ich empfehle nicht, dass Sie diese Technik unbedingt anwenden - siehe meine früheren Warnungen -, aber sie geht Ihre Frage an.

Weitere Informationen finden Sie in meinem Artikel Verbessern der Join-Leistung für partitionierte Tabellen .

Säulenspeicher

Da Sie SQL Server 2012 verwenden (und davon ausgehen, dass es sich um Enterprise handelt), haben Sie auch die Möglichkeit, einen Columnstore-Index zu verwenden. Dies zeigt das Potenzial von Batch-Modus-Hash-Joins, wenn genügend Speicher verfügbar ist:

CREATE NONCLUSTERED COLUMNSTORE INDEX cs 
ON dbo.test_transaction_properties (transactionID);

CREATE NONCLUSTERED COLUMNSTORE INDEX cs 
ON dbo.test_transaction_item_detail (transactionID);

Mit diesen Indizes an Ort und Stelle der Abfrage ...

SELECT
    COUNT_BIG(*)
FROM dbo.test_transaction_properties AS ttp
JOIN dbo.test_transaction_item_detail AS ttid ON
    ttid.transactionID = ttp.transactionID;

... ergibt den folgenden Ausführungsplan vom Optimierer ohne irgendwelche Tricks:

Säulenspeicherplan 1

Das Korrigieren der Ergebnisse dauert 2 Sekunden , aber das Eliminieren der Zeilenmodusverarbeitung für das Skalaraggregat trägt noch mehr dazu bei:

SELECT
    COUNT_BIG(*)
FROM dbo.test_transaction_properties AS ttp
JOIN dbo.test_transaction_item_detail AS ttid ON
    ttid.transactionID = ttp.transactionID
GROUP BY
    ttp.transactionID % 1;

Optimierter Säulenspeicher

Die optimierte Spaltenspeicherabfrage wird in 851 ms ausgeführt .

Geoff Patterson hat den Fehlerbericht " Partition Wise Joins" erstellt, der jedoch als "Won't Fix" geschlossen wurde.

Paul White sagt GoFundMonica
quelle
5
Hervorragende Lernerfahrung hier. Danke dir. +1
Edward Dortland
1
Vielen Dank, Paul! Hier gibt es großartige Informationen, die die Frage mit Sicherheit im Detail beantworten.
Geoff Patterson
2
Vielen Dank, Paul! Hier gibt es großartige Informationen, die die Frage mit Sicherheit im Detail beantworten. Wir befinden uns in einer gemischten SQL 2008/2012-Umgebung, aber ich werde den Spaltenspeicher für die Zukunft weiter untersuchen. Natürlich wünsche ich mir immer noch, dass SQL Server in meinem Anwendungsfall einen parallelen Merge-Join effektiv nutzen könnte - und den viel geringeren Speicherbedarf, den er haben könnte oder stimmen Sie darüber ab: connect.microsoft.com/SQLServer/feedback/details/759266/…
Geoff Patterson
0

Sie können den Optimierer mithilfe von Abfragetipps so einsetzen, wie Sie es für besser halten.

In diesem Fall, OPTION (MERGE JOIN)

Oder Sie können das ganze Schwein gehen und verwenden USE PLAN

podiluska
quelle
Ich würde das nicht persönlich machen: Der Hinweis wird nur für das aktuelle Datenvolumen und die Verteilung von Nutzen sein.
9.
Das Interessante ist, dass die Verwendung von OPTION (MERGE JOIN) zu einem weitaus schlechteren Plan führt. Das Optimierungsprogramm ist nicht intelligent genug, um zu erkennen, dass der MERGE JOIN von der Partitionsfunktion gelöscht werden kann. Wenn Sie diesen Hinweis anwenden, dauert die Abfrage ca. 46 Sekunden. Sehr frustrierend!
@gbn was ist vermutlich der Grund, warum der Optimierer in erster Linie für den Hash-Join geht?
@ gpatterson Wie nervig! :)
Was passiert, wenn Sie die Partitionierung manuell über eine Vereinigung erzwingen (dh Ihre kurze Abfrage wird nicht mit den anderen ähnlichen Abfragen in Zusammenhang gebracht)?