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
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"
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"
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.
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.
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:
Au lieu d'une jointure de 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.
Présentation
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ée 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.
(HASH JOIN, MERGE JOIN)
à la requêteDonné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;
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.
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
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)))
))