Invertieren Sie einen booleschen Ausdruck, der UNBEKANNT zurückgeben kann

11

Beispiel

Ich habe einen Tisch

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

und ein boolescher T-SQL-Ausdruck, der als TRUE, FALSE oder (aufgrund der ternären Logik von SQL) UNBEKANNT ausgewertet werden kann:

SELECT * FROM myTable WHERE myField = 'someValue'

-- yields record 1

Wenn ich alle anderen Datensätze erhalten möchte , kann ich den Ausdruck nicht einfach negieren

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

-- yields only record 3

Ich weiß, warum dies geschieht (ternäre Logik), und ich weiß, wie ich dieses spezielle Problem lösen kann.

Ich weiß, dass ich nur verwenden kann myField = 'someValue' AND NOT myField IS NULLund ich bekomme einen "invertierbaren" Ausdruck, der niemals UNBEKANNT ergibt:

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

-- yields records 2 and 3, hooray!

Allgemeiner Fall

Lassen Sie uns nun über den allgemeinen Fall sprechen. Lassen Sie uns sagen , anstatt myField = 'someValue'ich etwas komplexer Ausdruck haben viele Felder und Bedingungen , die, vielleicht Subqueries:

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

Gibt es eine generische Möglichkeit, diese Expedition "umzukehren"? Bonuspunkte, wenn es für Unterausdrücke funktioniert:

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

Ich muss SQL Server 2008-2014 unterstützen, aber wenn es eine elegante Lösung gibt, die eine neuere Version als 2008 erfordert, bin ich auch daran interessiert, davon zu hören.

Heinzi
quelle

Antworten:

15

Sie können die Bedingung in einen CASE-Ausdruck einschließen, der ein binäres Ergebnis zurückgibt, z. B. 1 oder 0:

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

Wenn Sie den Ausdruck negieren, erhalten Sie alle anderen Zeilen aus derselben Datenquelle, einschließlich derer, in denen someColumn null ist:

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
;

Seit SQL Server 2012 haben Sie auch die IIF-Funktion , die nur ein Wrapper um einen binären CASE wie oben ist. Also, dieser CASE-Ausdruck:

CASE WHEN someColumn = someValue THEN 1 ELSE 0 END

sieht so aus, wenn es mit IIF umgeschrieben wird:

IIF(someColumn = someValue, 1, 0)

Und Sie können es genauso verwenden wie den CASE-Ausdruck. Es wird keinen Unterschied in der Leistung geben, nur der Code wird etwas prägnanter sein, möglicherweise auch auf diese Weise sauberer.

Andriy M.
quelle
Das ist eine schöne Idee! Verwenden Sie CASE, um einen Booleschen Ausdruck in einen Ausdruck zu "konvertieren", mit dem Sie arbeiten können, und verwenden Sie dann einen Vergleich, um ihn wieder in einen Booleschen Ausdruck zu "konvertieren".
Heinzi
10

Der erste Gedanke, der mir einfällt:

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;

Kehrt zurück:

Ergebnis

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

Kehrt zurück:

Negiertes Ergebnis

Dies hängt davon ab, wie EXISTSimmer wahr oder falsch zurückgegeben wird , niemals unbekannt . Die Notwendigkeit für die SELECT 1 WHEREist leider notwendig, aber es könnte für Ihre Anforderung praktikabel sein, zum Beispiel:

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

Siehe EXISTS (Transact-SQL)


Ein etwas komplexeres Beispiel zeigt, wie entweder EXISTSoder CASE/IIFMethoden angewendet werden können, um einzelne Prädikate zu invertieren:

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
quelle
3

Wenn es Ihnen nichts ausmacht, die Unterausdrücke im Voraus neu zu schreiben, können Sie Folgendes verwenden COALESCE:

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

Sie müssen sicherstellen, dass 'notSomeValue'sich das von unterscheidet 'someValue'; vorzugsweise wäre es ein völlig illegaler Wert für die Spalte. (Das kann NULLnatürlich auch nicht sein .) Dies ist leicht zu negieren, selbst wenn Sie eine lange Liste haben:

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

Sauberer, einfacher und offensichtlicher als CASEoder IIF, meiner Meinung nach. Der Hauptnachteil ist, dass ein zweiter Wert, von dem Sie wissen, dass er nicht gleich ist, aber dies ist nur dann wirklich ein Problem, wenn Sie den tatsächlichen Wert im Voraus nicht kennen. In diesem Fall können Sie tun, was Hanno Binder vorschlägt und verwendet COALESCE(myField, CONCAT('not', 'someValue')) = 'someValue'(wo 'someValue'tatsächlich parametrisiert werden würde).

COALESCE Es ist dokumentiert, dass es ab SQL Server 2005 verfügbar ist.

Beachten Sie, dass das Durcheinander mit Ihrer Abfrage (unter Verwendung einer der hier empfohlenen Methoden) es für die Datenbank schwieriger machen kann, Ihre Abfrage zu optimieren. Bei großen Datenmengen ist die IS NULLVersion wahrscheinlich einfacher zu optimieren.

jpmc26
quelle
1
COALESCE(myField, CONCAT('not', 'someValue')) = 'someValue'sollte für jeden "someValue" und alle Daten in der Tabelle funktionieren.
JimmyB
2

Es gibt den integrierten EXCEPT- Set-Operator, der die Ergebnisse einer zweiten Abfrage effektiv aus den Ergebnissen der ersten Abfrage entfernt.

select * from table
except
select * from table
where <really complex predicates>
Michael Green
quelle
Hoffen wir, dass es ein kleiner Tisch ist :-)
Lennart
-4

Ist COALESCE verfügbar?

SELECT * FROM myTable WHERE NOT COALESCE(myField = 'someValue', FALSE)
Malvolio
quelle
4
Ja, COALESCE ist verfügbar, aber nein, dies funktioniert nicht: (a) COALESCE akzeptiert keinen booleschen Ausdruck (ISNULL übrigens auch nicht) und (b) der Wahrheitswert FALSE ist in SQL as nicht direkt verfügbar ein wörtliches. Probieren Sie es aus und Sie erhalten einen Syntaxfehler.
Heinzi
@Heinzi - Ich habe es versucht, es hat funktioniert, deshalb habe ich es gepostet. Vielleicht funktioniert es nicht unter T-SQL, aber unter Postgres und MySQL ist es in Ordnung.
Malvolio
2
@ Malvolio: Die Frage ist jedoch markiert sql-server, nicht mysqloder postgresql.
Andriy M
@Malvolio, weil Postgres einen BOOLEANTyp hat und MySQL einen (gefälschten) BOOLEANTyp, der Parameter der COALESCE()Funktion sein kann. Wenn die Frage mit sql-agnosticoder markiert sql-standardwäre, wäre die Antwort in Ordnung.
Ypercubeᵀᴹ
@ ypercubeᵀᴹ - eh, was kann ich dir sagen? Holen Sie sich eine bessere Datenbank.
Malvolio