Wie nutze ich den Batch-Modus mit einem UNPIVOT (einem Loop-Join)?

12

Ich habe eine Frage zu folgendem Formular:

SELECT ...
FROM ColumnstoreTable cs
CROSS APPLY (
    SELECT *
    FROM (VALUES
        ('A', cs.DataA)
        , ('B', cs.DataB)
        , ('C', cs.DataC)
    ) x(Col0, Col1)
) someValues

Dies entnimmt jede Zeile aus einer Columnstore-gestützten Unterabfrage ( ColumnstoreTable) und multipliziert diese Zeilen. Dies ist im Wesentlichen ein UNPIVOT. Die eigentliche Abfrage ist größer als diese. Dieser Teil der Abfrage fließt in die andere Verarbeitung ein.

Das Problem hierbei ist, dass dies CROSS APPLYals Loop-Join implementiert ist, was eine vernünftige Wahl darstellt. Leider unterstützen Loop-Joins den Batch-Modus nicht.

Dieser Teil der Abfrage ist sehr leistungskritisch und ich vermute, dass die Ausführung im Batch-Modus der Leistung sehr zuträglich sein kann.

Wie kann ich diese Abfrage umschreiben, damit ich den Batch-Modus nicht verlasse?

Ich habe versucht, stattdessen eine temporäre Tabelle zu verwenden VALUES, aber das hat nichts an der Tatsache geändert, dass es keine Gleichheits-Join-Bedingung gibt, an der ein Hash-Join durchgeführt werden kann.

boot4life
quelle

Antworten:

12

Ein Ansatz könnte darin bestehen, eine #temp-Tabelle für die Werte zu verwenden und auch eine Dummy-Equijoin-Spalte einzuführen, um einen Hash-Join zu ermöglichen. Beispielsweise:

-- Create a #temp table with a dummy column to match the hash join
-- and the actual column you want
CREATE TABLE #values (dummy INT NOT NULL, Col0 CHAR(1) NOT NULL)
INSERT INTO #values (dummy, Col0)
VALUES (0, 'A'),
        (0, 'B'),
        (0, 'C')
GO

-- A similar query, but with a dummy equijoin condition to allow for a hash join
SELECT v.Col0, 
    CASE v.Col0 
        WHEN 'A' THEN cs.DataA 
        WHEN 'B' THEN cs.DataB 
        WHEN 'C' THEN cs.DataC
        END AS Col1
FROM ColumnstoreTable cs
JOIN #values v
    -- Join your dummy column to any numeric column on the columnstore,
    -- multiplying that column by 0 to ensure a match to all #values
    ON v.dummy = cs.DataA * 0

Leistungs- und Abfrageplan

Dieser Ansatz ergibt einen Abfrageplan wie den folgenden, und die Hash-Übereinstimmung wird im Stapelmodus ausgeführt:

Bildbeschreibung hier eingeben

Wenn ich die SELECTAnweisung durch eine SUMder CASEAnweisung ersetze, um zu vermeiden, dass alle diese Zeilen an die Konsole übertragen werden müssen, und dann die Abfrage für eine echte 100-mm-Zeilenspeichertabelle ausführe, die ich herumliegen habe, sehe ich eine ziemlich gute Leistung beim Generieren der erforderlichen 300-mm-Tabelle Reihen:

CPU time = 33803 ms, elapsed time = 4363 ms.

Und der aktuelle Plan zeigt eine gute Parallelisierung des Hash-Joins.

Bildbeschreibung hier eingeben

Hinweise zur Parallelisierung von Hash-Joins, wenn alle Zeilen denselben Wert haben

Die Leistung dieser Abfrage hängt stark davon ab, ob jeder Thread auf der Testseite des Joins Zugriff auf die vollständige Hash-Tabelle hat (im Gegensatz zu einer Version mit Hash-Partition, bei der alle Zeilen einem einzelnen Thread zugeordnet werden, vorausgesetzt, es gibt nur einen eindeutigen Wert) für die dummySpalte).

Glücklicherweise ist dies in diesem Fall der Fall (wie wir am Fehlen eines ParallelismOperators auf der Testseite sehen können) und sollte zuverlässig der Fall sein, da im Stapelmodus eine einzelne Hash-Tabelle erstellt wird, die von allen Threads gemeinsam genutzt wird. Daher kann jeder Thread seine Zeilen aus der Columnstore Index ScanTabelle entnehmen und sie mit der gemeinsam genutzten Hash-Tabelle abgleichen. In SQL Server 2012 war diese Funktionalität viel weniger vorhersehbar, da der Operator aufgrund eines Überlaufs im Zeilenmodus neu gestartet wurde, wodurch sowohl der Batch-Modus als auch ein Repartition StreamsOperator auf der Testseite des Joins verloren gingen, der in diesem Fall zu einem Thread-Versatz führen würde . Das Zulassen, dass Verschüttungen im Stapelverarbeitungsmodus verbleiben, ist eine wesentliche Verbesserung in SQL Server 2014.

Meines Wissens verfügt der Zeilenmodus nicht über diese Funktion für gemeinsam genutzte Hashtabellen. In einigen Fällen jedoch, normalerweise mit einer Schätzung von weniger als 100 Zeilen auf der Build-Seite, erstellt SQL Server für jeden Thread eine separate Kopie der Hash-Tabelle (erkennbar an dem Distribute Streamsin den Hash-Join führenden Thread ). Dies kann sehr leistungsfähig sein, ist jedoch weitaus weniger zuverlässig als der Batch-Modus, da dies von Ihren Kardinalitätsschätzungen abhängt und SQL Server versucht, die Vorteile im Vergleich zu den Kosten für die Erstellung einer vollständigen Kopie der Hash-Tabelle für jeden Thread zu bewerten.

UNION ALL: eine einfachere Alternative

Paul White wies darauf hin, dass eine andere und möglicherweise einfachere Option UNION ALLdarin besteht, die Zeilen für jeden Wert zu kombinieren. Dies ist wahrscheinlich die beste Wahl, vorausgesetzt, Sie können diese SQL problemlos dynamisch aufbauen. Beispielsweise:

SELECT 'A' AS Col0, c.DataA AS Col1
FROM ColumnstoreTable c
UNION ALL
SELECT 'B' AS Col0, c.DataB AS Col1
FROM ColumnstoreTable c
UNION ALL
SELECT 'C' AS Col0, c.DataC AS Col1
FROM ColumnstoreTable c

Dies ergibt auch einen Plan, der den Batch-Modus verwenden kann und eine noch bessere Leistung als die ursprüngliche Antwort bietet. (Obwohl in beiden Fällen die Leistung schnell genug ist, dass das Auswählen oder Schreiben der Daten in eine Tabelle schnell zum Engpass wird.) Bei diesem UNION ALLAnsatz wird auch vermieden, dass Spiele wie das Multiplizieren mit 0 gespielt werden. Manchmal ist es am besten, einfach zu denken!

CPU time = 8673 ms, elapsed time = 4270 ms.

Bildbeschreibung hier eingeben

Geoff Patterson
quelle
Der UNION ALL-Trick ist gut zu wissen, schlägt jedoch fehl, wenn die Quellabfrage nicht nur eine Tabelle, sondern eine kostspielige Abfrage für sich ist.
boot4life