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 in
where
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.
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.
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 .
NOT IN
renvoie 0 enregistrements comparés à une valeur inconnuePuisque NULL
est une inconnue, une requête NOT IN
contenant un NULL
ou NULL
s 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.
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
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)!
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ècesspq
fournisseurs connus pour fournir des pièces en quantités connuesnotant 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;
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.
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.
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 )
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
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