PAS DANS vs PAS EXISTE

538

Laquelle de ces requêtes est la plus rapide?

N'EXISTE PAS:

SELECT ProductID, ProductName 
FROM Northwind..Products p
WHERE NOT EXISTS (
    SELECT 1 
    FROM Northwind..[Order Details] od 
    WHERE p.ProductId = od.ProductId)

Ou PAS DANS:

SELECT ProductID, ProductName 
FROM Northwind..Products p
WHERE p.ProductID NOT IN (
    SELECT ProductID 
    FROM Northwind..[Order Details])

Le plan d'exécution des requêtes indique qu'ils font tous les deux la même chose. Si tel est le cas, quelle est la forme recommandée?

Ceci est basé sur la base de données NorthWind.

[Éditer]

Je viens de trouver cet article utile: http://weblogs.sqlteam.com/mladenp/archive/2007/05/18/60210.aspx

Je pense que je vais m'en tenir à PAS EXISTE.

ilitirit
la source
3
avez-vous essayé le plan en utilisant une jointure gauche où est nul?
Sebas
1
NOT IN et NOT EXISTS ne sont pas identiques. Jetez un oeil à ce lien pour la différence entre eux: weblogs.sqlteam.com/mladenp/archive/2007/05/18/60210.aspx
Ameya Gokhale
2
Je me demande si les bases de données diffèrent, mais dans mon dernier benchmark contre PostgreSQL, cette NOT INrequête: SELECT "A".* FROM "A" WHERE "A"."id" NOT IN (SELECT "B"."Aid" FROM "B" WHERE "B"."Uid" = 2)est presque 30 fois plus rapide que cela NOT EXISTS:SELECT "A".* FROM "A" WHERE (NOT (EXISTS (SELECT 1 FROM "B" WHERE "B"."user_id" = 2 AND "B"."Aid" = "A"."id")))
Phương Nguyễn
1
@rcdmk Avez-vous vérifié la date des questions?
ilitirit

Réponses:

693

J'ai toujours par défaut NOT EXISTS.

Les plans d'exécution peuvent être les mêmes pour le moment, mais si l'une ou l'autre colonne est modifiée à l'avenir pour permettre à NULLs, la NOT INversion devra faire plus de travail (même si aucun NULLs n'est réellement présent dans les données) et la sémantique de NOT INsi NULLs sont présents ne sont probablement pas ceux que vous voulez de toute façon.

Lorsque ni Products.ProductIDou [Order Details].ProductIDpermettre NULLde le NOT INseront traités de manière identique à la requête suivante.

SELECT ProductID,
       ProductName
FROM   Products p
WHERE  NOT EXISTS (SELECT *
                   FROM   [Order Details] od
                   WHERE  p.ProductId = od.ProductId) 

Le plan exact peut varier, mais pour mes données d'exemple, j'obtiens ce qui suit.

Ni NULL

Une idée fausse assez courante semble être que les sous-requêtes corrélées sont toujours «mauvaises» par rapport aux jointures. Ils peuvent certainement l'être lorsqu'ils forcent un plan de boucles imbriquées (sous-requête évaluée ligne par ligne), mais ce plan inclut un opérateur logique anti-semi-jointure. Les semi-jointures anti ne sont pas limitées aux boucles imbriquées mais peuvent également utiliser des jointures de hachage ou de fusion (comme dans cet exemple).

/*Not valid syntax but better reflects the plan*/ 
SELECT p.ProductID,
       p.ProductName
FROM   Products p
       LEFT ANTI SEMI JOIN [Order Details] od
         ON p.ProductId = od.ProductId 

Si [Order Details].ProductIDest NULL-able la requête devient alors

SELECT ProductID,
       ProductName
FROM   Products p
WHERE  NOT EXISTS (SELECT *
                   FROM   [Order Details] od
                   WHERE  p.ProductId = od.ProductId)
       AND NOT EXISTS (SELECT *
                       FROM   [Order Details]
                       WHERE  ProductId IS NULL) 

La raison en est que la sémantique correcte si [Order Details]contient des NULL ProductIds est de ne renvoyer aucun résultat. Consultez la bobine anti-jointure supplémentaire et le nombre de lignes pour vérifier ce qui est ajouté au plan.

Un NULL

Si Products.ProductIDest également modifié pour devenir NULL-able, la requête devient alors

SELECT ProductID,
       ProductName
FROM   Products p
WHERE  NOT EXISTS (SELECT *
                   FROM   [Order Details] od
                   WHERE  p.ProductId = od.ProductId)
       AND NOT EXISTS (SELECT *
                       FROM   [Order Details]
                       WHERE  ProductId IS NULL)
       AND NOT EXISTS (SELECT *
                       FROM   (SELECT TOP 1 *
                               FROM   [Order Details]) S
                       WHERE  p.ProductID IS NULL) 

La raison en est que NULL Products.ProductIdne doit pas être renvoyé dans les résultats, sauf si la NOT INsous-requête ne doit renvoyer aucun résultat (c'est-à [Order Details]- dire que la table est vide). Dans ce cas, cela devrait. Dans le plan de mes exemples de données, cela est implémenté en ajoutant un autre anti-jointure comme ci-dessous.

Les deux NULL

L'effet de cela est montré dans le billet de blog déjà lié par Buckley . Dans cet exemple, le nombre de lectures logiques passe d'environ 400 à 500 000.

De plus, le fait qu'un seul NULLpuisse réduire le nombre de lignes à zéro rend l'estimation de la cardinalité très difficile. Si SQL Server suppose que cela se produira mais qu'en fait il n'y avait pas de NULLlignes dans les données, le reste du plan d'exécution peut être catastrophiquement pire, s'il ne s'agit que d'une partie d'une plus grande requête, avec des boucles imbriquées inappropriées provoquant l'exécution répétée d'un sous-système coûteux arbre par exemple .

Ce n'est cependant pas le seul plan d'exécution possible pour une colonne NOT INsur NULL-able. Cet article en montre un autre pour une requête sur la AdventureWorks2008base de données.

Pour le NOT INsur une NOT NULLcolonne ou le NOT EXISTScontre une colonne nullable ou non nullable, il donne le plan suivant.

N'existe pas

Lorsque la colonne devient NULL-able, le NOT INplan ressemble maintenant à

Not In - Null

Il ajoute un opérateur de jointure interne supplémentaire au plan. Cet appareil est expliqué ici . Tout est là pour convertir la recherche d'index unique corrélée précédente Sales.SalesOrderDetail.ProductID = <correlated_product_id>en deux recherches par ligne externe. Celui supplémentaire est activé WHERE Sales.SalesOrderDetail.ProductID IS NULL.

Comme c'est sous une anti semi jointure si celle-ci retourne des lignes, la deuxième recherche ne se produira pas. Cependant, s'il Sales.SalesOrderDetailne contient aucun NULL ProductIDs, il doublera le nombre d'opérations de recherche requises.

Martin Smith
la source
4
Puis-je demander comment vous obtenez le graphique de profilage comme indiqué?
xis
5
@xis Il s'agit de plans d'exécution ouverts dans l'explorateur de plans SQL Sentry. Vous pouvez également afficher graphiquement les plans d'exécution dans SSMS.
Martin Smith
J'apprécie cela pour la seule raison que: NOT EXISTSfonctionne de la façon dont je m'attends NOT INà fonctionner (ce qui n'est pas le cas).
levininja
Avec NOT EXISTS, j'essaie d'utiliser SELECT 1 tel que NOT EXISTS (SELECT 1 FROM quelque chose OERE quelque chose) afin que la base de données n'ait pas réellement besoin de renvoyer des colonnes du disque. Utiliser EXPLAIN pour déterminer si cela fait une différence dans votre cas est probablement une bonne idée.
Mayur Patel
4
@Mayur Pas besoin de cela dans SQL Server. stackoverflow.com/questions/1597442/…
Martin Smith
84

Sachez également que NOT IN n'est pas équivalent à NOT EXISTS lorsqu'il s'agit de null.

Ce post l'explique très bien

http://sqlinthewild.co.za/index.php/2010/02/18/not-exists-vs-not-in/

Lorsque la sous-requête renvoie même un null, NOT IN ne correspondra à aucune ligne.

La raison de cela peut être trouvée en regardant les détails de ce que signifie réellement l'opération NOT IN.

Disons que, à des fins d'illustration, il y a 4 lignes dans la table appelée t, il y a une colonne appelée ID avec les valeurs 1..4

WHERE SomeValue NOT IN (SELECT AVal FROM t)

est équivalent à

WHERE SomeValue != (SELECT AVal FROM t WHERE ID=1)
AND SomeValue != (SELECT AVal FROM t WHERE ID=2)
AND SomeValue != (SELECT AVal FROM t WHERE ID=3)
AND SomeValue != (SELECT AVal FROM t WHERE ID=4)

Disons en outre que AVal est NULL où ID = 4. D'où cette comparaison! = Renvoie INCONNU. La table de vérité logique pour ET indique que INCONNU et VRAI est INCONNU, INCONNU et FAUX est FAUX. Aucune valeur ne peut être ET avec INCONNU pour produire le résultat VRAI

Par conséquent, si une ligne de cette sous-requête renvoie NULL, tout l'opérateur NOT IN sera évalué à FALSE ou NULL et aucun enregistrement ne sera retourné

Buckley
la source
24

Si le planificateur d'exécution dit que ce sont les mêmes, ce sont les mêmes. Utilisez celui qui rendra votre intention plus évidente - dans ce cas, le second.

John Millikin
la source
3
le temps du planificateur d'exécution peut être identique, mais les résultats d'exécution peuvent différer, il y a donc une différence. NOT IN produira des résultats inattendus si vous avez NULL dans votre jeu de données (voir la réponse de Buckley). Il vaut mieux utiliser NOT EXISTS par défaut.
nanonerd
15

En fait, je pense que ce serait le plus rapide:

SELECT ProductID, ProductName 
    FROM Northwind..Products p  
          outer join Northwind..[Order Details] od on p.ProductId = od.ProductId)
WHERE od.ProductId is null
James Curran
la source
2
Peut-être pas le plus rapide lorsque l'optimiseur fait son travail, mais sera certainement plus rapide quand ce n'est pas le cas.
Cade Roux
2
Il a peut-être également simplifié sa requête pour ce message
Kip
1
D'accord La jointure externe gauche est souvent plus rapide qu'une sous-requête.
HLGEM
7
@HLGEM Pas d'accord. D'après mon expérience, le meilleur cas pour LOJ est qu'ils sont les mêmes et SQL Server convertit le LOJ en un anti semi jointure. Dans le pire des cas, SQL Server LEFT JOINT tout et filtre les valeurs NULL, ce qui peut être beaucoup plus inefficace. Exemple de cela au bas de cet article
Martin Smith
12

J'ai une table qui a environ 120 000 enregistrements et je dois sélectionner uniquement ceux qui n'existent pas (correspondant à une colonne varchar) dans quatre autres tables avec un nombre de lignes d'environ 1500, 4000, 40000, 200. Toutes les tables impliquées ont un index unique sur la Varcharcolonne concernée .

NOT INa pris environ 10 minutes, a NOT EXISTSpris 4 secondes.

J'ai une requête récursive qui pourrait avoir une section non accordée qui aurait pu contribuer aux 10 minutes, mais l'autre option prenant 4 secondes explique, au moins pour moi, c'est NOT EXISTSbien mieux ou du moins cela INet EXISTSne sont pas exactement les mêmes et valent toujours un vérifiez avant d'aller de l'avant avec le code.

Yella Chalamala
la source
8

Dans votre exemple spécifique, ils sont identiques, car l'optimiseur a compris que ce que vous essayez de faire est le même dans les deux exemples. Mais il est possible que dans des exemples non triviaux l'optimiseur ne le fasse pas, et dans ce cas, il y a des raisons de préférer l'un à l'autre à l'occasion.

NOT INdevrait être préféré si vous testez plusieurs lignes dans votre sélection externe. La sous-requête à l'intérieur de l' NOT INinstruction peut être évaluée au début de l'exécution, et la table temporaire peut être vérifiée par rapport à chaque valeur dans la sélection externe, plutôt que de réexécuter la sous-sélection à chaque fois comme cela serait requis avec l' NOT EXISTSinstruction.

Si la sous-requête doit être corrélée avec la sélection externe, cela NOT EXISTSpeut être préférable, car l'optimiseur peut découvrir une simplification qui empêche la création de tables temporaires pour exécuter la même fonction.

Jeffrey L Whitledge
la source
6

J'utilisais

SELECT * from TABLE1 WHERE Col1 NOT IN (SELECT Col1 FROM TABLE2)

et a constaté qu'il donnait de mauvais résultats (par mauvais je veux dire aucun résultat). Comme il y avait un NULL dans TABLE2.Col1.

Lors de la modification de la requête en

SELECT * from TABLE1 T1 WHERE NOT EXISTS (SELECT Col1 FROM TABLE2 T2 WHERE T1.Col1 = T2.Col2)

m'a donné les bons résultats.

Depuis lors, j'ai commencé à utiliser NOT EXISTS partout.

ravish.hacker
la source
5

Ils sont très similaires mais pas vraiment les mêmes.

En termes d'efficacité, j'ai trouvé que la jointure gauche est une déclaration nulle plus efficace (quand une abondance de lignes doit être sélectionnée, c'est-à-dire)

Onga Leo-Yoda Vellem
la source
2

Si l'optimiseur dit qu'ils sont identiques, alors considérez le facteur humain. Je préfère voir PAS EXISTE :)

un jour
la source
1

C'est une très bonne question, j'ai donc décidé d'écrire un article très détaillé sur ce sujet sur mon blog.

Modèle de table de base de données

Supposons que nous avons les deux tables suivantes dans notre base de données, qui forment une relation de table un-à-plusieurs.

Tables SQL EXISTS

La studenttable est le parent et la student_gradetable enfant car elle possède une colonne Student_id Foreign Key faisant référence à la colonne id Primary Key dans la table student.

Le student tablecontient les deux enregistrements suivants:

| id | first_name | last_name | admission_score |
|----|------------|-----------|-----------------|
| 1  | Alice      | Smith     | 8.95            |
| 2  | Bob        | Johnson   | 8.75            |

Et, la student_gradetable stocke les notes que les étudiants ont reçues:

| id | class_name | grade | student_id |
|----|------------|-------|------------|
| 1  | Math       | 10    | 1          |
| 2  | Math       | 9.5   | 1          |
| 3  | Math       | 9.75  | 1          |
| 4  | Science    | 9.5   | 1          |
| 5  | Science    | 9     | 1          |
| 6  | Science    | 9.25  | 1          |
| 7  | Math       | 8.5   | 2          |
| 8  | Math       | 9.5   | 2          |
| 9  | Math       | 9     | 2          |
| 10 | Science    | 10    | 2          |
| 11 | Science    | 9.4   | 2          |

SQL EXISTS

Disons que nous voulons obtenir tous les élèves qui ont reçu une note de 10 en classe de mathématiques.

Si nous ne sommes intéressés que par l'identifiant de l'élève, nous pouvons exécuter une requête comme celle-ci:

SELECT
    student_grade.student_id
FROM
    student_grade
WHERE
    student_grade.grade = 10 AND
    student_grade.class_name = 'Math'
ORDER BY
    student_grade.student_id

Mais, l'application est intéressée à afficher le nom complet d'un student, pas seulement l'identifiant, nous avons donc également besoin des informations de la studenttable.

Afin de filtrer les studentenregistrements qui ont une note de 10 en mathématiques, nous pouvons utiliser l'opérateur SQL EXISTS, comme ceci:

SELECT
    id, first_name, last_name
FROM
    student
WHERE EXISTS (
    SELECT 1
    FROM
        student_grade
    WHERE
        student_grade.student_id = student.id AND
        student_grade.grade = 10 AND
        student_grade.class_name = 'Math'
)
ORDER BY id

Lors de l'exécution de la requête ci-dessus, nous pouvons voir que seule la ligne Alice est sélectionnée:

| id | first_name | last_name |
|----|------------|-----------|
| 1  | Alice      | Smith     |

La requête externe sélectionne les studentcolonnes de lignes que nous souhaitons renvoyer au client. Cependant, la clause WHERE utilise l'opérateur EXISTS avec une sous-requête interne associée.

L'opérateur EXISTS renvoie true si la sous-requête renvoie au moins un enregistrement et false si aucune ligne n'est sélectionnée. Le moteur de base de données n'a pas à exécuter entièrement la sous-requête. Si un seul enregistrement correspond, l'opérateur EXISTS renvoie true et l'autre ligne de requête associée est sélectionnée.

La sous-requête interne est corrélée car la colonne student_id de la student_gradetable est comparée à la colonne id de la table student externe.

SQL N'EXISTE PAS

Considérons que nous voulons sélectionner tous les élèves qui n'ont pas de note inférieure à 9. Pour cela, nous pouvons utiliser NOT EXISTS, ce qui annule la logique de l'opérateur EXISTS.

Par conséquent, l'opérateur NOT EXISTS renvoie true si la sous-requête sous-jacente ne renvoie aucun enregistrement. Cependant, si un seul enregistrement correspond à la sous-requête interne, l'opérateur NOT EXISTS renvoie false et l'exécution de la sous-requête peut être arrêtée.

Pour faire correspondre tous les enregistrements d'étudiant qui n'ont pas de student_grade associé avec une valeur inférieure à 9, nous pouvons exécuter la requête SQL suivante:

SELECT
    id, first_name, last_name
FROM
    student
WHERE NOT EXISTS (
    SELECT 1
    FROM
        student_grade
    WHERE
        student_grade.student_id = student.id AND
        student_grade.grade < 9
)
ORDER BY id

Lors de l'exécution de la requête ci-dessus, nous pouvons voir que seul l'enregistrement Alice est mis en correspondance:

| id | first_name | last_name |
|----|------------|-----------|
| 1  | Alice      | Smith     |

Ainsi, l'avantage d'utiliser les opérateurs SQL EXISTS et NOT EXISTS est que l'exécution de la sous-requête interne peut être arrêtée tant qu'un enregistrement correspondant est trouvé.

Vlad Mihalcea
la source
-1

Ça dépend..

SELECT x.col
FROM big_table x
WHERE x.key IN( SELECT key FROM really_big_table );

ne serait pas relativement lent, il n'y a pas grand-chose pour limiter la taille de ce que la requête vérifie pour voir si elles sont entrées. EXISTS serait préférable dans ce cas.

Mais, selon l'optimiseur du SGBD, cela ne pourrait pas être différent.

Comme exemple de quand EXISTS est meilleur

SELECT x.col
FROM big_table x
WHERE EXISTS( SELECT key FROM really_big_table WHERE key = x.key);
  AND id = very_limiting_criteria
Greg Ogle
la source
1
INet EXISTS obtenez le même plan dans SQL Server . La question est sur le point NOT INvs de NOT EXISTStoute façon.
Martin Smith