web-dev-qa-db-fra.com

Clause NOT IN et valeurs NULL

Ce problème est survenu lorsque j'ai obtenu différents comptes d'enregistrement pour ce que je pensais être des requêtes identiques, l'une utilisant une contrainte not inwhere et l'autre un left join. La table dans la contrainte not in avait une valeur null (données incorrectes), ce qui a amené cette requête à renvoyer un nombre de 0 enregistrements. Je comprends un peu pourquoi mais je pourrais utiliser un peu d’aide pour bien comprendre le concept.

Pour le dire simplement, pourquoi la requête A renvoie-t-elle un résultat mais pas B?

A: select 'true' where 3 in (1, 2, 3, null)
B: select 'true' where 3 not in (1, 2, null)

C'était sur SQL Server 2005. J'ai aussi constaté que l'appel de set ansi_nulls off provoque le retour d'un résultat par B.

228
Jamie Ide

La requête A est la même que:

select 'true' where 3 = 1 or 3 = 2 or 3 = 3 or 3 = null

Puisque 3 = 3 est vrai, vous obtenez un résultat.

La requête B est la même chose que:

select 'true' where 3 <> 1 and 3 <> 2 and 3 <> null

Lorsque ansi_nulls est activé, 3 <> null est UNKNOWN, le prédicat est évalué à UNKNOWN et vous n'obtenez aucune ligne.

Lorsque ansi_nulls est désactivé, 3 <> null est à true, le prédicat est évalué à true et vous obtenez une ligne.

267
Brannon

Chaque fois que vous utilisez NULL, vous avez vraiment affaire à une logique à trois valeurs.

Votre première requête renvoie les résultats lors de l'évaluation de la clause WHERE:

    3 = 1 or 3 = 2 or 3 = 3 or 3 = null
which is:
    FALSE or FALSE or TRUE or UNKNOWN
which evaluates to 
    TRUE

Le deuxième:

    3 <> 1 and 3 <> 2 and 3 <> null
which evaluates to:
    TRUE and TRUE and UNKNOWN
which evaluates to:
    UNKNOWN

Le UNKNOWN n’est pas le même que FALSE, vous pouvez facilement le tester en appelant:

select 'true' where 3 <> null
select 'true' where not (3 <> null)

Les deux requêtes ne vous donneront aucun résultat

Si INCONNU était identique à FAUX, en supposant que la première requête vous donnerait FAUX, la seconde devrait être évaluée à VRAI, car elle aurait été identique à NON (FAUX).
Ce n'est pas le cas.

Il y a un très bon article sur ce sujet sur SqlServerCentral .

Toute la question des valeurs NULL et de la logique à trois valeurs peut être un peu déroutante au début, mais il est essentiel de la comprendre pour pouvoir écrire des requêtes correctes dans TSQL

Un autre article que je recommanderais est SQL Aggregate Functions and NULL .

52
kristof

NOT IN renvoie 0 enregistrements comparés à une valeur inconnue

Puisque NULL est une inconnue, une requête NOT IN contenant un NULL ou NULLs dans la liste des valeurs possibles renverra toujours les enregistrements 0 car il n'y a aucun moyen pour être sûr que la valeur NULL n'est pas la valeur testée.

25
YonahW

Comparer à null n'est pas défini, sauf si vous utilisez IS NULL.

Ainsi, lorsque vous comparez 3 à NULL (requête A), il renvoie non défini.

C'est à dire. SELECT 'true' où 3 in (1,2, null) et SELECT 'true' où 3 not in (1,2, null)

produira le même résultat, car NOT (UNDEFINED) n'est pas encore défini, mais pas VRAI

18
Sunny Milenov

Le titre de cette question au moment de la rédaction est

La contrainte SQL NOT IN et les valeurs NULL

D'après le texte de la question, il semble que le problème se produise dans une requête SQL DML SELECT plutôt que dans une requête DDL SQL CONSTRAINT.

Toutefois, en particulier compte tenu du libellé du titre, je tiens à souligner que certaines déclarations faites ici sont des déclarations potentiellement trompeuses, à l'instar de (paraphrasant)

Lorsque le prédicat est évalué comme inconnu, vous n'obtenez aucune ligne.

Bien que ce soit le cas pour SQL DML, l’effet est différent lorsqu’on considère les contraintes.

Considérez ce tableau très simple avec deux contraintes tirées directement des prédicats de la question (et traitées dans une excellente réponse de @Brannon):

DECLARE @T TABLE 
(
 true CHAR(4) DEFAULT 'true' NOT NULL, 
 CHECK ( 3 IN (1, 2, 3, NULL )), 
 CHECK ( 3 NOT IN (1, 2, NULL ))
);

INSERT INTO @T VALUES ('true');

SELECT COUNT(*) AS tally FROM @T;

Selon la réponse de @ Brannon, la première contrainte (en utilisant IN) est évaluée à TRUE et la deuxième contrainte (en utilisant NOT IN) est évaluée à INCONNU. Cependant , l'insertion réussit! Par conséquent, dans ce cas, il n’est pas strictement correct de dire "vous n’obtenez aucune ligne" car nous avons effectivement inséré une ligne.

L’effet ci-dessus est bien le bon en ce qui concerne la norme SQL-92. Comparer et contraster la section suivante de la spécification SQL-92

7.6 où clause

Le résultat de est une table des lignes de T pour lesquelles le résultat de la condition de recherche est vrai.

4.10 Contraintes d'intégrité

Une contrainte de vérification de table est satisfaite si et seulement si la condition de recherche spécifiée n'est pas fausse pour aucune ligne d'une table.

En d'autres termes:

Dans SQL DML, les lignes sont supprimées du résultat lorsque la variable WHERE a la valeur UNKNOWN car elle ne satisfait pas à la condition "est vraie".

Dans SQL DDL (c'est-à-dire les contraintes), les lignes ne sont pas supprimées du résultat lorsqu'elles sont évaluées en INCONNU, car ne vérifie que la condition "n'est pas fausse".

Bien que les effets dans SQL DML et SQL DDL respectivement puissent sembler contradictoires, il existe une raison pratique de donner le bénéfice du doute aux résultats de UNKNOWN en leur permettant de satisfaire une contrainte (plus correctement, en leur permettant de ne pas manquer de satisfaire une contrainte) : sans ce comportement, toutes les contraintes devraient gérer explicitement les valeurs nulles, ce qui serait très peu satisfaisant du point de vue de la conception du langage (sans parler du fait que c’est un problème pour les programmeurs!)

p.s. si vous trouvez aussi difficile de suivre une logique telle que "inconnue ne manque pas de satisfaire une contrainte" comme je dois l'écrire, alors considérez que vous pouvez vous passer de tout cela en évitant simplement les colonnes nullables dans SQL DDL et tout ce qui est en SQL DML qui produit des valeurs nulles (par exemple, des jointures externes)!

9
onedaywhen

On peut conclure des réponses que NOT IN (subquery) ne gère pas correctement les valeurs NULL et doit être évité au profit de NOT EXISTS. Cependant, une telle conclusion peut être prématurée. Dans le scénario suivant, attribué à Chris Date (Conception et programmation de la base de données, vol 2 n ° 9, septembre 1989), c'est NOT IN qui gère correctement les valeurs NULL et renvoie le résultat correct plutôt que NOT EXISTS.

Considérez un tableau sp pour représenter les fournisseurs (sno) connus pour fournir des pièces (pno) en quantité (qty). La table contient actuellement les valeurs suivantes:

      VALUES ('S1', 'P1', NULL), 
             ('S2', 'P1', 200),
             ('S3', 'P1', 1000)

Notez que la quantité est annulable, c’est-à-dire pour pouvoir enregistrer le fait qu’un fournisseur est connu pour fournir des pièces, même s’il n’est pas connu en quelle quantité.

La tâche consiste à rechercher les fournisseurs dont le numéro de pièce est connu "P1", mais pas par millier.

Les éléments suivants utilisent NOT IN pour identifier correctement le fournisseur "S2" uniquement:

WITH sp AS 
     ( SELECT * 
         FROM ( VALUES ( 'S1', 'P1', NULL ), 
                       ( 'S2', 'P1', 200 ),
                       ( 'S3', 'P1', 1000 ) )
              AS T ( sno, pno, qty )
     )
SELECT DISTINCT spx.sno
  FROM sp spx
 WHERE spx.pno = 'P1'
       AND 1000 NOT IN (
                        SELECT spy.qty
                          FROM sp spy
                         WHERE spy.sno = spx.sno
                               AND spy.pno = 'P1'
                       );

Cependant, la requête ci-dessous utilise la même structure générale mais avec NOT EXISTS mais inclut incorrectement le fournisseur 'S1' dans le résultat (c'est-à-dire pour lequel la quantité est nulle):

WITH sp AS 
     ( SELECT * 
         FROM ( VALUES ( 'S1', 'P1', NULL ), 
                       ( 'S2', 'P1', 200 ),
                       ( 'S3', 'P1', 1000 ) )
              AS T ( sno, pno, qty )
     )
SELECT DISTINCT spx.sno
  FROM sp spx
 WHERE spx.pno = 'P1'
       AND NOT EXISTS (
                       SELECT *
                         FROM sp spy
                        WHERE spy.sno = spx.sno
                              AND spy.pno = 'P1'
                              AND spy.qty = 1000
                      );

Donc, NOT EXISTS n'est pas la solution miracle, il est peut-être apparu!

Bien entendu, la source du problème provient de la présence de valeurs nulles. La "vraie" solution consiste donc à éliminer ces valeurs nulles.

Ceci peut être réalisé (entre autres conceptions possibles) en utilisant deux tableaux:

  • sp fournisseurs connus pour fournir des pièces
  • spq fournisseurs connus pour fournir des pièces en quantités connues

notant qu'il devrait y avoir probablement une contrainte de clé étrangère où spq fait référence à sp.

Le résultat peut ensuite être obtenu à l'aide de l'opérateur relationnel "moins" (le mot clé EXCEPT en SQL standard), par exemple.

WITH sp AS 
     ( SELECT * 
         FROM ( VALUES ( 'S1', 'P1' ), 
                       ( 'S2', 'P1' ),
                       ( 'S3', 'P1' ) )
              AS T ( sno, pno )
     ),
     spq AS 
     ( SELECT * 
         FROM ( VALUES ( 'S2', 'P1', 200 ),
                       ( 'S3', 'P1', 1000 ) )
              AS T ( sno, pno, qty )
     )
SELECT sno
  FROM spq
 WHERE pno = 'P1'
EXCEPT 
SELECT sno
  FROM spq
 WHERE pno = 'P1'
       AND qty = 1000;
7
onedaywhen

Dans A, 3 est testé pour l’égalité avec chaque membre de l’ensemble, donnant (FALSE, FALSE, TRUE, UNKNOWN). Comme l'un des éléments est VRAI, la condition est VRAIE. (Il est également possible qu'un court-circuit se produise ici, donc il s'arrête dès qu'il atteint le premier VRAI et n'évalue jamais 3 = NULL.)

En B, je pense que la condition est évaluée par NOT (3 in (1,2, null)). Test 3 pour l’égalité avec les rendements définis (FALSE, FALSE, UNKNOWN), qui est agrégé à UNKNOWN. NOT (INCONNU) donne INCONNU. Donc globalement, la vérité de la maladie est inconnue, ce qui à la fin est essentiellement traitée comme FAUX.

7
Dave Costa

Null signifie et absence de données, c'est-à-dire qu'il est inconnu et non une valeur de donnée nulle. Il est très facile pour les personnes ayant des antécédents en programmation de confondre cela parce que, dans les langages de type C, utiliser des pointeurs nuls n’est en effet rien.

Par conséquent, dans le premier cas, 3 est bien dans l'ensemble de (1,2,3, null), donc true est renvoyé

Dans le second cependant, vous pouvez le réduire à

sélectionnez 'true' où 3 pas dans (null)

Donc, rien n'est retourné car l'analyseur ignore tout de la série à laquelle vous la comparez - ce n'est pas une série vide, mais une série inconnue. L'utilisation de (1, 2, null) n'aide en rien, car l'ensemble (1,2) est évidemment faux, mais vous êtes alors opposé à l'inconnu, qui est inconnu.

6
Cruachan

SI vous voulez filtrer avec NOT IN pour une sous-requête contenant des valeurs NULL, il suffit de vérifier non nul

SELECT blah FROM t WHERE blah NOT IN
        (SELECT someotherBlah FROM t2 WHERE someotherBlah IS NOT NULL )
5
Mihai

c'est pour Boy:

select party_code 
from abc as a
where party_code not in (select party_code 
                         from xyz 
                         where party_code = a.party_code);

cela fonctionne indépendamment des paramètres ansi

1
Rostand Abear

cela peut également être utile pour connaître la différence logique entre join, existe et http://weblogs.sqlteam.com/mladenp/archive/2007/05/18/60210.aspx

0
Mladen