Valeurs NULL à l'intérieur de la clause NOT IN

245

Ce problème est survenu lorsque j'ai obtenu différents nombres d'enregistrements pour ce que je pensais être des requêtes identiques, l'une utilisant une not in wherecontrainte et l'autre a left join. La table dans la not incontrainte avait une valeur nulle (données incorrectes) qui a provoqué le renvoi de 0 enregistrements à cette requête. Je comprends en quelque sorte pourquoi mais je pourrais utiliser de l'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 également constaté que l'appel set ansi_nulls offobligeait B à retourner un résultat.

Jamie Ide
la source

Réponses:

283

La requête A est identique à:

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

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

La requête B est identique à:

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

Quand ansi_nullsest 3 <> nullactivé , est INCONNU, donc le prédicat est évalué comme INCONNU et vous n'obtenez aucune ligne.

Quand ansi_nullsest désactivé, 3 <> nullest vrai, donc le prédicat est évalué à vrai et vous obtenez une ligne.

Brannon
la source
11
Quelqu'un a-t-il déjà souligné que la conversion NOT INen une série de <> andchangements modifie le comportement sémantique de pas dans cet ensemble à autre chose?
Ian Boyd
8
@Ian - Il ressemble à "A NOT IN ('X', 'Y')" est en fait un alias pour A <> 'X' ET A <> 'Y' en SQL. (Je constate que vous l'avez découvert vous-même sur stackoverflow.com/questions/3924694/… , mais que vous vouliez vous assurer que votre objection a été traitée dans cette question.)
Ryan Olson
Je suppose que cela explique pourquoi SELECT 1 WHERE NULL NOT IN (SELECT 1 WHERE 1=0);donne une ligne au lieu du jeu de résultats vide auquel je m'attendais.
binki
2
Il s'agit d'un comportement très médiocre de SQL Server, car s'il s'attend à une comparaison NULL en utilisant "IS NULL", il doit étendre la clause IN à ce même comportement et ne pas appliquer bêtement la mauvaise sémantique à lui-même.
OzrenTkalcecKrznaric
@binki, vous interrogez s'exécute si exécuté ici rextester.com/l/sql_server_online_compiler mais ne fonctionne pas si exécuté ici sqlcourse.com/cgi-bin/interpreter.cgi .
Istiaque Ahmed
53

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

Votre première requête renvoie des résultats lorsque la clause WHERE est évaluée comme suit:

    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

L'INCONNU n'est pas le même que FAUX, 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 l'inconnu était le même que FAUX, alors en supposant que la première requête vous donnerait FAUX, la seconde devrait être évaluée comme VRAIE car elle aurait été la même que NON (FAUX).
Ce n'est pas le cas.

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

Tout le problème des NULL et de la logique à trois valeurs peut être un peu déroutant au début, mais il est essentiel de comprendre afin d'écrire des requêtes correctes dans TSQL

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

kristof
la source
33

NOT IN renvoie 0 enregistrements par rapport à une valeur inconnue

Étant donné qu'il NULLs'agit d'un inconnu, une NOT INrequête contenant un NULLou NULLs dans la liste des valeurs possibles retournera toujours des 0enregistrements car il n'y a aucun moyen de s'assurer que la NULLvaleur n'est pas la valeur en cours de test.

YonahW
la source
3
Telle est la réponse en un mot. J'ai trouvé que c'était plus facile à comprendre même sans aucun exemple.
Govind Rai
18

La comparaison avec null n'est pas définie, sauf si vous utilisez IS NULL.

Ainsi, lors de la comparaison de 3 à NULL (requête A), il renvoie undefined.

Soit SELECT 'vrai' où 3 pouces (1,2, null) et SELECT 'vrai' où 3 pas dans (1,2, null)

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

Sunny Milenov
la source
Bon point. sélectionnez 1 où null dans (null) ne renvoie pas de lignes (ansi).
crokusek
9

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

Contrainte SQL NOT IN et valeurs NULL

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

Cependant, compte tenu en particulier du libellé du titre, je tiens à souligner que certaines déclarations faites ici sont des déclarations potentiellement trompeuses, celles qui s'inspirent de (paraphrasant)

Lorsque le prédicat est évalué sur UNKNOWN, vous n'obtenez aucune ligne.

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

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 (utilisation IN) est évaluée à VRAI et la seconde contrainte (utilisation NOT IN) est évaluée à INCONNU. Cependant , l'insert réussit! Par conséquent, dans ce cas, il n'est pas strictement correct de dire "vous n'obtenez aucune ligne" car nous avons en effet inséré une ligne en conséquence.

L'effet ci-dessus est en effet le bon en ce qui concerne la norme SQL-92. Comparez et contrastez la section suivante de la spécification SQL-92

7.6 clause where

Le résultat de est un tableau de ces 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 une ligne d'une table.

En d'autres termes:

Dans SQL DML, les lignes sont supprimées du résultat lorsque l' WHEREévalué à INCONNU car il ne remplit pas la condition "est vrai".

Dans SQL DDL (c'est-à-dire les contraintes), les lignes ne sont pas supprimées du résultat lorsqu'elles sont évaluées à UNKNOWN car il remplit la condition "n'est pas faux".

Bien que les effets dans SQL DML et SQL DDL respectivement puissent sembler contradictoires, il y a une raison pratique de donner aux résultats INCONNUS le `` bénéfice du doute '' 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 et ce serait très insatisfaisant du point de vue de la conception du langage (sans parler, une bonne douleur pour les codeurs!)

ps si vous trouvez difficile de suivre une logique telle que "inconnu ne manque pas de satisfaire une contrainte" comme je suis en train de l'écrire, alors considérez que vous pouvez vous passer de tout cela simplement en évitant les colonnes nullables en SQL DDL et quoi que ce soit en SQL DML qui produit des valeurs nulles (par exemple des jointures externes)!

un jour
la source
Honnêtement, je ne pensais pas qu'il y ait quoi que ce soit à dire à ce sujet. Intéressant.
Jamie Ide
2
@Jamie Ide: En fait, j'ai une autre réponse sur le sujet: parce qu'impliquer des null NOT IN (subquery)peut donner des résultats inattendus, il est tentant d'éviter IN (subquery)complètement et de toujours l'utiliser NOT EXISTS (subquery)(comme je l'ai fait une fois!) Car il semble qu'il gère toujours correctement les null. Cependant, il y a des cas où NOT IN (subquery)donne le résultat attendu alors que NOT EXISTS (subquery)donne des résultats inattendus! Je pourrai peut-être encore écrire ceci si je peux trouver mes notes sur le sujet (besoin de notes parce que ce n'est pas intuitif!) La conclusion est la même, cependant: évitez les nulls!
quand le
@onedaywhen je suis confus par votre affirmation selon laquelle NULL devrait être un boîtier spécial pour avoir un comportement cohérent (cohérent en interne, non conforme à la spécification). Ne suffirait-il pas de modifier 4.10 pour lire "Une contrainte de vérification de table est satisfaite si et seulement si la condition de recherche spécifiée est vraie"?
DylanYoung
@DylanYoung: Non, la spécification est formulée de cette façon pour une raison cruciale: SQL souffre d'une logique à trois valeurs, où ces valeurs se trouvent TRUE, FALSEet UNKNOWN. Je suppose que 4.10 aurait pu lire: "Une contrainte de vérification de table est satisfaite si et seulement si la condition de recherche spécifiée est VRAIE ou INCONNUE pour chaque ligne d'une table" - notez mon changement à la fin de la phrase - que vous avez omis - - de "pour tout" à "pour tous". Je ressens le besoin de capitaliser les valeurs logiques car le sens de "vrai" et "faux" dans le langage naturel doit sûrement se référer à la logique classique à deux valeurs.
quand
1
Considérez: CREATE TABLE T ( a INT NOT NULL UNIQUE, b INT CHECK( a = b ) );- l'intention ici est que bdoit être égal aou nul. Si une contrainte devait aboutir à VRAI pour être satisfaite, alors nous aurions besoin de changer la contrainte pour gérer explicitement les valeurs nulles, par exemple CHECK( a = b OR b IS NULL ). Ainsi, chaque contrainte devrait avoir une ...OR IS NULLlogique ajoutée par l'utilisateur pour chaque colonne nullable impliquée: plus de complexité, plus de bogues quand ils ont oublié de le faire, etc. Je pense donc que le comité des normes SQL a juste essayé d'être pragmatique.
onedaywhen
7

En A, 3 est testé pour l'égalité par rapport à chaque membre de l'ensemble, ce qui donne (FAUX, FAUX, VRAI, INCONNU). Étant donné que 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 en fait dès qu'il atteint le premier TRUE et n'évalue jamais 3 = NULL.)

En B, je pense qu'il évalue la condition comme NON (3 in (1,2, null)). Test 3 de l'égalité par rapport aux rendements définis (FALSE, FALSE, UNKNOWN), qui est agrégé à UNKNOWN. NOT (UNKNOWN) donne INCONNU. Donc, dans l'ensemble, la vérité de la condition est inconnue, ce qui à la fin est essentiellement traité comme FAUX.

Dave Costa
la source
7

On peut conclure des réponses ici qui NOT IN (subquery)ne traitent pas correctement les valeurs nulles et doivent être évitées en faveur de NOT EXISTS. Cependant, une telle conclusion peut être prématurée. Dans le scénario suivant, attribué à Chris Date (Database Programming and Design, Vol 2 No 9, septembre 1989), c'est NOT INqu'il gère correctement les valeurs nulles et renvoie le résultat correct, plutôt que NOT EXISTS.

Considérons un tableau sppour représenter les fournisseurs ( sno) qui sont connus pour fournir des pièces ( pno) en quantité ( qty). Le tableau contient actuellement les valeurs suivantes:

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

Notez que la quantité peut être annulée, 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 à trouver les fournisseurs connus qui fournissent le numéro de pièce «P1» mais pas en quantités de 1000.

Les utilisations suivantes permettent NOT INd'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 EXISTSmais 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
                      );

Ce NOT EXISTSn'est donc pas la balle d'argent qu'elle a pu apparaître!

Bien sûr, la source du problème est la présence de nulls, donc la «vraie» solution est d'éliminer ces nulls.

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 probablement y avoir une contrainte de clé étrangère où les spqréférences sp.

Le résultat peut ensuite être obtenu en utilisant l'opérateur relationnel «moins» (qui est le EXCEPTmot clé dans 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;
un jour
la source
1
OMG. Merci d'avoir écrit ceci ... ça me rendait fou ..
Govind Rai
6

Null signifie et absence de données, c'est-à-dire qu'elles sont inconnues, pas une valeur de données de rien. Il est très facile pour les personnes issues de la programmation de confondre cela, car dans les langages de type C, lorsque vous utilisez des pointeurs, null n'est en effet rien.

Donc dans le premier cas, 3 est en effet dans l'ensemble de (1,2,3, nul) donc vrai est retourné

Dans la seconde, cependant, vous pouvez le réduire à

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

Donc rien n'est retourné parce que l'analyseur ne sait rien de l'ensemble auquel vous le comparez - ce n'est pas un ensemble vide mais un ensemble inconnu. L'utilisation de (1, 2, null) n'aide pas car l'ensemble (1,2) est manifestement faux, mais vous êtes contre et inconnu, ce qui est inconnu.

Cruachan
la source
6

SI vous voulez filtrer avec NOT IN pour une sous-requête contenant des NULL, vérifiez que non nul

SELECT blah FROM t WHERE blah NOT IN
        (SELECT someotherBlah FROM t2 WHERE someotherBlah IS NOT NULL )
Mihai
la source
J'ai eu un problème avec la requête de jointure externe qui n'a renvoyé aucun enregistrement dans des situations spéciales, j'ai donc vérifié cette solution pour les scénarios Null et existants et cela a fonctionné pour moi, si un autre problème s'est produit, je serai mentionné ici, merci beaucoup.
QMaster
1

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

CB
la source
pour la question d'origine: B: sélectionnez 'true' où 3 pas dans (1, 2, null) un moyen de supprimer les nulls doit être fait par exemple sélectionnez 'true' où 3 pas dans (1, 2, isnull (null, 0) ) la logique globale est, si NULL est la cause, alors trouvez un moyen de supprimer les valeurs NULL à une étape de la requête.
sélectionnez party_code dans abc en tant que où party_code ne se trouve pas (sélectionnez party_code dans xyz où party_code n'est pas nul) mais bonne chance si vous avez oublié que le champ autorise les null, ce qui est souvent le cas
1

SQL utilise une logique à trois valeurs pour les valeurs de vérité. La INrequête produit le résultat attendu:

SELECT * FROM (VALUES (1), (2)) AS tbl(col) WHERE col IN (NULL, 1)
-- returns first row

Mais l'ajout d'un NOTn'inverse pas les résultats:

SELECT * FROM (VALUES (1), (2)) AS tbl(col) WHERE NOT col IN (NULL, 1)
-- returns zero rows

En effet, la requête ci-dessus est équivalente à la suivante:

SELECT * FROM (VALUES (1), (2)) AS tbl(col) WHERE NOT (col = NULL OR col = 1)

Voici comment la clause where est évaluée:

| col | col = NULL (1) | col = 1 | col = NULL OR col = 1 | NOT (col = NULL OR col = 1) |
|-----|----------------|---------|-----------------------|-----------------------------|
| 1   | UNKNOWN        | TRUE    | TRUE                  | FALSE                       |
| 2   | UNKNOWN        | FALSE   | UNKNOWN (2)           | UNKNOWN (3)                 |

Remarquerez que:

  1. La comparaison des NULLrendementsUNKNOWN
  2. L' ORexpression où aucun des opérandes ne se trouve TRUEet au moins un opérande est UNKNOWNrenvoie UNKNOWN( ref )
  3. L' NOTdes UNKNOWNrendements UNKNOWN( ref )

Vous pouvez étendre l'exemple ci-dessus à plus de deux valeurs (par exemple NULL, 1 et 2) mais le résultat sera le même: si l'une des valeurs est NULLalors aucune ligne ne correspondra.

Salman A
la source