Comment utiliser le mode batch avec UNPIVOT (une jointure en boucle)?

12

J'ai une requête du formulaire suivant:

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

Cela prend chaque ligne d'une sous-requête soutenue par Columnstore ( ColumnstoreTable) et multiplie ces lignes. C'est essentiellement un UNPIVOT. La vraie requête est plus grande que cela. Cette partie de la requête alimente d'autres traitements.

Le problème ici est que cela CROSS APPLYest implémenté comme une jointure en boucle, ce qui est un choix raisonnable. Malheureusement, les jointures en boucle ne prennent pas en charge le mode batch.

Cette partie de la requête est très critique pour les performances et je soupçonne que son exécution en mode batch pourrait être très bénéfique pour les performances.

Comment puis-je réécrire cette requête afin de ne pas quitter le mode batch?

J'ai essayé d'utiliser une table temporaire au lieu de VALUES, mais cela n'a pas changé le fait qu'il n'y a pas de condition de jointure d'égalité pour la jointure de hachage.

boot4life
la source

Réponses:

12

Une approche pourrait être d'utiliser une table #temp pour les valeurs et également d'introduire une colonne équijoin factice pour permettre une jointure de hachage. Par exemple:

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

Plan de performance et de requête

Cette approche génère un plan de requête comme le suivant, et la correspondance de hachage est effectuée en mode batch:

entrez la description de l'image ici

Si je remplace l' SELECTinstruction par un SUMde l' CASEinstruction afin d'éviter d'avoir à diffuser toutes ces lignes sur la console, puis d'exécuter la requête sur une vraie table columnstore de ligne de 100MM que je traîne, je vois des performances assez bonnes pour générer les 300MM requis Lignes:

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

Et le plan réel montre une bonne parallélisation de la jointure de hachage.

entrez la description de l'image ici

Remarques sur la parallélisation de jointure de hachage lorsque toutes les lignes ont la même valeur

Les performances de cette requête dépendent fortement de chaque thread du côté sonde de la jointure ayant accès à la table de hachage complète (par opposition à une version partitionnée de hachage, qui mapperait toutes les lignes à un seul thread étant donné qu'il n'y a qu'une seule valeur distincte pour la dummycolonne).

Heureusement, cela est vrai dans ce cas (comme nous pouvons le voir par le manque d' Parallelismopérateur du côté de la sonde) et devrait être vrai de manière fiable car le mode batch crée une seule table de hachage qui est partagée entre les threads. Par conséquent, chaque thread peut prendre ses lignes dans le Columnstore Index Scanet les faire correspondre à cette table de hachage partagée. Dans SQL Server 2012, cette fonctionnalité était beaucoup moins prévisible car un déversement provoquait le redémarrage de l'opérateur en mode Row, perdant à la fois l'avantage du mode batch et nécessitant également un Repartition Streamsopérateur du côté sonde de la jointure, ce qui entraînerait un biais de thread dans ce cas . Permettre aux déversements de rester en mode batch est une amélioration majeure dans SQL Server 2014.

À ma connaissance, le mode ligne n'a pas cette capacité de table de hachage partagée. Toutefois, dans certains cas, généralement avec une estimation de moins de 100 lignes côté génération, SQL Server créera une copie distincte de la table de hachage pour chaque thread (identifiable par le Distribute Streamsdébut de la jointure de hachage). Cela peut être très puissant, mais il est beaucoup moins fiable que le mode batch car il dépend de vos estimations de cardinalité et SQL Server essaie d'évaluer les avantages par rapport au coût de construction d'une copie complète de la table de hachage pour chaque thread.

UNION ALL: une alternative plus simple

Paul White a souligné qu'une autre option, potentiellement plus simple, serait d'utiliser UNION ALLpour combiner les lignes pour chaque valeur. C'est probablement votre meilleur pari en supposant qu'il est facile pour vous de construire dynamiquement ce SQL. Par exemple:

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

Cela donne également un plan capable d'utiliser le mode batch et offre des performances encore meilleures que la réponse d'origine. (Bien que dans les deux cas, les performances soient assez rapides pour que toute sélection ou écriture des données dans une table devienne rapidement le goulot d'étranglement.) L' UNION ALLapproche évite également de jouer à des jeux comme la multiplication par 0. Parfois, il vaut mieux penser simple!

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

entrez la description de l'image ici

Geoff Patterson
la source
L'astuce UNION ALL est bonne à savoir, mais échoue si la requête source n'est pas seulement une table mais une requête coûteuse en elle-même.
boot4life