web-dev-qa-db-fra.com

SQL Server n'offre pas la fusion parallèle Joignez-vous à deux tables partitionnées équivalentes

Excuses d'avance pour la question très détaillée. J'ai inclus les questions pour générer un ensemble de données complète pour reproduire le problème et je exécute SQL Server 2012 sur une machine à 32 noyau. Cependant, je ne pense pas que cela soit spécifique à SQL Server 2012 et j'ai forcé un maxDop de 10 pour cet exemple particulier.

J'ai deux tables qui sont partitionnées à l'aide du même schéma de partition. Lorsque vous les rejoignez sur la colonne utilisée pour la partition, j'ai remarqué que SQL Server n'est pas capable d'optimiser une fusion parallèle Joindre autant que l'on pourrait s'attendre à ce qu'on puisse s'attendre et choisit ainsi d'utiliser une jointure de hachage à la place. Dans ce cas particulier, je suis capable de simuler manuellement une jointure de fusion parallèle beaucoup plus optimale en divisant la requête en 10 gammes disjointes basées sur la fonction de partition et en exécutant chacune de ces requêtes simultanément dans SSMS. En utilisant Waitfor pour les exécuter tout à la même époque, le résultat est que toutes les requêtes complètent à environ 40% du temps total utilisé par la jointure de hachage parallèle d'origine.

Existe-t-il un moyen d'obtenir SQL Server de faire en sa propre optimisation dans le cas de tables partitionnées équivalentes? Je comprends que SQL Server peut généralement entraîner beaucoup de frais généraux afin de faire une fusion parallèle, mais il semble qu'il existe une méthode de rayonnement très naturelle avec une surcharge minimale dans ce cas. Peut-être que c'est juste un cas spécialisé que l'optimiseur n'est pas encore assez intelligent pour reconnaître?

Voici le SQL pour configurer une définition de données simplifiée afin de reproduire ce problème:

/* 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)

Maintenant, nous sommes enfin prêts à reproduire la requête sous-optimale!

/* 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)

enter image description here

enter image description here

Cependant, en utilisant un seul thread pour traiter chaque partition (l'exemple de la première partition ci-dessous) entraînerait un plan beaucoup plus efficace. J'ai testé cela en exécutant une requête comme celle ci-dessous pour chacune des 10 partitions à précisément le même moment, et les 10 finis en un peu plus de 1 seconde:

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)

enter image description hereenter image description here

21
Geoff Patterson

Vous avez raison que l'optimiseur SQL Server ne préfère pas générer des plans de jointure parallèlement MERGE (cela coûte cette alternative assez élevée). Parallel MERGE _ nécessite toujours des échanges de reparition sur les deux entrées de jointure, et plus important encore, cela nécessite que la commande de ligne soit préservée à travers ces échanges.

Le parallélisme est plus efficace lorsque chaque thread peut fonctionner de manière indépendante; La préservation des commandes conduit souvent à des attentes de synchronisation fréquentes et peut finalement causer des échanges au déversement à tempdb pour résoudre une condition d'impasse intra-requis.

Ces problèmes peuvent être contournés en exécutant plusieurs instances de la touche une requête entière sur un fil chacun, avec chaque thread Traitement une gamme exclusive de données. Ce n'est pas une stratégie que l'optimiseur estime toutefois nativement. Comme c'est le cas, le modèle d'origine SQL Server pour le parallélisme enfreint la requête des échanges et exécute les segments de plan formés par ces diviseurs sur plusieurs threads.

Il existe des moyens d'atteindre des plans de requête entiers à exécuter sur plusieurs threads sur des gammes de données exclusives, mais elles nécessitent une supercheuse que tout le monde ne sera pas content (et ne sera pas pris en charge par Microsoft ou garanti de travailler à l'avenir). Une telle approche consiste à parcourir les partitions d'une table partitionnée et à donner à chaque fil la tâche de produire un sous-total. Le résultat est le SUM des comptes de rangée renvoyés par chaque fil indépendant:

L'obtention de numéros de partition est assez facile des métadonnées:

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;

Nous utilisons ensuite ces numéros pour conduire une jointure corrélée (APPLY) et le $PARTITION fonction pour limiter chaque thread au numéro de partition actuel:

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;

Le plan de requête montre une jointure MERGE étant effectuée pour chaque ligne du tableau @P. Les propriétés de numérisation d'index en cluster confirment que seule une seule partition est traitée sur chaque itération:

Apply serial plan

Malheureusement, cela ne résulte que de la transformation série séquentielle des partitions. Sur le jeu de données que vous avez fourni, mon ordinateur portable 4 cœurs (hyperthreadisé à 8) renvoie le résultat correct dans 7 secondes avec toutes les données en mémoire.

Pour obtenir les sous-plans MERGE à exécuter simultanément, nous avons besoin d'un plan parallèle dans lequel les identifiants de partition sont distribués sur les threads disponibles (MAXDOP) et chacun MERGE sous-plan. sur un seul fil à l'aide des données dans une partition. Malheureusement, l'optimiseur décide fréquemment contre le parallèle MERGE sur des motifs de coûts, et il n'y a pas de moyen documenté de forcer un plan parallèle. Il existe une manière non documentée (et non supportée), en utilisant Drapeau de trace 8649 :

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

Maintenant, le plan de requête montre des nombres de partition de @P étant distribué entre les threads sur une base ronde. Chaque thread traverse la partie intérieure des boucles imbriquées Joignez-vous à une seule partition, obtenant notre objectif de traiter simultanément les données disjointes. Le même résultat est maintenant retourné dans 3 secondes sur mes 8 hyper-noyaux, avec les huit à 100% d'utilisation.

Parallel APPLY

Je ne recommande pas que vous utilisiez cette technique nécessairement - voir mes avertissements précédents - mais cela répond à votre question.

Voir mon article Amélioration des performances de jointure partitionnée Pour plus de détails.

Colonne

Voir que vous utilisez SQL Server 2012 (et en supposant que c'est l'entreprise), vous avez également la possibilité d'utiliser un index de colonne. Cela montre le potentiel des jointures de hachage de mode par lots où une mémoire suffisante est disponible:

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

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

Avec ces index en place la requête ...

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

... Résultats dans le plan d'exécution suivant de l'optimiseur sans aucune supercherie:

Columnstore plan 1

Résultats corrects dans 2 secondes , mais éliminer le traitement en mode ligne pour l'agrégat scalaire contribue encore plus:

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;

Optimized columnstore

La requête optimisée du magasin de colonnes est exécutée dans 851ms .

Geoff Patterson a créé le rapport de bogue Partition wise jointures mais il a été fermé comme ne corrigera pas.

18
Paul White 9

La façon de faire fonctionner l'optimiseur comme vous le pensez mieux est via des indemnités de requête.

Dans ce cas, OPTION (MERGE JOIN)

Ou vous pouvez aller tout le porc et utiliser USE PLAN

0
podiluska