Pourquoi est-ce plus rapide et est-il sûr à utiliser? (OERE la première lettre est dans l'alphabet)


10

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


4
Petite réponse à votre note technique: AND LEFT(TRIM(largeTbl.last_name), 1) BETWEEN 'a' AND 'z' COLLATE LATIN1_GENERAL_CI_AIdevrait faire ce que vous voulez là sans vous obliger à lister tous les caractères et à avoir un code difficile à lire
Erik A

Avez-vous des lignes où la condition finale dans le WHEREest fausse? Notez en particulier que la comparaison peut être sensible à la casse.
jpmc26 du

@ErikvonAsmuth fait un excellent point. Mais, juste une petite note technique: pour SQL Server 2008 et 2008 R2, il est préférable d'utiliser les classements de la version "100" (si disponible pour la culture / l'environnement local utilisé). Ce serait donc le cas 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 donc Latin1_General_100_CI_AI_SCdans ce cas. Les versions> 100 (uniquement japonais jusqu'à présent) n'en ont pas (ou n'ont pas besoin) _SC(par exemple Japanese_XJIS_140_CI_AI).
Solomon Rutzky

Réponses:


9

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" entrez la description de l'image ici

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" entrez la description de l'image ici

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. entrez la description de l'image ici

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.

entrez la description de l'image ici entrez la description de l'image ici

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:

entrez la description de l'image ici

Au lieu d'une jointure en boucle imbriquée sur le non optimisé:

entrez la description de l'image ici

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 entrez la description de l'image ici

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 entrez la description de l'image ici 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.

  • Mise à jour des statistiques, recompilation pour essayer d'obtenir le meilleur plan.
  • Ajout d'OPTION (HASH JOIN, MERGE JOIN)à la requête
  • ...

Données de test + requêtes utilisées

CREATE TABLE #smallTableOfPeople(importantValue int, birthDate datetime2, first_name varchar(50),last_name varchar(50));
CREATE TABLE #largeTableOfPeople(importantValue int, birth_date datetime2, first_name varchar(50),last_name varchar(50));


set nocount on;
DECLARE @i int = 1
WHILE @i <= 1000
BEGIN
insert into #smallTableOfPeople (importantValue,birthDate,first_name,last_name)
VALUES(NULL, dateadd(mi,@i,'2018-01-18 11:05:29.067'),'Frodo','Baggins');

set @i += 1;
END


set nocount on;
DECLARE @j int = 1
WHILE @j <= 20000
BEGIN
insert into #largeTableOfPeople (importantValue,birth_Date,first_name,last_name)
VALUES(@j, dateadd(mi,@j,'2018-01-18 11:05:29.067'),'Frodo','Baggins');

set @j += 1;
END


SET STATISTICS IO, TIME ON;

SELECT  smallTbl.importantValue , largeTbl.importantValue
FROM #smallTableOfPeople smallTbl
JOIN #largeTableOfPeople largeTbl
    ON largeTbl.birth_date = smallTbl.birthDate
    AND DIFFERENCE(RTRIM(LTRIM(smallTbl.last_name)),RTRIM(LTRIM(largeTbl.last_name))) = 4
    AND DIFFERENCE(RTRIM(LTRIM(smallTbl.first_name)),RTRIM(LTRIM(largeTbl.first_name))) = 4
WHERE smallTbl.importantValue IS NULL
-- The following line is "the optimization"
AND LEFT(RTRIM(LTRIM(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','æ','ä','ø','å');

SELECT  smallTbl.importantValue , largeTbl.importantValue
FROM #smallTableOfPeople smallTbl
JOIN #largeTableOfPeople largeTbl
    ON largeTbl.birth_date = smallTbl.birthDate
    AND DIFFERENCE(RTRIM(LTRIM(smallTbl.last_name)),RTRIM(LTRIM(largeTbl.last_name))) = 4
    AND DIFFERENCE(RTRIM(LTRIM(smallTbl.first_name)),RTRIM(LTRIM(largeTbl.first_name))) = 4
WHERE smallTbl.importantValue IS NULL
-- The following line is "the optimization"
--AND LEFT(RTRIM(LTRIM(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','æ','ä','ø','å')




drop table #largeTableOfPeople;
drop table #smallTableOfPeople;

8

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é de 190,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

entrez la description de l'image ici

Deuxième plan

entrez la description de l'image ici

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 INconditions n'a éliminé que 51 lignes sur les 4 008 334 du tableau, mais l'optimiseur a estimé qu'il éliminerait beaucoup plus

entrez la description de l'image ici

   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', 'æ', 'ä', 'ø', 'å' ) 

De 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 TRIMSQL 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 avec TRIMcela, 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 le varchar(100)est rempli avec des espaces de fin comme cela se produit pour charl'estimation inférieure est utilisée

La INliste est étendue ORet SQL Server utilise une interruption exponentielle avec un maximum de 4 prédicats pris en compte. L' 219.707estimation est donc la suivante.

DECLARE @TableCardinality FLOAT = 4008334, 
        @DistinctColumnValueEstimate FLOAT = 34207

DECLARE @NotSelectivity float = 1 - (1/@DistinctColumnValueEstimate)

SELECT @TableCardinality * ( 1 - (
@NotSelectivity * 
SQRT(@NotSelectivity) * 
SQRT(SQRT(@NotSelectivity)) * 
SQRT(SQRT(SQRT(@NotSelectivity)))
))
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.