Inverse une expression booléenne qui peut renvoyer UNKNOWN

11

Exemple

J'ai une table

ID  myField
------------
 1  someValue
 2  NULL
 3  someOtherValue

et une expression booléenne T-SQL qui peut être évaluée à VRAI, FAUX ou (en raison de la logique ternaire de SQL) INCONNU:

SELECT * FROM myTable WHERE myField = 'someValue'

-- yields record 1

Si je veux obtenir tous les autres enregistrements , je ne peux pas simplement nier l'expression

SELECT * FROM myTable WHERE NOT (myField = 'someValue')

-- yields only record 3

Je sais comment cela se produit (logique ternaire), et je sais comment résoudre ce problème spécifique.

Je sais que je peux simplement utiliser myField = 'someValue' AND NOT myField IS NULLet j'obtiens une expression "inversible" qui ne donne jamais INCONNU:

SELECT * FROM myTable WHERE NOT (myField = 'someValue' AND myField IS NOT NULL)

-- yields records 2 and 3, hooray!

Cas général

Maintenant, parlons du cas général. Disons qu'au lieu d' myField = 'someValue'avoir une expression complexe impliquant de nombreux champs et conditions, peut-être des sous-requêtes:

SELECT * FROM myTable WHERE ...some complex Boolean expression...

Existe-t-il un moyen générique pour "inverser" cette expession? Points bonus si cela fonctionne pour les sous-expressions:

SELECT * FROM myTable 
 WHERE ...some expression which stays... 
   AND ...some expression which I might want to invert...

Je dois prendre en charge SQL Server 2008-2014, mais s'il existe une solution élégante nécessitant une version plus récente que 2008, je suis également intéressé d'en entendre parler.

Heinzi
la source

Réponses:

15

Vous pouvez inclure la condition dans une expression CASE qui renvoie un résultat binaire, par exemple 1 ou 0:

SELECT
  ...
FROM
  ...
WHERE
  CASE WHEN someColumn = someValue THEN 1 ELSE 0 END = 1
;

La négation de l'expression vous donnera toutes les autres lignes de la même source de données, y compris celles où someColumn est null:

SELECT
  ...
FROM
  ...
WHERE
  NOT CASE WHEN someColumn = someValue THEN 1 ELSE 0 END = 1
  -- or: CASE WHEN someColumn = someValue THEN 1 ELSE 0 END <> 1
;

Depuis SQL Server 2012, vous avez également la fonction IIF , qui n'est qu'un wrapper autour d'un CAS binaire comme ci-dessus. Donc, cette expression CASE:

CASE WHEN someColumn = someValue THEN 1 ELSE 0 END

ressemblera à ceci si réécrit en utilisant IIF:

IIF(someColumn = someValue, 1, 0)

Et vous pouvez l'utiliser exactement de la même manière que l'expression CASE. Il n'y aura pas de différence de performances, seul le code sera légèrement plus concis, peut-être aussi plus propre de cette façon.

Andriy M
la source
Voilà une bonne idée! Utilisez CASE pour «convertir» une expression booléenne en une expression avec laquelle vous pouvez travailler, puis utilisez une comparaison pour la «reconvertir» en une expression booléenne.
Heinzi
10

La première pensée qui me vient à l'esprit:

DECLARE @T AS table (c1 integer NULL);

INSERT @T (c1)
VALUES (1), (NULL), (2);

-- Original expression c1 = 1
SELECT T.c1
FROM @T AS T
WHERE c1 = 1;

Retour:

résultat

-- Negated
SELECT T.c1
FROM @T AS T
WHERE NOT EXISTS (SELECT 1 WHERE c1 = 1);

Retour:

Résultat négatif

Cela repose sur la façon dont EXISTSretourne toujours vrai ou faux , jamais inconnu . Le besoin de SELECT 1 WHEREest malheureusement nécessaire, mais il pourrait être réalisable pour votre besoin, par exemple:

sql = "
    SELECT * 
    FROM someTable 
    WHERE " + someExpression + 
    " AND NOT EXISTS (SELECT 1 WHERE " + 
    someOtherExpression + ")";
result = executeAndShow(sql);

Voir EXISTS (Transact-SQL)


Un exemple travaillé un peu plus complexe montrant comment les méthodes EXISTSou CASE/IIFpourraient être appliquées pour inverser les prédicats individuels:

DECLARE @T AS table 
(
    c1 integer NULL,
    c2 integer NULL,
    c3 integer NULL
);

INSERT @T 
    (c1, c2, c3)
VALUES 
    (1, NULL, 2),
    (2, 2, 3),
    (NULL, 1, 4);

Code:

-- Original
SELECT 
    T.c1,
    T.c2,
    T.c3
FROM @T AS T
WHERE
    1 = 1
    -- Predicate #1
    AND T.c1 = 2
    -- Predicate #2
    AND T.c2 =
    (
        SELECT MAX(T2.c2)
        FROM @T AS T2
        WHERE T2.c2 IS NOT NULL
    )
    -- Predicate #3
    AND T.c3 IN (3, 4)
    ;

-- Invert predicates #1 and #2
SELECT 
    T.c1,
    T.c2,
    T.c3
FROM @T AS T
WHERE
    1 = 1
    AND NOT EXISTS (SELECT 1 WHERE 1 = 1
        -- Predicate #1
        AND T.c1 = 2)
    AND NOT EXISTS (SELECT 1 WHERE 1 = 1
        -- Predicate #2
            AND T.c2 =
            (
                SELECT MAX(T2.c2)
                FROM @T AS T2
                WHERE T2.c2 IS NOT NULL
            ))
    -- Predicate #3
    AND T.c3 IN (3, 4)
    ;
Paul White 9
la source
3

Si cela ne vous dérange pas de réécrire les sous-expressions à l'avance, vous pouvez utiliser COALESCE:

SELECT *
FROM myTable
WHERE NOT (COALESCE(myField, 'notSomeValue') = 'someValue')

Vous devez vous assurer que cela 'notSomeValue'est distinct de 'someValue'; de préférence, ce serait une valeur complètement illégale pour la colonne. (Cela ne peut pas l'être non NULLplus, bien sûr.) C'est facile à nier, même si vous avez une longue liste:

SELECT *
FROM myTable
WHERE NOT (
    COALESCE(myField, 'notSomeValue') = 'someValue' AND
    COALESCE(myField2, 'notSomeValue') = 'someValue2' AND
    COALESCE(myField3, 'notSomeValue') = 'someValue3' AND
    COALESCE(myField4, 'notSomeValue') = 'someValue4'
)

Plus propre, plus simple et plus évident que CASEou IIF, à mon avis. L'inconvénient principal est d'avoir une deuxième valeur que vous savez n'est pas égale, mais ce n'est vraiment un problème que si vous ne connaissez pas la valeur réelle à l'avance. Dans ce cas, vous pouvez faire ce que Hanno Binder suggère et utiliser COALESCE(myField, CONCAT('not', 'someValue')) = 'someValue'(où 'someValue'serait réellement paramétré).

COALESCE est disponible à partir de SQL Server 2005.

Sachez que jouer avec votre requête comme celui-ci (en utilisant l'une des méthodes recommandées ici) peut rendre plus difficile pour la base de données l'optimisation de votre requête. Pour les grands ensembles de données, la IS NULLversion est probablement plus facile à optimiser.

jpmc26
la source
1
COALESCE(myField, CONCAT('not', 'someValue')) = 'someValue'devrait fonctionner pour toute "someValue" et toutes les données de la table.
JimmyB
2

Il y a l' opérateur de jeu EXCEPT intégré qui, en fait, supprime les résultats d'une seconde requête des premiers.

select * from table
except
select * from table
where <really complex predicates>
Michael Green
la source
Espérons que ce soit une petite table :-)
Lennart
-4

COALESCE est-il disponible?

SELECT * FROM myTable WHERE NOT COALESCE(myField = 'someValue', FALSE)
Malvolio
la source
4
Oui, COALESCE est disponible, mais non, cela ne fonctionnera pas: (a) COALESCE n'acceptera pas une expression booléenne (ni ISNULL, d'ailleurs) et (b) la valeur de vérité FALSE n'est pas directement disponible dans SQL comme un littéral. Essayez-le et vous obtiendrez une erreur de syntaxe.
Heinzi
@Heinzi - Je l'ai essayé, ça a marché, c'est pourquoi je l'ai posté. Peut-être que cela ne fonctionne pas sur T-SQL, mais ça va sur Postgres et MySQL.
Malvolio
2
@Malvolio: La question est cependant balisée sql-server, pas mysqlou postgresql.
Andriy M
@Malvolio c'est parce que Postgres a un BOOLEANtype et MySQL a un type (truqué) BOOLEANqui peut être des paramètres de la COALESCE()fonction. Si la question avait été marquée avec sql-agnosticou sql-standard, la réponse serait correcte.
ypercubeᵀᴹ
@ ypercubeᵀᴹ - eh, que puis-je vous dire? Obtenez une meilleure base de données.
Malvolio