web-dev-qa-db-fra.com

Utilisation de $ de la fonction de partition pour améliorer la performance de la requête

J'ai des tables qui sont partitionnées en fonction d'une colonne INT.

Je vois des requêtes qui utilisent $Partition Fonction Pour comparer le numéro de partition au lieu de comparer les données de champ réelles.

Par exemple, au lieu de dire:

select *
from T1 
    inner join T2 on T2.SnapshotKey = T1.SnapshotKey

ils ont été écrits comme ci-dessous:

select *
from T1 
    inner join T2 on $Partition.PF_Name(T2.SnapshotKey) = $Partition.PF_Name(T1.SnapshotKey)

PF_Name est le nom de la fonction de partition.

Je vois des commentaires sur ces questions que cela a été fait pour améliorer les performances, et lorsque j'exécute les deux requêtes, je vois différents dans le temps d'exécution et le plan d'exécution différent. Je ne sais pas comment ces deux requêtes sont différentes.

Ceci est une vraie requête:

-- this takes about 9 seconds 
select sum(PrinBal)
from fpc
    inner join gl on gl.SnapshotKey = fpc.SnapshotKey
where fpc.SnapshotKey = 201703

-- this takes about 5 seconds
select sum(PrinBal)
from fpc
    inner join gl on $Partition.SnapshotKeyPF(gl.SnapshotKey) = $Partition.SnapshotKeyPF(fpc.SnapshotKey)
where fpc.SnapshotKey = 201703

Et ci-dessous est le plan d'exécution de la requête réelle:

You can see the execution plan here

Désolé, je ne peux pas télécharger même un plan d'exécution désinfecté car les téléchargements sont surveillés par notre réseau et il pourrait s'agir d'une violation de la politique.

La question est la suivante: pourquoi les plans d'exécution sont différents et pourquoi la deuxième requête est plus rapide.

J'apprécie si quelqu'un peut partager une idée à ce sujet. Juste intéressé à savoir pourquoi ils sont différents. Plus rapide est mieux cependant.

Ceci se produit sur SQL Server 2014. Si je suis identique sur SQL Server 2012, le résultat sera différent et la première requête fonctionnera plus rapidement!

7
user71787

Pourquoi les plans d'exécution sont différents

Première requête

select sum(PrinBal)
from fpc
    inner join gl on gl.SnapshotKey = fpc.SnapshotKey
where fpc.SnapshotKey = 201703

L'optimiseur sait:

  • gl.SnapshotKey = fpc.SnapshotKey; et
  • fpc.SnapshotKey = 201703

donc, il peut en déduire:

  • gl.SnapshotKey = 201703

Comme si vous aviez écrit:

select sum(PrinBal)
from fpc
    inner join gl on gl.SnapshotKey = fpc.SnapshotKey
where fpc.SnapshotKey = 201703
and gl.SnapshotKey = 201703

La valeur littérale 201703 peut également être utilisée par l'optimiseur pour déterminer l'ID de partition. Avec les deux SnapshotKey prédicats (une donnée, une inféré) Cela signifie que l'optimiseur connaît l'ID de partition pour les deux tables.

Aller plus loin, avec une valeur littérale (201703) pour SnapshotKey maintenant disponible sur les deux tables, le prédicat de jointure:

  • gl.SnapshotKey = fpc.SnapshotKey

sIMPLIFIES À:

  • 201703 = 201703; ou simplement
  • true

Ce qui signifie qu'il n'y a pas de joint prédicat du tout. Le résultat est une jointure de croix logique. Exprimer le plan d'exécution final en utilisant la syntaxe T-SQL disponible la plus proche, c'est comme si vous l'avez écrit:

SELECT
    CASE
        WHEN SUM(Q1.c) = 0 THEN NULL
        ELSE SUM(Q1.s)
    END
FROM 
(
    SELECT c = COUNT_BIG(*), s = SUM(GL.PrinBal)
    FROM dbo.gl AS GL
    WHERE GL.SnapshotKey = 201703
    AND $PARTITION.PF(GL.SnapshotKey) = $PARTITION.PF(201703)
) AS Q1
CROSS JOIN
(
    SELECT Dummy = 1
    FROM dbo.fpc AS FPC
    WHERE FPC.SnapshotKey = 201703
    AND $PARTITION.PF(FPC.SnapshotKey) = $PARTITION.PF(201703)
) AS Q2;

Deuxième requête

select sum(PrinBal)
from fpc
    inner join gl on $Partition.PF(gl.SnapshotKey) = $Partition.PF(fpc.SnapshotKey)
where fpc.SnapshotKey = 201703

L'optimiseur ne peut plus déduire rien de gl.SnapshotKey, les simplifications et les transformations faites pour la première requête ne sont plus possibles.

En effet, sauf s'il est vrai que chaque partition ne détient qu'un seul SnapshotKey, la réécriture n'est pas garantie pour produire les mêmes résultats.

Encore une fois, exprimant le plan d'exécution produit à l'aide de la syntaxe T-SQL disponible la plus proche:

SELECT
    CASE
        WHEN SUM(Q2.c) = 0 THEN NULL
        ELSE SUM(Q2.s)
    END
FROM 
(
    SELECT
        Q1.PtnID,
        c = COUNT_BIG(*),
        s = SUM(Q1.PrinBal)
    FROM 
    (
        SELECT GL.PrinBal, PtnID = $PARTITION.PF(GL.SnapshotKey)
        FROM dbo.gl AS GL
    ) AS Q1
    GROUP BY
        Q1.PtnID
) AS Q2
CROSS APPLY
(
    SELECT Dummy = 1
    FROM dbo.fpc AS FPC
    WHERE
        $PARTITION.PF(FPC.SnapshotKey) = Q2.PtnID
        AND FPC.SnapshotKey = 201703
) AS Q3;

Cette fois, il n'y a pas de jointure de croix logique. Au lieu de cela, il y a une jointure corrélée (une application) sur l'ID de partition.


pourquoi la deuxième requête est plus rapide.

C'est difficile à évaluer à partir des informations données. En utilisant des données et des tableaux simulés en fonction des requêtes et de la planification de l'image fournie, j'ai trouvé la première requête surperformé la seconde dans chaque cas.

La même requête exprimée à l'aide d'une syntaxe différente peut souvent produire un plan d'exécution différent, simplement parce que l'optimiseur a commencé à partir d'un point différent et exploré des options dans un ordre différent avant de trouver un plan d'exécution approprié. La recherche de plan n'est pas exhaustive et toutes les transformations logiques éventuelles sont disponibles, le résultat final risque d'être différent. Comme indiqué ci-dessus, les deux requêtes n'expriment pas nécessairement la même exigence de toute façon (au moins étant donné les informations à la disposition de l'optimiseur).

Sur une note distincte, sachez que la mise en œuvre initiale de la colonne de colonne dans SQL Server 2012 (et à une moindre mesure, 2014) présente de nombreuses limitations, notamment sur le côté de l'optimisation des choses. Vous irez probablement mieux, et plus cohérent, des résultats en mettant à niveau une version plus récente (idéalement le tout dernier). Cela est particulièrement vrai si vous allez utiliser partitionnement.

Je ne vous recommanderai certainement pas que vous soyez dans l'habitude de réécrire les jointures en utilisant $PARTITION, sauf en tant que dernier recours, et avec une parfaite compréhension de ce que vous faites.

C'est à peu près tout ce que je peux dire sans pouvoir voir le schéma ou le détail de plan.

9
Paul White 9