Faisons-nous quelque chose de mal ou s'agit-il d'une erreur SQL Server?
C'est un bug de résultats erronés, que vous devez signaler via votre canal d'assistance habituel. Si vous n'avez pas de contrat de support, il peut être utile de savoir que les incidents payés sont normalement remboursés si Microsoft confirme le comportement du problème.
Le bogue nécessite trois ingrédients:
- Boucles imbriquées avec une référence externe (une application)
- Une bobine d'index paresseux côté intérieur qui cherche sur la référence externe
- Un opérateur de concaténation interne
Par exemple, la requête dans la question génère un plan similaire à celui-ci:
Il existe de nombreuses façons de supprimer l'un de ces éléments, de sorte que le bogue ne se reproduit plus.
Par exemple, vous pouvez créer des index ou des statistiques qui signifient que l'optimiseur choisit de ne pas utiliser de spouleur d'index paresseux. Ou bien, on pourrait utiliser des astuces pour forcer un hachage ou une fusion au lieu d'utiliser la concaténation. Vous pouvez également réécrire la requête pour exprimer la même sémantique, mais cela se traduira par une forme de plan différente dans laquelle un ou plusieurs des éléments requis sont manquants.
Plus de détails
Une spouleur d'index paresseuse met automatiquement en cache les lignes de résultat du côté interne, dans une table de travail indexée par des valeurs de référence externe (paramètre corrélé). Si on demande à une spaine d'index paresseuse d'indiquer une référence externe déjà vue, elle extrait la ligne de résultat mise en cache de sa table de travail (un "rembobinage"). Si une valeur de référence externe qu'il n'a pas vue auparavant est demandée au spool, il exécute son sous-arbre avec la valeur de référence externe actuelle et met en cache le résultat (un "rapprochement"). Le prédicat de recherche sur le spool d'index paresseux indique la ou les clés de sa table de travail.
Le problème se produit dans cette forme de plan spécifique lorsque le spool vérifie si une nouvelle référence externe est identique à celle déjà vue. La jointure de boucles imbriquées met correctement à jour ses références externes et informe les opérateurs de son entrée interne via leurs PrepRecompute
méthodes d'interface. Au début de cette vérification, les opérateurs du côté intérieur lisent la CParamBounds:FNeedToReload
propriété pour voir si la référence externe a changé depuis la dernière fois. Un exemple de trace de pile est présenté ci-dessous:
Lorsque la sous-arborescence indiquée ci-dessus existe, en particulier lorsque la concaténation est utilisée, quelque chose ne va pas (peut-être un problème ByVal / ByRef / Copy) avec les liaisons telles qu'elles CParamBounds:FNeedToReload
retournent toujours la valeur false, que la référence externe ait été modifiée ou non.
Lorsque le même sous-arbre existe, mais qu'une Union de fusion ou une Union de hachage est utilisée, cette propriété essentielle est définie correctement à chaque itération et le spool d'index paresseux rembobine ou se rebindit chaque fois que nécessaire. Soit dit en passant, le tri et l'agrégat distincts sont sans reproche. Je soupçonne Merge et Hash Union de créer une copie de la valeur précédente, alors que Concatenation utilise une référence. Il est à peu près impossible de vérifier cela sans accès au code source de SQL Server, malheureusement.
Le résultat net est que le spouleur d'index paresseux dans la forme de plan problématique pense toujours qu'il a déjà vu la référence externe actuelle, rembobine en cherchant dans sa table de travail, ne trouve généralement rien, donc aucune ligne n'est renvoyée pour cette référence externe. En exécutant l'exécution dans un débogueur, le spool n'exécute jamais que sa RewindHelper
méthode et jamais sa ReloadHelper
méthode (reload = rebind dans ce contexte). Ceci est évident dans le plan d'exécution car les opérateurs situés sous le spool ont tous le même nombre de 'Nombre d'exécutions = 1'.
L'exception, bien sûr, concerne la première référence externe, l'indicateur Lazy Index Spool est donné. Cela exécute toujours le sous-arbre et met en cache une ligne de résultat dans la table de travail. Toutes les itérations suivantes entraînent un retour arrière, qui ne produira une ligne (la seule ligne mise en cache) lorsque l'itération en cours a la même valeur pour la référence externe que la première fois.
Ainsi, pour tout jeu d'entrée donné sur le côté extérieur de la jointure de boucles imbriquées, la requête retournera autant de lignes qu'il y a de doublons de la première ligne traitée (plus une pour la première ligne elle-même).
Démo
Tableau et exemple de données:
CREATE TABLE #T1
(
pk integer IDENTITY NOT NULL,
c1 integer NOT NULL,
CONSTRAINT PK_T1
PRIMARY KEY CLUSTERED (pk)
);
GO
INSERT #T1 (c1)
VALUES
(1), (2), (3), (4), (5), (6),
(1), (2), (3), (4), (5), (6),
(1), (2), (3), (4), (5), (6);
La requête suivante (triviale) génère un nombre correct de deux pour chaque ligne (18 au total) à l'aide d'une union de fusion:
SELECT T1.c1, C.c1
FROM #T1 AS T1
CROSS APPLY
(
SELECT COUNT_BIG(*) AS c1
FROM
(
SELECT T1.c1
UNION
SELECT NULL
) AS U
) AS C;
Si nous ajoutons maintenant un indice de requête pour forcer une concaténation:
SELECT T1.c1, C.c1
FROM #T1 AS T1
CROSS APPLY
(
SELECT COUNT_BIG(*) AS c1
FROM
(
SELECT T1.c1
UNION
SELECT NULL
) AS U
) AS C
OPTION (CONCAT UNION);
Le plan d'exécution a la forme problématique:
Et le résultat est maintenant incorrect, juste trois lignes:
Bien que ce comportement ne soit pas garanti, la première ligne de l'analyse d'index en cluster a la c1
valeur 1. Il existe deux autres lignes avec cette valeur, de sorte que trois lignes sont générées au total.
Maintenant, tronquez la table de données et chargez-la avec plus de doublons de la première ligne:
TRUNCATE TABLE #T1;
INSERT #T1 (c1)
VALUES
(1), (2), (3), (4), (5), (6),
(1), (2), (3), (4), (5), (6),
(1), (1), (1), (1), (1), (1);
Le plan de concaténation est maintenant le suivant:
Et, comme indiqué, 8 lignes sont produites, toutes avec c1 = 1
bien sûr:
Je remarque que vous avez ouvert un élément Connect pour ce bogue, mais ce n’est vraiment pas le lieu de signaler des problèmes qui ont un impact sur la production. Si tel est le cas, vous devez contacter le support technique Microsoft.
Ce bug de résultats erronés a été corrigé à un moment donné. Il ne se reproduit plus pour moi sur aucune version de SQL Server à partir de 2012. Il reproduit sur la version 10.50.6560.0 (X64) de SQL Server 2008 R2 SP3-GDR.