SQL "sélectionner où pas dans la sous-requête" ne renvoie aucun résultat

130

Avertissement: j'ai compris le problème (je pense), mais je voulais ajouter ce problème à Stack Overflow car je ne pouvais (facilement) le trouver nulle part. De plus, quelqu'un pourrait avoir une meilleure réponse que moi.

J'ai une base de données où une table "commune" est référencée par plusieurs autres tables. Je voulais voir quels enregistrements de la table commune étaient orphelins (c'est-à-dire qu'ils n'avaient aucune référence à partir des autres tables).

J'ai exécuté cette requête:

select *
from Common
where common_id not in (select common_id from Table1)
and common_id not in (select common_id from Table2)

Je sais qu'il existe des enregistrements orphelins, mais aucun enregistrement n'a été retourné. Pourquoi pas?

(Il s'agit de SQL Server, si cela compte.)

Jeremy Stein
la source
Ce stackoverflow.com/a/129152/1667619 répond assez bien à la question POURQUOI.
Ruchan

Réponses:

234

Mettre à jour:

Ces articles de mon blog décrivent plus en détail les différences entre les méthodes:


Il existe trois façons de faire une telle requête:

  • LEFT JOIN / IS NULL:

    SELECT  *
    FROM    common
    LEFT JOIN
            table1 t1
    ON      t1.common_id = common.common_id
    WHERE   t1.common_id IS NULL
  • NOT EXISTS:

    SELECT  *
    FROM    common
    WHERE   NOT EXISTS
            (
            SELECT  NULL
            FROM    table1 t1
            WHERE   t1.common_id = common.common_id
            )
  • NOT IN:

    SELECT  *
    FROM    common
    WHERE   common_id NOT IN
            (
            SELECT  common_id
            FROM    table1 t1
            )

Quand table1.common_idn'est pas nullable, toutes ces requêtes sont sémantiquement identiques.

Quand il est nullable, NOT INest différent, puisque IN(et, par conséquent, NOT IN) retourne NULLquand une valeur ne correspond à rien dans une liste contenant un NULL.

Cela peut être déroutant mais peut devenir plus évident si nous rappelons la syntaxe alternative pour ceci:

common_id = ANY
(
SELECT  common_id
FROM    table1 t1
)

Le résultat de cette condition est un produit booléen de toutes les comparaisons dans la liste. Bien sûr, une seule NULLvaleur donne le NULLrésultat qui rend également le résultat entier NULL.

Nous ne pouvons jamais dire avec certitude que ce common_idn'est égal à rien de cette liste, car au moins une des valeurs l'est NULL.

Supposons que nous ayons ces données:

common

--
1
3

table1

--
NULL
1
2

LEFT JOIN / IS NULLet NOT EXISTSretournera 3, NOT INne retournera rien (car il sera toujours évalué à l'un FALSEou l' autre NULL).

Dans MySQL, en cas de colonne non nullable, LEFT JOIN / IS NULLet NOT INsont un peu plus efficaces (plusieurs pour cent) que NOT EXISTS. Si la colonne est nullable, NOT EXISTSc'est le plus efficace (encore une fois, pas beaucoup).

Dans Oracle, les trois requêtes génèrent les mêmes plans (an ANTI JOIN).

Dans SQL Server, NOT IN/ NOT EXISTSsont plus efficaces, car LEFT JOIN / IS NULLne peuvent pas être optimisés à un ANTI JOINpar son optimiseur.

Dans PostgreSQL, LEFT JOIN / IS NULLet NOT EXISTSsont plus efficaces que NOT IN, sine, ils sont optimisés pour un Anti Join, while NOT INuses hashed subplan(ou même un plain subplansi la sous-requête est trop grande pour le hachage)

Quassnoi
la source
8
Très bonne réponse! Merci!
StevenMcD
c'est génial et très utile
kavun
1
+1 parce que, quatre ans et demi plus tard, cette réponse m'a aidé à résoudre un problème qui m'a laissé perplexe!
Carson63000
@ Carson63000 Snap! Je pensais que je devenais fou avant de voir cette réponse
Bobby
1
@IstiaqueAhmed: prend la NOT EXISTSvaleur TRUE si la requête à l'intérieur renvoie des lignes. SELECT NULLpourrait aussi bien être SELECT *ou SELECT 1ou quoi que ce soit d'autre, le NOT EXISTSprédicat ne regarde pas les valeurs des lignes, les compte seulement.
Quassnoi
36

Si vous voulez que le monde soit un lieu booléen à deux valeurs, vous devez éviter vous-même le cas nul (troisième valeur).

N'écrivez pas de clauses IN qui autorisent les valeurs nulles du côté liste. Filtrez-les!

common_id not in
(
  select common_id from Table1
  where common_id is not null
)
Amy B
la source
6
Les valeurs nulles dans la liste de clauses sont une raison courante pour les résultats de requête manquants.
Amy B du
«En comparant avec un nul, la réponse est inconnue» - d'après la réponse de @Jeremy Stein. De common_id not in, nous pouvons encore avoir une common_idvaleur qui est NULL. Le problème de l'absence de résultats ne persiste-t-il donc pas?
Istiaque Ahmed
5

Table1 ou Table2 a des valeurs nulles pour common_id. Utilisez plutôt cette requête:

select *
from Common
where common_id not in (select common_id from Table1 where common_id is not null)
and common_id not in (select common_id from Table2 where common_id is not null)
Jeremy Stein
la source
1
Et s'il y a des données dans une table mais pas dans l'autre? Voulez-vous "et" ou "ou" là-bas?
Philip Kelley
1
Je recherche des enregistrements qui ne sont référencés dans aucune table, donc je veux ET. Je vais clarifier la question.
Jeremy Stein
4
select *
from Common c
where not exists (select t1.commonid from table1 t1 where t1.commonid = c.commonid)
and not exists (select t2.commonid from table2 t2 where t2.commonid = c.commonid)
patmortech
la source
4

Juste du haut de ma tête ...

select c.commonID, t1.commonID, t2.commonID
from Common c
     left outer join Table1 t1 on t1.commonID = c.commonID
     left outer join Table2 t2 on t2.commonID = c.commonID
where t1.commonID is null 
     and t2.commonID is null

J'ai fait quelques tests et voici mes résultats avec la réponse de @ patmortech et les commentaires de @ rexem.

Si Table1 ou Table2 n'est pas indexé sur commonID, vous obtenez une analyse de table mais la requête de @ patmortech est toujours deux fois plus rapide (pour une table maître de 100K lignes).

Si aucun des deux n'est indexé sur commonID, vous obtenez deux analyses de table et la différence est négligeable.

Si les deux sont indexés sur commonID, la requête "n'existe pas" s'exécute dans 1/3 du temps.

Austin Salonen
la source
1
Cela devrait être un ET dans la clause where. Sinon, cela fonctionne.
Jeremy Stein
1
changé par votre commentaire. Le "ou" sélectionne les orphelins dans l'une ou l'autre table.
Austin Salonen
1
C'est mieux. Au fait, y a-t-il une raison pour laquelle je devrais utiliser des jointures externes plutôt que la sous-requête?
Jeremy Stein
3
La lisibilité est la principale. Je soupçonne qu'un meilleur plan d'exécution serait généré mais sans plan de requête, je ne peux pas le confirmer.
Austin Salonen
2
Cette approche est pire que l'utilisation de NOT EXISTS - la jointure entraîne la récupération de plus de lignes que nécessaire, les résultats comparés pour les colonnes étant nulles. Et NOT EXISTS est plus lisible au démarrage.
OMG Ponies
3
SELECT T.common_id
  FROM Common T
       LEFT JOIN Table1 T1 ON T.common_id = T1.common_id
       LEFT JOIN Table2 T2 ON T.common_id = T2.common_id
 WHERE T1.common_id IS NULL
   AND T2.common_id IS NULL
manji
la source
1
Cette approche est pire que l'utilisation de NOT EXISTS - la jointure entraîne la récupération de plus de lignes que nécessaire, les résultats comparés pour les colonnes étant nulles. Cela fonctionne, mais les performances ne seront pas aussi bonnes - peut-être pire que l'utilisation de IN avec des sous-requêtes corrélées.
OMG Ponies
3

Supposons ces valeurs pour common_id:

Common - 1
Table1 - 2
Table2 - 3, null

Nous voulons que la ligne en commun renvoie, car elle n'existe dans aucune des autres tables. Cependant, le nul jette une clé à molette.

Avec ces valeurs, la requête équivaut à:

select *
from Common
where 1 not in (2)
and 1 not in (3, null)

Cela équivaut à:

select *
from Common
where not (1=2)
and not (1=3 or 1=null)

C'est là où commence le problème. Lors de la comparaison avec un nul, la réponse est inconnue . Ainsi, la requête se réduit à

select *
from Common
where not (false)
and not (false or unkown)

faux ou inconnu est inconnu:

select *
from Common
where true
and not (unknown)

true and not unkown est également inconnu:

select *
from Common
where unknown

La condition where ne renvoie pas les enregistrements dont le résultat est inconnu, nous ne récupérons donc aucun enregistrement.

Une façon de résoudre ce problème consiste à utiliser l'opérateur exist plutôt que dans. Exists ne renvoie jamais unkown car il opère sur des lignes plutôt que des colonnes. (Une ligne existe ou non; aucune de cette ambiguïté nulle au niveau de la ligne!)

select *
from Common
where not exists (select common_id from Table1 where common_id = Common.common_id)
and not exists (select common_id from Table2 where common_id = Common.common_id)
Jeremy Stein
la source
2

cela a fonctionné pour moi :)

sélectionnez * dans Common

common_id absent (sélectionnez ISNULL (common_id, 'dummy-data') dans Table1)

et common_id pas dans (sélectionnez ISNULL (common_id, 'dummy-data') dans Table2)

arqué
la source
@marlar, les sous-requêtes renvoient toujours 1 ou 0, pas une liste de valeurs. Alors, comment se NOT INproduira-t-il?
Istiaque Ahmed
0
select *,
(select COUNT(ID)  from ProductMaster where ProductMaster.CatID = CategoryMaster.ID) as coun 
from CategoryMaster
Donga jayesh
la source
0

J'ai eu un exemple où je recherchais et parce qu'une table contenait la valeur comme un double, l'autre comme une chaîne, ils ne correspondraient pas (ou ne correspondraient pas sans un cast). Mais seulement PAS IN . Comme SELECT ... IN ... a fonctionné. Bizarre, mais je pensais que je partagerais au cas où quelqu'un d'autre rencontrerait cette solution simple.

ransems
la source
0

Veuillez suivre l'exemple ci-dessous pour comprendre le sujet ci-dessus:

Vous pouvez également visiter le lien suivant pour connaître Anti join

select department_name,department_id from hr.departments dep
where not exists 
    (select 1 from hr.employees emp
    where emp.department_id=dep.department_id
    )
order by dep.department_name;
DEPARTMENT_NAME DEPARTMENT_ID
Benefits    160
Construction    180
Contracting 190
.......

Mais si nous utilisons NOT INdans ce cas, nous n'obtenons aucune donnée.

select Department_name,department_id from hr.departments dep 
where department_id not in (select department_id from hr.employees );

Aucune donnée disponible

Cela se produit lorsque ( select department_id from hr.employees) renvoie une valeur nulle et la requête entière est évaluée comme fausse. Nous pouvons le voir si nous modifions légèrement le SQL comme ci-dessous et gérons les valeurs nulles avec la fonction NVL.

select Department_name,department_id from hr.departments dep 
where department_id not in (select NVL(department_id,0) from hr.employees )

Maintenant, nous obtenons des données:

DEPARTMENT_NAME DEPARTMENT_ID
Treasury    120
Corporate Tax   130
Control And Credit  140
Shareholder Services    150
Benefits    160
....

Encore une fois, nous obtenons des données car nous avons traité la valeur nulle avec la fonction NVL.

Rajesh Sarkar
la source
Les résultats SQl n'apparaissent pas sous forme de tableau, merci de me contacter.
Rajesh Sarkar