web-dev-qa-db-fra.com

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

Pour faire court, nous mettons à jour de petites tables de personnes avec les valeurs 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 idiote 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

10
JohnF

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" enter image description here

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" enter image description here

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, qui se trouve être 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.

MODIFIER:

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. enter image description here

Cela signifie que le prédicat est celui qui effectue la majeure partie 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.

enter image description hereenter image description here

Sans faire aucune recherche, mais uniquement en retournant 4M 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:

enter image description here

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

enter image description here

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.

Présentation

La requête optimiséeenter image description here

Le plan de la requête optimisée présente un parallélisme, utilise une jointure de correspondance de hachage et doit faire moins de filtrage résiduel IO. Il utilise également un bitmap pour éliminer les valeurs de clé qui ne peuvent pas produire de lignes de jointure. (Aussi rien est en cours de mise à jour)

La requête non optimiséeenter image description here Le plan de la requête non optimisée n'a pas de parallélisme, utilise une jointure en boucle imbriquée et doit effectuer un filtrage résiduel IO filtrage sur 550 millions d'enregistrements. (La mise à jour a également lieu)

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) inclure (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 de 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;
9
Randi Vertongen

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 fonctionnait dans une base de données où il n'y avait aucun travail à faire après la jointure.

Premier plan

enter image description here

Deuxième plan

enter image description here

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 ceci est en fait reproductible en comparant comme avec comme alors l'explication est que vous venez d'avoir de la chance dans ce cas.

Le filtre avec les conditions 37 IN n'a éliminé que 51 lignes sur les 4 008 334 du tableau, mais l'optimiseur a estimé qu'il éliminerait beaucoup plus

enter image description here

   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 incorrectes de cardinalité 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 le TRIM SQL Server est capable de le convertir en un intervalle de plage dans l'histogramme de la colonne de base et de donner des estimations beaucoup plus précises, mais avec le TRIM il a juste recours à des suppositions.

La nature de la supposition peut varier, mais l'estimation pour un seul prédicat sur LEFT(TRIM(largeTbl.last_name), 1) est dans certaines circonstances * juste estimé à 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 la varchar(100) est remplie d'espaces de fin comme cela se produit pour char l'estimation la plus basse est utilisée

La liste IN est développée en OR et SQL Server utilise arrêt exponentiel avec un maximum de 4 prédicats considérés. L'estimation 219.707 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)))
))
8
Martin Smith