Résultats de sélection imprévisibles de SQL Server (erreur dbms?)

37

Vous trouverez ci-dessous un exemple simple, qui donne des résultats étranges, imprévisibles et impossibles à expliquer par notre équipe. Faisons-nous quelque chose de mal ou s'agit-il d'une erreur SQL Server?

Après une enquête, nous avons réduit la zone de recherche à la clause d'union dans la sous - requête , qui sélectionne un enregistrement dans la table "hommes".

Cela fonctionne comme prévu dans SQL Server 2000 (retourne 12 lignes), mais en 2008 et 2012, il ne renvoie qu'une ligne.

create table dual (dummy int)

insert into dual values (0)

create table men (
man_id int,
wife_id int )

-- there are 12 men, 6 married 
insert into men values (1, 1)
insert into men values (2, 2)
insert into men values (3, null)
insert into men values (4, null)
insert into men values (5, null)
insert into men values (6, 3)
insert into men values (7, 5)
insert into men values (8, 7)
insert into men values (9, null)
insert into men values (10, null)
insert into men values (11, null)
insert into men values (12, 9)

Cela retourne une seule ligne: 1 1 2

select 
man_id,
wife_id,
(select count( * ) from 
    (select dummy from dual
     union select men.wife_id  ) family_members
) as family_size
from men
--where wife_id = 2 -- uncomment me and try again

Ne commentez pas la dernière ligne et cela donne: 2 2 2

Il y a beaucoup de comportements étranges:

  • Après une série de chutes, crée, tronque et insère sur la table "men", cela fonctionne parfois (retourne 12 lignes)
  • Lorsque vous remplacez "union select men.wife_id" par "union all select men.wife_id" ou "union select isnull (men.wife_id, null)" (!!!), il renvoie 12 lignes (comme prévu).
  • Le comportement étrange semble ne pas être lié au type de données de la colonne "épouse_id". Nous l'avons observé sur un système de développement avec des ensembles de données beaucoup plus volumineux.
  • "where wife_id> 0" renvoie 6 lignes
  • nous observons également un comportement étrange des points de vue avec ce genre de déclarations. SELECT * renvoie un sous-ensemble de lignes, SELECT TOP 1000 renvoie tout
Ryszard Bocian
la source

Réponses:

35

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:

  1. Boucles imbriquées avec une référence externe (une application)
  2. Une bobine d'index paresseux côté intérieur qui cherche sur la référence externe
  3. Un opérateur de concaténation interne

Par exemple, la requête dans la question génère un plan similaire à celui-ci:

Plan annoté

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 PrepRecomputeméthodes d'interface. Au début de cette vérification, les opérateurs du côté intérieur lisent la CParamBounds:FNeedToReloadproprié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:

CParamBounds: FNeedToReload

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:FNeedToReloadretournent 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 RewindHelperméthode et jamais sa ReloadHelpermé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'.

RewindHelper

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;

Plan de fusion

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:

Plan de concaténation

Et le résultat est maintenant incorrect, juste trois lignes:

Résultat à trois rangées

Bien que ce comportement ne soit pas garanti, la première ligne de l'analyse d'index en cluster a la c1valeur 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:

Plan de concaténation sur 8 rangées

Et, comme indiqué, 8 lignes sont produites, toutes avec c1 = 1bien sûr:

Résultat 8 lignes

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.

Paul White dit GoFundMonica
la source
-3

Pourquoi utilisez-vous une sous-requête sans l'instruction from? Je pense que cela pourrait faire la différence entre les serveurs 2005 et 2008. Peut-être que vous pourriez aller avec une jointure explicite?

select 
m1.man_id,
m1.wife_id,
(select count( * ) from 
    (select dummy from dual
     union
     select m2.wife_id
     from men m2
     where m2.man_id = m1.man_id) family_members
) as family_size
from men m1

la source
3
Oui, cela fonctionne, mais ma version devrait également fonctionner. L'exemple ci-dessus abstrait est une version très simplifiée de notre requête de production, ce qui est beaucoup plus logique.