web-dev-qa-db-fra.com

pourquoi cette jointure gauche est-elle plus rapide qu'une jointure interne?

J'optimise donc cette requête et je suis sûr que dans ce cas, je peux remplacer une jointure interne par une jointure gauche sans affecter les données. Cependant, je ne sais pas vraiment pourquoi cela est plus rapide. Voici la requête:

SELECT DISTINCT  cl.NAME                            AS company_name,
                     cl.id                              AS company_id,
                     ep.Plan__c                           AS plan_id,
                     ep.Employee__c,
                     ep.id,
                     do.Subcategory__c,
                     ep.Plan_Type__c,
                     pt.SubType,
                     Sum((pt.[shares] * fvh.[ValuePerShare])) AS TotalValue,
                     ppe.Deferral_Option__c,
                     dt.Defer_type_Code,
                     do.Short_Code__c,
                     pt.ContributionYear
  FROM   dbo.ParticipantTrades pt WITH (NOLOCK) 
        INNER JOIN dbo.PayoutPathElection ppe WITH (NOLOCK) 
                ON pt.payoutPathElectionID = ppe.Id 
        INNER JOIN dbo.DeferralOption do WITH (NOLOCK) 
                ON ppe.Deferral_Option__c = do.id 
        INNER JOIN dbo.EmployeePlan ep WITH (NOLOCK) 
                ON pt.employeePlan = ep.Id 
        LEFT JOIN dbo.DeferralType dt WITH (NOLOCK) 
                ON pt.deferralType = dt.defID
        INNER JOIN dbo.Fnc_lastfundvalue('2019-01-30') AS fvh 
                ON pt.fund = fvh.Fund
        INNER JOIN dbo.Clients cl with (NOLOCK)
                ON ep.Company__c = cl.Id
  WHERE ep.Company__c = '0017000001WL1HfAAL' AND ep.Plan_Type__c  LIKE '%' AND pt.tradeDate <= '2019-01-30'
  Group by   cl.NAME,
             cl.id,
             ep.Plan__c,
             ep.Employee__c,
             ep.id,
             do.Subcategory__c,
             ep.Plan_Type__c,
             pt.SubType,
             ppe.Deferral_Option__c,
             dt.Defer_type_Code,
             do.Short_Code__c,
             pt.ContributionYear

Le goulot d'étranglement se trouve à la jointure de la fonction de valeur de table (Fnc_lastfundvalue). Mon intuition sur la raison pour laquelle le changer en une jointure gauche est plus rapide est qu'il peut alors réorganiser les jointures et cela provoque moins de débordement dans tempdb? Voici le plan de requête avant et après la modification de INNER JOIN dbo.Fnc_lastfundvalue .. en LEFT JOIN dbo.Fnc_lastfundvalue ..

Avant (29 secondes): https://www.brentozar.com/pastetheplan/?id=Bk1Y-WqEE

Après (3 secondes): https://www.brentozar.com/pastetheplan/?id=B1hoW-544

NB: Les plans d'exécution ci-dessus sont créés sur une boîte de développement. Le serveur de production est toujours sur SQL Server 2008.

4
Rick

Il semble que la différence vient du fait que le plan lent a été exécuté en premier.

Le plan lent fonctionnait clairement contre un cache froid car il montre des lectures physiques supplémentaires et a réussi à accumuler 15 secondes supplémentaires de PAGEIOLATCH_SH attend par rapport au cas rapide.

Il semble que cela ait affecté à la fois l'exécution du TVF lui-même (qui a pris 5.5 secondes par rapport à 1.35 dans le cas rapide.) et le plan plus large utilisant le résultat de celui-ci.

Vous dites dans les commentaires qu'à la deuxième exécution, il a fallu 11 secondes. C'est encore 3 fois plus lent que le plan rapide (3.446 secondes) n'explique donc pas toute la différence de performances.

Le principal problème que vous rencontrez est dû à la mauvaise estimation par défaut des TVF multi-instructions (correction de 100 lignes dans les niveaux de compatibilité 2014/2016 et 1 dans les versions antérieures). En réalité, votre TVF renvoie 1,715 Lignes.

Dans le cas des jointures internes, car les jointures internes sont associatives et commutatives, elles peuvent être réorganisées de manière flexible et sont déplacées vers la partie la plus profonde de l'arbre. Cela aurait du sens si une ligne était effectivement retournée car elle pourrait réduire le nombre de lignes au début pour les autres jointures du plan.

Le plan d'exécution est ci-dessous. Les annotations roses sont "Temps réel écoulé (ms)" du XML.

enter image description here

En raison de l'estimation sur 1 ligne, il commence mal en se joignant à dbo.ParticipantTrades et en sélectionnant un plan avec des boucles et des recherches imbriquées. Cette boucle imbriquée a un temps écoulé de 13.976 secondes (dont la plupart ont probablement été consacrées à attendre le tri de déversement immédiatement en amont pour lui demander des lignes, avec 3.26 secondes prises sur les recherches elles-mêmes et associées IO attend les lectures physiques chez cet opérateur).

923,646 des lignes sont émises à partir de la jointure par rapport à une estimation 1423.18 et cette erreur d'estimation se propage vers le haut dans le plan à travers 3 sortes et une jointure de hachage se répandant au fur et à mesure (en raison de la sous-estimation de la ligne)

Lorsque vous ajoutez le LEFT JOIN il ne peut pas être réorganisé aussi librement et la jointure se produit beaucoup plus haut dans le plan (où cela ferait moins de dégâts dans la problématique INNER JOIN Cas). La sémantique de jointure externe aide ici de toute façon.

Alors que l'estimation du nombre de lignes sortant du TVF est toujours 1 cela n'affecte pas les estimations de la jointure dans laquelle il est directement impliqué (SQL Server suppose que la cardinalité sortant de cette jointure sera la même que celle de l'autre sous-arborescence de la jointure et c'est en fait ce qui se passe - une jointure externe ne peut pas réduire ce nombre car une ligne non jointe passerait toujours - juste avec NULL pour les colonnes fvh).

Il y a toujours des erreurs d'estimation de cardinalité dans le plan de jointure externe mais pas de la même ampleur et il demande une allocation de mémoire suffisante pour éviter de se répandre n'importe où.

Ce problème d'estimation de cardinalité a été résolu dans la version la plus récente avec exécution entrelacée . En attendant (comme vous le dites dans les commentaires, il doit être compatible avec 2008), vous pouvez l'entrelacer manuellement en stockant le résultat TVF dans un #temp table et jonction sur celle-ci pour permettre la prise en compte du résultat intermédiaire (et des statistiques de colonne).

6
Martin Smith