web-dev-qa-db-fra.com

Pourquoi la modification de l'ordre des colonnes de jointure déclarées introduit-elle un tri?

J'ai deux tables avec des colonnes clés nommées, typées et indexées de manière identique. L'un d'eux a un unique index clusterisé, l'autre a un non unique.

La configuration du test

Script d'installation, y compris des statistiques réalistes:

DROP TABLE IF EXISTS #left;
DROP TABLE IF EXISTS #right;

CREATE TABLE #left (
    a       char(4) NOT NULL,
    b       char(2) NOT NULL,
    c       varchar(13) NOT NULL,
    d       bit NOT NULL,
    e       char(4) NOT NULL,
    f       char(25) NULL,
    g       char(25) NOT NULL,
    h       char(25) NULL
    --- and a few other columns
);

CREATE UNIQUE CLUSTERED INDEX IX ON #left (a, b, c, d, e, f, g, h)

UPDATE STATISTICS #left WITH ROWCOUNT=63800000, PAGECOUNT=186000;

CREATE TABLE #right (
    a       char(4) NOT NULL,
    b       char(2) NOT NULL,
    c       varchar(13) NOT NULL,
    d       bit NOT NULL,
    e       char(4) NOT NULL,
    f       char(25) NULL,
    g       char(25) NOT NULL,
    h       char(25) NULL
    --- and a few other columns
);

CREATE CLUSTERED INDEX IX ON #right (a, b, c, d, e, f, g, h)

UPDATE STATISTICS #right WITH ROWCOUNT=55700000, PAGECOUNT=128000;

La repro

Lorsque je joins ces deux tables sur leurs clés de clustering, je m'attends à une jointure MERGE un-à-plusieurs, comme ceci:

SELECT *
FROM #left AS l
LEFT JOIN #right AS r ON
    l.a=r.a AND
    l.b=r.b AND
    l.c=r.c AND
    l.d=r.d AND
    l.e=r.e AND
    l.f=r.f AND
    l.g=r.g AND
    l.h=r.h
WHERE l.a='2018';

Voici le plan de requête que je veux:

This is what I want.

(Peu importe les avertissements, ils ont à voir avec les fausses statistiques.)

Cependant, si je change l'ordre des colonnes autour de la jointure, comme ceci:

SELECT *
FROM #left AS l
LEFT JOIN #right AS r ON
    l.c=r.c AND     -- used to be third
    l.a=r.a AND     -- used to be first
    l.b=r.b AND     -- used to be second
    l.d=r.d AND
    l.e=r.e AND
    l.f=r.f AND
    l.g=r.g AND
    l.h=r.h
WHERE l.a='2018';

... ça arrive:

The query plan after changing the declared column order in the join.

L'opérateur de tri semble ordonner les flux selon l'ordre déclaré de la jointure, c'est-à-dire c, a, b, d, e, f, g, h, qui ajoute une opération de blocage à mon plan de requête.

Choses que j'ai regardées

  • J'ai essayé de changer les colonnes en NOT NULL, mêmes résultats.
  • La table d'origine a été créée avec ANSI_PADDING OFF, mais en le créant avec ANSI_PADDING ON n'affecte pas ce plan.
  • J'ai essayé un INNER JOIN au lieu de LEFT JOIN, pas de changement.
  • Je l'ai découvert sur un SP2 Enterprise 2014, créé une repro sur un développeur 2017 (CU actuel).
  • La suppression de la clause WHERE sur la colonne d'index de tête génère le bon plan, mais cela affecte en quelque sorte les résultats .. :)

Enfin, nous arrivons à la question

  • Est-ce intentionnel?
  • Puis-je éliminer le tri sans changer la requête (qui est le code du fournisseur, donc je préfère vraiment ne pas ...). Je peux changer la table et les index.
41
Daniel Hutmacher

Est-ce intentionnel?

C'est par conception, oui. La meilleure source publique pour cette affirmation a malheureusement été perdue lorsque Microsoft a retiré le site de commentaires Connect, effaçant de nombreux commentaires utiles des développeurs de l'équipe SQL Server.

Quoi qu'il en soit, la conception actuelle de l'optimiseur ne cherche pas activement à éviter les tris inutiles en soi . Ceci est le plus souvent rencontré avec les fonctions de fenêtrage et similaires, mais peut également être observé avec d'autres opérateurs sensibles à l'ordre, et en particulier à l'ordre préservé entre opérateurs.

Néanmoins, l'optimiseur est assez bon (dans de nombreux cas) pour éviter un tri inutile, mais ce résultat se produit normalement pour des raisons autres que d'essayer agressivement différentes combinaisons de commande. En ce sens, il ne s'agit pas tant d '"espace de recherche" que des interactions complexes entre les caractéristiques de l'optimiseur orthogonal qui se sont avérées augmenter la qualité générale du plan à un coût acceptable.

Par exemple, le tri peut souvent être évité simplement en faisant correspondre une exigence de commande (par exemple, le niveau supérieur ORDER BY) À un index existant. Trivialement dans votre cas, cela pourrait signifier ajouter ORDER BY l.a, l.b, l.c, l.d, l.e, l.f, l.g, l.h; Mais c'est une simplification excessive (et inacceptable parce que vous ne voulez pas changer la requête).

Plus généralement, chaque groupe de mémos peut être associé à des propriétés requises ou souhaitées, qui peuvent inclure un ordre d'entrée. Lorsqu'il n'y a pas de raison évidente d'appliquer une commande particulière (par exemple pour satisfaire un ORDER BY, Ou pour garantir des résultats corrects d'une commande- opérateur physique sensible), il y a un élément de "chance". J'ai écrit plus sur les détails de cela en ce qui concerne la jointure de fusion (en mode union ou join) dans Éviter les tris avec la concaténation de jointure de fusion . Une grande partie de cela va au-delà de la surface supportée du produit, alors traitez-le comme informatif et sujet à changement.

Dans votre cas particulier, oui, vous pouvez ajuster l'indexation comme le suggère jadarnel27 pour éviter les tris; bien qu'il y ait peu de raisons de préférer une fusion ici. Vous pouvez également suggérer un choix entre une jointure physique de hachage ou de boucle avec OPTION(HASH JOIN, LOOP JOIN) en utilisant un guide de plan sans modifier la requête, en fonction de votre connaissance des données, et le compromis entre le meilleur, le pire et la moyenne- performances du boîtier.

Enfin, par curiosité, notez que les tris peuvent être évités avec un simple ORDER BY l.b, Au prix d'une jointure de fusion plusieurs à plusieurs potentiellement moins efficace sur b seul, avec un complexe résiduel. Je mentionne cela principalement comme une illustration de l'interaction entre les fonctionnalités de l'optimiseur que j'ai mentionnées précédemment et de la manière dont les exigences de niveau supérieur peuvent se propager.

30
Paul White 9

Puis-je éliminer le tri sans changer la requête (qui est le code du fournisseur, donc je préfère vraiment ne pas ...). Je peux changer la table et les index.

Si vous pouvez modifier les index, changez l'ordre de l'index sur #right pour faire correspondre l'ordre des filtres dans la jointure supprime le tri (pour moi):

CREATE CLUSTERED INDEX IX ON #right (c, a, b, d, e, f, g, h)

Étonnamment (pour moi, au moins), cela n'entraîne aucune requête se terminant par un tri.

Est-ce intentionnel?

En regardant la sortie de quelques drapeaux de trace étranges , il y a une différence intéressante dans la structure finale du mémo:

screenshot of final memo structure for each query

Comme vous pouvez le voir dans le "Groupe racine" en haut, les deux requêtes ont la possibilité d'utiliser une jointure de fusion comme opération physique principale pour exécuter cette requête.

Bonne requête

La jointure sans le tri est piloté par le groupe 29 option 1 et le groupe 31 option 1 (qui sont chacun des balayages de plage sur les index concernés). Il est filtré par le groupe 27 (non illustré), qui est la série d'opérations de comparaison logique qui filtrent la jointure.

Mauvaise requête

Celui avec le tri est déterminé par les (nouvelles) options 3 de chacun de ces deux groupes (29 et 31). L'option 3 effectue un tri physique sur les résultats des analyses de plage mentionnées précédemment (option 1 de chacun de ces groupes).

Pourquoi?

Pour une raison quelconque, l'option d'utiliser directement 29.1 et 31.1 comme sources pour la jointure de fusion n'est même pas disponible pour l'optimiseur dans la deuxième requête. Sinon, je pense qu'il serait répertorié sous le groupe racine parmi les autres options. S'il était disponible, il choisirait certainement ceux-là plutôt que les opérations de tri massivement plus coûteuses.

Je ne peux que conclure que:

  • il s'agit d'un bogue (ou plus probablement d'une limitation) dans l'algorithme de recherche de l'optimiseur
    • la modification des index et des jointures pour n'avoir que 5 clés supprime le tri pour la deuxième requête (6, 7 et 8 clés ont toutes le tri).
    • Cela implique que l'espace de recherche avec 8 clés est si grand que l'optimiseur n'a tout simplement pas le temps d'identifier la solution non triable comme une option viable avant qu'elle ne se termine tôt avec la raison "plan suffisamment bon trouvé"
    • il me semble un peu bogué que l'ordre des conditions de jointure influence autant le processus de recherche de l'optimiseur, mais c'est vraiment un peu au-dessus de ma tête
  • le tri est nécessaire pour garantir l'exactitude des résultats
    • celui-ci semble peu probable, car la requête peut s'exécuter sans le tri lorsqu'il y a moins de clés, ou les clés sont spécifiées dans un ordre différent

J'espère que quelqu'un peut venir et expliquer pourquoi le tri est nécessaire, mais je pensais que la différence dans le bâtiment Memo était suffisamment intéressante pour être publiée comme réponse.

20
Josh Darnell