J'ai une requête qui s'exécute dans un laps de temps acceptable, mais je veux en tirer le maximum de performances.
L'opération que j'essaie d'améliorer est la "recherche d'index" à droite du plan, à partir du nœud 17.
J'ai ajouté des indices appropriés, mais les estimations que je reçois pour cette opération représentent la moitié de ce qu'elles sont censées être.
J'ai cherché à changer mes index et à ajouter une table temporaire et à réécrire la requête, mais je n'ai pas pu la simplifier davantage pour obtenir les bonnes estimations.
Quelqu'un a-t-il des suggestions sur ce que je peux essayer d'autre?
Le plan complet et ses détails peuvent être trouvés ici .
Le plan non anonymisé peut être trouvé ici.
Mise à jour:
J'ai le sentiment que la version initiale de la question a soulevé beaucoup de confusion, donc je vais ajouter le code original avec quelques explications.
create procedure [dbo].[someProcedure] @asType int, @customAttrValIds idlist readonly
as
begin
set nocount on;
declare @dist_ca_id int;
select *
into #temp
from @customAttrValIds
where id is not null;
select @dist_ca_id = count(distinct CustomAttrID)
from CustomAttributeValues c
inner join #temp a on c.Id = a.id;
select a.Id
, a.AssortmentId
from Assortments a
inner join AssortmentCustomAttributeValues acav
on a.Id = acav.Assortment_Id
inner join CustomAttributeValues cav
on cav.Id = acav.CustomAttributeValue_Id
where a.AssortmentType = @asType
and acav.CustomAttributeValue_Id in (select id from #temp)
group by a.AssortmentId
, a.Id
having count(distinct cav.CustomAttrID) = @dist_ca_id
option(recompile);
end
Réponses:
Pourquoi la dénomination initiale étrange dans le lien pasteThePlan?
Réponse : Parce que j'ai utilisé le plan anonymisé de SQL Sentry Plan Explorer.
Pourquoi
OPTION RECOMPILE
?Réponse : Parce que je peux me permettre des recompilations afin d'éviter de renifler les paramètres (les données sont / pourraient être faussées). J'ai testé et je suis satisfait du plan que l'Optimizer génère lors de l'utilisation
OPTION RECOMPILE
.WITH SCHEMABINDING
?Réponse : Je voudrais vraiment éviter cela et ne l'utiliser que lorsque j'aurai une vue indexée. Quoi qu'il en soit, il s'agit d'une fonction système (
COUNT()
), donc inutileSCHEMABINDING
ici.
Réponses à plus de questions possibles:
Pourquoi est-ce que j'utilise
INSERT INTO #temp FROM @customAttrributeValues
?Réponse : Parce que j'ai remarqué et je sais maintenant que lorsque vous utilisez des variables connectées à une requête, toutes les estimations qui sortent du travail avec une variable sont toujours 1. Et j'ai testé de placer les données dans une table temporaire et l' estimation est alors égale aux lignes réelles .
Pourquoi ai-je utilisé
and acav.CustomAttributeValue_Id in (select id from #temp)
?Réponse : J'aurais pu le remplacer par un JOIN sur #temp, mais les développeurs étaient très confus et ont proposé l'
IN
option. Je ne pense pas vraiment qu'il y aurait une différence même en remplaçant et de toute façon, cela ne pose aucun problème.
la source
#temp
création et l'utilisation seraient un problème de performances, pas un gain. Vous enregistrez dans une table non indexée pour être utilisé une seule fois. Essayez de le supprimer complètement (et éventuellement de le changerin (select id from #temp)
enexists
sous-requête.select id from @customAttrValIds
place deselect id from #temp
et le nombre estimé de lignes était1
pour la variable et3
pour #temp (qui correspondait au nombre réel de lignes). C'est pourquoi je l'ai remplacé@
par#
. Et je DOIS me souviens d' un discours (de Brent O ou Aaron Bertrand) où ils ont dit que lorsque vous utilisez une variable TBL les estimations pour ce seront toujours 1. Et comme une amélioration pour obtenir de meilleures estimations qu'ils utiliseraient une table temporaire.Réponses:
Le plan a été compilé sur une instance SQL Server 2008 R2 RTM (build 10.50.1600). Vous devez installer le Service Pack 3 (build 10.50.6000), suivi des derniers correctifs pour l'amener à la dernière build (actuelle) 10.50.6542. Ceci est important pour un certain nombre de raisons, notamment la sécurité, les corrections de bogues et les nouvelles fonctionnalités.
L'optimisation de l'incorporation des paramètres
Concernant la présente question, SQL Server 2008 R2 RTM ne prend pas en charge l'optimisation d'intégration de paramètres (PEO) pour
OPTION (RECOMPILE)
. À l'heure actuelle, vous payez le coût des recompilations sans réaliser l'un des principaux avantages.Lorsque PEO est disponible, SQL Server peut utiliser les valeurs littérales stockées dans les variables et paramètres locaux directement dans le plan de requête. Cela peut entraîner des simplifications spectaculaires et des augmentations de performances. Il y a plus d'informations à ce sujet dans mon article, Reniflage de paramètres, Incorporation et Options RECOMPILE .
Hachage, tri et échange de déversements
Ceux-ci ne sont affichés dans les plans d'exécution que lorsque la requête a été compilée sur SQL Server 2012 ou version ultérieure. Dans les versions antérieures, nous devions surveiller les déversements pendant l'exécution de la requête à l'aide du profileur ou des événements étendus. Les déversements entraînent toujours des E / S physiques vers (et à partir de) la mémoire de sauvegarde persistante tempdb , ce qui peut avoir des conséquences importantes sur les performances, en particulier si le déversement est important ou si le chemin d'E / S est sous pression.
Dans votre plan d'exécution, il existe deux opérateurs Hash Match (Aggregate). La mémoire réservée à la table de hachage est basée sur l' estimation des lignes de sortie (en d'autres termes, elle est proportionnelle au nombre de groupes trouvés lors de l'exécution). La mémoire accordée est fixée juste avant le début de l'exécution et ne peut pas augmenter pendant l'exécution, quelle que soit la quantité de mémoire libre de l'instance. Dans le plan fourni, les deux opérateurs Hash Match (Aggregate) produisent plus de lignes que l'optimiseur prévu, et peuvent donc rencontrer un déversement sur tempdb lors de l'exécution.
Il existe également un opérateur Hash Match (Inner Join) dans le plan. La mémoire réservée à la table de hachage est basée sur l' estimation des lignes d'entrée côté sonde . L'entrée de la sonde estime 847 399 lignes, mais 1 223 636 sont rencontrées au moment de l'exécution. Cet excès peut également provoquer un déversement de hachage.
Agrégat redondant
Le Hash Match (Aggregate) au nœud 8 effectue une opération de regroupement
(Assortment_Id, CustomAttrID)
, mais les lignes d'entrée sont égales aux lignes de sortie:Cela suggère que la combinaison de colonnes est une clé (donc le regroupement est sémantiquement inutile). Le coût d'exécution de l'agrégat redondant est augmenté par la nécessité de passer deux fois 1,4 millions de lignes sur les échanges de partitionnement de hachage (les opérateurs de parallélisme de chaque côté).
Étant donné que les colonnes impliquées proviennent de tables différentes, il est plus difficile que d'habitude de communiquer ces informations d'unicité à l'optimiseur, ce qui permet d'éviter l'opération de regroupement redondant et les échanges inutiles.
Distribution inefficace des threads
Comme indiqué dans la réponse de Joe Obbish , l'échange au niveau du nœud 14 utilise le partitionnement de hachage pour répartir les lignes entre les threads. Malheureusement, le petit nombre de lignes et les planificateurs disponibles signifient que les trois lignes se retrouvent sur un seul thread. Le plan apparemment parallèle s'exécute en série (avec surcharge parallèle) jusqu'à l'échange au nœud 9.
Vous pouvez résoudre ce problème (pour obtenir un partage alterné ou un partitionnement de diffusion) en éliminant le tri distinct au nœud 13. La façon la plus simple de le faire est de créer une clé primaire en cluster sur la
#temp
table et d'effectuer l'opération distincte lors du chargement de la table:Mise en cache des statistiques de table temporaire
Malgré l'utilisation de
OPTION (RECOMPILE)
, SQL Server peut toujours mettre en cache l'objet table temporaire et ses statistiques associées entre les appels de procédure. Il s'agit généralement d'une optimisation des performances bienvenue, mais si la table temporaire est remplie avec une quantité similaire de données sur les appels de procédure adjacents, le plan recompilé peut être basé sur des statistiques incorrectes (mises en cache à partir d'une exécution précédente). Ceci est détaillé dans mes articles, Tables temporaires dans les procédures stockées et Mise en cache des tables temporaires expliquées .Pour éviter cela, utilisez-le
OPTION (RECOMPILE)
avec un explicite uneUPDATE STATISTICS #TempTable
fois la table temporaire remplie et avant qu'elle soit référencée dans une requête.Réécriture de requête
Cette partie suppose que les modifications apportées à la création de la
#Temp
table ont déjà été apportées.Compte tenu des coûts de déversements de hachage possibles et de l'agrégat redondant (et des échanges environnants), il peut être avantageux de matérialiser l'ensemble au nœud 10:
Le
PRIMARY KEY
est ajouté dans une étape distincte pour garantir que la génération d'index contient des informations de cardinalité précises et pour éviter le problème de mise en cache des statistiques de table temporaire.Cette matérialisation est susceptible de se produire en mémoire (en évitant les E / S tempdb ) si l'instance dispose de suffisamment de mémoire disponible. Cela est encore plus probable une fois que vous effectuez une mise à niveau vers SQL Server 2012 (SP1 CU10 / SP2 CU1 ou version ultérieure), ce qui a amélioré le comportement d'écriture désirée .
Cette action donne à l'optimiseur des informations de cardinalité précises sur l'ensemble intermédiaire, lui permet de créer des statistiques et nous permet de déclarer
(Assortment_Id, CustomAttrID)
comme clé.Le plan pour la population de
#Temp2
devrait ressembler à ceci (notez l'analyse d'index clusterisé de#Temp
, pas de tri distinct, et l'échange utilise maintenant le partitionnement en ligne à tour de rôle):Avec cet ensemble disponible, la requête finale devient:
Nous pourrions réécrire manuellement le
COUNT_BIG(DISTINCT...
comme simpleCOUNT_BIG(*)
, mais avec les nouvelles informations clés, l'optimiseur le fait pour nous:Le plan final peut utiliser une jointure boucle / hachage / fusion en fonction des informations statistiques sur les données auxquelles je n'ai pas accès. Une autre petite note: j'ai supposé qu'il
CREATE [UNIQUE?] NONCLUSTERED INDEX IX_ ON dbo.Assortments (AssortmentType, Id, AssortmentId);
existe un indice comme .Quoi qu'il en soit, l'important dans les plans finaux est que les estimations devraient être bien meilleures, et la séquence complexe des opérations de regroupement a été réduite à un seul agrégat de flux (qui ne nécessite pas de mémoire et ne peut donc pas se répandre sur le disque).
Il est difficile de dire que les performances seront en fait meilleures dans ce cas avec le tableau temporaire supplémentaire, mais les estimations et les choix de plan seront beaucoup plus résistants aux changements de volume et de distribution des données au fil du temps. Cela peut être plus précieux à long terme qu'une petite augmentation des performances aujourd'hui. Dans tous les cas, vous disposez désormais de bien plus d'informations sur lesquelles baser votre décision finale.
la source
Les estimations de cardinalité de votre requête sont en fait très bonnes. Il est rare que le nombre de lignes estimées corresponde exactement au nombre de lignes réelles, surtout lorsque vous avez autant de jointures. Les estimations de cardinalité de jointure sont difficiles à obtenir pour l'optimiseur. Une chose importante à noter est que le nombre de lignes estimées pour la partie intérieure de la boucle imbriquée est par exécution de cette boucle. Ainsi, lorsque SQL Server indique que 463869 lignes seront extraites avec l'index, la véritable estimation dans ce cas est le nombre d'exécutions (2) * 463869 = 927738, ce qui n'est pas si éloigné du nombre réel de lignes, 1391608. Étonnamment, le nombre de lignes estimées est presque parfait immédiatement après la jointure de la boucle imbriquée au nœud ID 10.
Les estimations de cardinalité médiocres sont généralement un problème lorsque l'optimiseur de requête sélectionne le mauvais plan ou n'accorde pas suffisamment de mémoire au plan. Je ne vois aucun déversement sur tempdb pour ce plan, donc la mémoire semble correcte. Pour la jointure de boucle imbriquée que vous appelez, vous disposez d'une petite table externe et d'une table interne indexée. Qu'est-ce qui ne va pas avec ça? Pour être précis, qu'attendriez-vous que l'optimiseur de requêtes fasse différemment ici?
En termes d'amélioration des performances, ce qui me semble le plus important, c'est que SQL Server utilise un algorithme de hachage pour distribuer des lignes parallèles, ce qui se traduit par le fait qu'elles se trouvent toutes sur le même thread:
Par conséquent, un thread effectue tout le travail avec la recherche d'index:
Cela signifie que votre requête ne s'exécute pas en parallèle jusqu'à ce que l'opérateur de répartition des flux à l'ID de nœud 9. Vous souhaitiez probablement un partitionnement à tour de rôle afin que chaque ligne se retrouve sur son propre thread. Cela permettra à deux threads de faire la recherche d'index pour l'ID de nœud 17. L'ajout d'un
TOP
opérateur superflu peut vous permettre un partitionnement à tour de rôle. Je peux ajouter des détails ici si vous le souhaitez.Si vous voulez vraiment vous concentrer sur les estimations de cardinalité, vous pouvez placer les lignes après la première jointure dans une table temporaire. Si vous collectez des statistiques sur la table temporaire qui fournissent à l'optimiseur plus d'informations sur la table externe pour la jointure de boucle imbriquée que vous avez appelée. Cela pourrait également entraîner un partitionnement à tour de rôle.
Si vous n'utilisez pas d'indicateurs de trace 4199 ou 2301, vous pouvez les considérer. L'indicateur de trace 4199 offre une grande variété de correctifs d'optimisation, mais ils peuvent dégrader certaines charges de travail. L'indicateur de trace 2301 modifie certaines des hypothèses de cardinalité de jointure de l'optimiseur de requête et le rend plus difficile à travailler. Dans les deux cas, testez soigneusement avant de les activer.
la source
Je crois qu'obtenir une meilleure estimation sur cette jointure ne changera pas le plan, à moins que 1,4 mill soit une partie suffisante de la table pour que l'optimiseur choisisse un scan d'index (pas de cluster) avec hachage ou fusion. Je soupçonne que ce ne serait pas le cas ici, ni réellement utile, mais vous pouvez tester les effets en remplaçant la jointure interne contre CustomAttributeValues par la jointure de hachage interne et la jointure de fusion interne .
J'ai également examiné le code de manière plus large et je ne vois aucun moyen de l'améliorer - je serais bien sûr intéressé à me tromper. Et si vous avez envie de publier la logique complète de ce que vous essayez d'accomplir, je serais intéressé par un autre regard.
la source
OPTION(FORCE ORDER)
, ce qui empêche l'optimiseur de réorganiser les jointures à partir de la séquence textuelle, ainsi que de nombreuses autres optimisations.Vous n'allez pas vous améliorer à partir d'une recherche d'index [non groupée]. La seule chose meilleure qu'une recherche d'index non cluster est une recherche d'index cluster.
En outre, je suis un administrateur de base de données SQL depuis dix ans et un développeur SQL depuis cinq ans auparavant, et d'après mon expérience, il est extrêmement rare de trouver une amélioration à une requête SQL en étudiant le plan d'exécution que vous ne pouviez pas. t trouver par d'autres moyens. La principale raison de générer le plan d'exécution est qu'il vous suggérera souvent des index manquants que vous pouvez ajouter pour améliorer les performances.
Les principaux gains de performances seront l'ajustement de la requête SQL elle-même, en cas d'inefficacité. Par exemple, il y a quelques mois, j'ai obtenu une fonction SQL pour exécuter 160 fois plus rapide en réécrivant un
SELECT UNION SELECT
tableau croisé dynamique de style pour utiliser l'PIVOT
opérateur SQL standard .Voyons donc,
SELECT * INTO
est généralement moins efficace qu'une normeINSERT Object1 (column list) SELECT column list
. Je réécrirais donc cela. Ensuite, si Function1 a été défini sansWITH SCHEMABINDING
, l'ajout d'uneWITH SCHEMABINDING
clause devrait lui permettre de s'exécuter plus rapidement.Vous avez choisi de nombreux alias qui n'ont pas de sens, comme alias Object2 en Object3. Vous devez choisir de meilleurs alias qui ne brouillent pas le code. Vous avez "Object7.Column5 dans (sélectionnez Colonne1 dans Object1)".
IN
les clauses de cette nature sont toujours plus efficaces écrites commeEXISTS (SELECT 1 FROM Object1 o1 WHERE o1.Column1 = Object7.Column5)
. J'aurais peut-être dû écrire cela dans l'autre sens.EXISTS
sera toujours au moins aussi bon queIN
. Ce n'est pas toujours mieux, mais c'est généralement le cas.En outre, je doute que cela
option(recompile)
améliore les performances des requêtes ici. Je testerais le retirer.la source