Pour faire court, nous mettons à jour de petites tables de personnes avec des valeurs à partir d'une très grande table de personnes. Dans un test récent, cette mise à jour prend environ 5 minutes pour s'exécuter.
Nous sommes tombés sur ce qui semble être l'optimisation la plus stupide possible, qui semble parfaitement fonctionner! La même requête s'exécute désormais en moins de 2 minutes et produit parfaitement les mêmes résultats.
Voici la requête. La dernière ligne est ajoutée comme "l'optimisation". Pourquoi la diminution intense du temps de requête? Manquons-nous quelque chose? Cela pourrait-il entraîner des problèmes à l'avenir?
UPDATE smallTbl
SET smallTbl.importantValue = largeTbl.importantValue
FROM smallTableOfPeople smallTbl
JOIN largeTableOfPeople largeTbl
ON largeTbl.birth_date = smallTbl.birthDate
AND DIFFERENCE(TRIM(smallTbl.last_name),TRIM(largeTbl.last_name)) = 4
AND DIFFERENCE(TRIM(smallTbl.first_name),TRIM(largeTbl.first_name)) = 4
WHERE smallTbl.importantValue IS NULL
-- The following line is "the optimization"
AND LEFT(TRIM(largeTbl.last_name), 1) IN ('a','à','á','b','c','d','e','è','é','f','g','h','i','j','k','l','m','n','o','ô','ö','p','q','r','s','t','u','ü','v','w','x','y','z','æ','ä','ø','å')
Notes techniques: Nous savons que la liste des lettres à tester peut nécessiter quelques lettres supplémentaires. Nous sommes également conscients de la marge d'erreur évidente lors de l'utilisation de "DIFFERENCE".
Plan de requête (régulier): https://www.brentozar.com/pastetheplan/?id=rypV84y7V
Plan de requête (avec "optimisation"): https://www.brentozar.com/pastetheplan/?id=r1aC2my7E
AND LEFT(TRIM(largeTbl.last_name), 1) BETWEEN 'a' AND 'z' COLLATE LATIN1_GENERAL_CI_AI
devrait faire ce que vous voulez là sans vous obliger à lister tous les caractères et à avoir un code difficile à lireWHERE
est fausse? Notez en particulier que la comparaison peut être sensible à la casse.Latin1_General_100_CI_AI
. Et pour SQL Server 2012 et plus récent (via au moins SQL Server 2019), il est préférable d'utiliser les classements activés par les caractères supplémentaires dans la version la plus élevée pour les paramètres régionaux utilisés. Ce serait doncLatin1_General_100_CI_AI_SC
dans ce cas. Les versions> 100 (uniquement japonais jusqu'à présent) n'en ont pas (ou n'ont pas besoin)_SC
(par exempleJapanese_XJIS_140_CI_AI
).Réponses:
Cela dépend des données de vos tables, de vos index, .... Difficile à dire sans pouvoir comparer les plans d'exécution / les statistiques io + temps.
La différence que j'attendrais est le filtrage supplémentaire qui se produit avant le JOIN entre les deux tables. Dans mon exemple, j'ai changé les mises à jour en sélectionne pour réutiliser mes tables.
Le plan d'exécution avec "l'optimisation"
Plan d'exécution
Vous voyez clairement une opération de filtrage se produire, dans mes données de test, aucun enregistrement n'a été filtré et, par conséquent, aucune amélioration n'a été apportée.
Le plan d'exécution, sans "l'optimisation"
Plan d'exécution
Le filtre a disparu, ce qui signifie que nous devrons compter sur la jointure pour filtrer les enregistrements inutiles.
Autre (s) raison (s) Une autre raison / conséquence du changement de la requête pourrait être qu'un nouveau plan d'exécution a été créé lors du changement de la requête, ce qui s'avère plus rapide. Un exemple de ceci est le moteur qui choisit un opérateur Join différent, mais c'est juste une supposition à ce stade.
ÉDITER:
Clarification après avoir obtenu les deux plans de requête:
La requête lit les 550 millions de lignes de la grande table et les filtre.
Cela signifie que le prédicat est celui qui effectue la plupart du filtrage, pas le prédicat de recherche. Résultat: les données sont lues, mais beaucoup moins renvoyées.
Faire en sorte que le serveur SQL utilise un index différent (plan de requête) / ajouter un index pourrait résoudre ce problème.
Alors, pourquoi la requête d'optimisation n'a-t-elle pas le même problème?
Parce qu'un plan de requête différent est utilisé, avec une analyse au lieu d'une recherche.
Sans faire aucune recherche, mais en ne retournant que 4 millions de lignes avec lesquelles travailler.
Différence suivante
Sans tenir compte de la différence de mise à jour (rien n'est mis à jour sur la requête optimisée), une correspondance de hachage est utilisée sur la requête optimisée:
Au lieu d'une jointure en boucle imbriquée sur le non optimisé:
Une boucle imbriquée est préférable lorsqu'une table est petite et l'autre grande. Puisqu'ils sont tous deux proches de la même taille, je dirais que la correspondance de hachage est le meilleur choix dans ce cas.
Aperçu
La requête optimisée
Le plan de la requête optimisée présente un parallélisme, utilise une jointure par correspondance de hachage et doit effectuer moins de filtrage d'E / S résiduel. Il utilise également un bitmap pour éliminer les valeurs de clé qui ne peuvent produire aucune ligne de jointure. (De plus, rien n'est mis à jour)
La requête non optimisée Le plan de la requête non optimisée n'a aucun parallélisme, utilise une jointure en boucle imbriquée et doit effectuer un filtrage d'E / S résiduel sur 550 millions d'enregistrements. (La mise à jour est également en cours)
Que pourriez-vous faire pour améliorer la requête non optimisée?
Modification de l'index pour que prénom et nom de famille figurent dans la liste des colonnes clés:
CRÉER L'INDEX IX_largeTableOfPeople_birth_date_first_name_last_name sur dbo.largeTableOfPeople (date_naissance, prénom, nom_famille) include (id)
Mais en raison de l'utilisation des fonctions et de la taille de ce tableau, ce n'est peut-être pas la solution optimale.
(HASH JOIN, MERGE JOIN)
à la requêteDonnées de test + requêtes utilisées
la source
Il n'est pas certain que la deuxième requête soit en fait une amélioration.
Les plans d'exécution contiennent des QueryTimeStats qui montrent une différence beaucoup moins dramatique que celle indiquée dans la question.
Le plan lent avait un temps écoulé de
257,556 ms
(4 minutes 17 secondes). Le plan rapide avait un temps écoulé de190,992 ms
(3 minutes 11 secondes) malgré l'exécution avec un degré de parallélisme de 3.De plus, le deuxième plan s'exécutait dans une base de données où il n'y avait aucun travail à faire après la jointure.
Premier plan
Deuxième plan
Ce temps supplémentaire pourrait donc être expliqué par le travail nécessaire pour mettre à jour 3,5 millions de lignes (le travail requis par l'opérateur de mise à jour pour localiser ces lignes, verrouiller la page, écrire la mise à jour sur la page et le journal des transactions n'est pas négligeable)
Si cela est en fait reproductible lorsque vous comparez des choses semblables, alors l'explication est que vous venez d'avoir de la chance dans ce cas.
Le filtre avec les 37
IN
conditions n'a éliminé que 51 lignes sur les 4 008 334 du tableau, mais l'optimiseur a estimé qu'il éliminerait beaucoup plusDe telles estimations de cardinalité incorrectes sont généralement une mauvaise chose. Dans ce cas, il a produit un plan de forme différente (et parallèle) qui, apparemment (?) A mieux fonctionné pour vous malgré les déversements de hachage causés par la sous-estimation massive.
Sans
TRIM
SQL Server, il est capable de convertir cela en un intervalle de plage dans l'histogramme de la colonne de base et de donner des estimations beaucoup plus précises, mais avecTRIM
cela, il a juste recours à des suppositions.La nature de la supposition peut varier, mais l'estimation d'un seul prédicat sur
LEFT(TRIM(largeTbl.last_name), 1)
est dans certaines circonstances * juste estimée àtable_cardinality/estimated_number_of_distinct_column_values
.Je ne sais pas exactement dans quelles circonstances - la taille des données semble jouer un rôle. J'ai pu reproduire cela avec des types de données de grande longueur fixe comme ici, mais j'ai obtenu une estimation différente et plus élevée avec
varchar
(qui a simplement utilisé une estimation plate de 10% et estimé à 100 000 lignes). @Solomon Rutzky souligne que si levarchar(100)
est rempli avec des espaces de fin comme cela se produit pourchar
l'estimation inférieure est utiliséeLa
IN
liste est étendueOR
et SQL Server utilise une interruption exponentielle avec un maximum de 4 prédicats pris en compte. L'219.707
estimation est donc la suivante.la source