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:
CREATE TABLE #Temp
(
id integer NOT NULL PRIMARY KEY CLUSTERED
);
INSERT #Temp
(
id
)
SELECT DISTINCT
CAV.id
FROM @customAttrValIds AS CAV
WHERE
CAV.id IS NOT NULL;
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 une UPDATE 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:
CREATE TABLE #Temp2
(
CustomAttrID integer NOT NULL,
Assortment_Id integer NOT NULL,
);
INSERT #Temp2
(
Assortment_Id,
CustomAttrID
)
SELECT
ACAV.Assortment_Id,
CAV.CustomAttrID
FROM #temp AS T
JOIN dbo.CustomAttributeValues AS CAV
ON CAV.Id = T.id
JOIN dbo.AssortmentCustomAttributeValues AS ACAV
ON T.id = ACAV.CustomAttributeValue_Id;
ALTER TABLE #Temp2
ADD CONSTRAINT PK_#Temp2_Assortment_Id_CustomAttrID
PRIMARY KEY CLUSTERED (Assortment_Id, CustomAttrID);
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:
SELECT
A.Id,
A.AssortmentId
FROM
(
SELECT
T.Assortment_Id
FROM #Temp2 AS T
GROUP BY
T.Assortment_Id
HAVING
COUNT_BIG(DISTINCT T.CustomAttrID) = @dist_ca_id
) AS DT
JOIN dbo.Assortments AS A
ON A.Id = DT.Assortment_Id
WHERE
A.AssortmentType = @asType
OPTION (RECOMPILE);
Nous pourrions réécrire manuellement le COUNT_BIG(DISTINCT...
comme simple COUNT_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.
#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.