Invertire un'espressione booleana che può restituire SCONOSCIUTO


11

Esempio

Ho un tavolo

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

e un'espressione booleana T-SQL che può valutare su VERO, FALSO o (a causa della logica ternaria di SQL) SCONOSCIUTO:

SELECT * FROM myTable WHERE myField = 'someValue'

-- yields record 1

Se voglio ottenere tutti gli altri record , non posso semplicemente negare l'espressione

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

-- yields only record 3

So come ciò accada (logica ternaria) e so come risolvere questo problema specifico.

So che posso semplicemente usare myField = 'someValue' AND NOT myField IS NULLe ottengo un'espressione "invertibile" che non produce mai SCONOSCIUTO:

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

-- yields records 2 and 3, hooray!

Caso generale

Ora parliamo del caso generale. Diciamo invece myField = 'someValue'che ho un'espressione complessa che coinvolge molti campi e condizioni, forse sottoquery:

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

Esiste un modo generico per "invertire" questa espansione? Punti bonus se funziona per le sottoespressioni:

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

Devo supportare SQL Server 2008-2014, ma se esiste una soluzione elegante che richiede una versione più recente rispetto al 2008, sono interessato anche a sentirne parlare.

Risposte:


15

È possibile racchiudere la condizione in un'espressione CASE che restituisce un risultato binario, ad esempio 1 o 0:

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

La negazione dell'espressione ti darà tutte le altre righe dalla stessa origine dati, comprese quelle in cui someColumn è 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
;

Da SQL Server 2012 hai anche la funzione IIF , che è solo un wrapper attorno a un CASO binario come sopra. Quindi, questa espressione CASE:

CASE WHEN someColumn = someValue THEN 1 ELSE 0 END

apparirà così se riscritto usando IIF:

IIF(someColumn = someValue, 1, 0)

E puoi usarlo esattamente allo stesso modo dell'espressione CASE. Non ci saranno differenze nelle prestazioni, solo il codice sarà leggermente più conciso, forse anche più pulito in questo modo.


Questa è una bella idea! Utilizzare CASE per "convertire" un'espressione booleana in un'espressione con cui si può lavorare, quindi utilizzare un confronto per "convertirla" in un'espressione booleana.
Heinzi,

10

Il primo pensiero che mi viene in mente:

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;

Ritorna:

risultato

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

Ritorna:

Risultato negativo

Questo si basa sul modo in cui EXISTSrestituisce sempre vero o falso , mai sconosciuto . SELECT 1 WHERESfortunatamente, la necessità di è necessaria, ma potrebbe essere realizzabile per le tue esigenze, ad esempio:

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

Vedi EXISTS (Transact-SQL)


Un esempio leggermente più complesso che mostra come uno EXISTSo due CASE/IIFmetodi potrebbero essere applicati per invertire singoli predicati:

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);

Codice:

-- 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)
    ;

3

Se non ti dispiace riscrivere le sottoespressioni in anticipo, puoi usare COALESCE:

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

È necessario assicurarsi che 'notSomeValue'sia distinto da 'someValue'; preferibilmente, sarebbe un valore completamente illegale per la colonna. (Non può essere NULLneanche, ovviamente.) È facile negarlo, anche se hai una lunga lista:

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

Secondo me , più pulito, più semplice e più ovvio di CASEo IIF. L'aspetto negativo principale è avere un secondo valore che sai non è uguale, ma questo è davvero un problema solo se non conosci il valore reale in anticipo. In tal caso, puoi fare come suggerisce e utilizzare Hanno BinderCOALESCE(myField, CONCAT('not', 'someValue')) = 'someValue' (dove 'someValue'sarebbe effettivamente parametrizzato).

COALESCE è documentato per essere disponibile da SQL Server 2005 in poi.

Tieni presente che la confusione con la tua query in questo modo (utilizzando uno dei metodi consigliati qui) può rendere più difficile per il database ottimizzare la tua query. Per set di dati di grandi dimensioni, la IS NULLversione è probabilmente più facile da ottimizzare.


1
COALESCE(myField, CONCAT('not', 'someValue')) = 'someValue'dovrebbe funzionare per qualsiasi "someValue" e tutti i dati nella tabella.
JimmyB,

2

C'è l' operatore di set EXCEPT integrato che, in effetti, rimuove i risultati di una seconda query dalla prima query.

select * from table
except
select * from table
where <really complex predicates>

Speriamo che sia un tavolino :-)
Lennart,

-4

COALESCE è disponibile?

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

4
Sì, COALESCE è disponibile, ma no, non funzionerà: (a) COALESCE non accetterà un'espressione booleana (né ISNULL, tra l'altro) e (b) il valore di verità FALSE non è direttamente disponibile in SQL come un letterale. Provalo e otterrai un errore di sintassi.
Heinzi,

@Heinzi - L'ho provato, ha funzionato, ecco perché l'ho pubblicato. Forse non funziona su T-SQL, ma va bene su Postgres e MySQL.
Malvolio,

2
@Malvolio: la domanda è taggata sql-server, comunque, mysqlo no postgresql.
Andriy M,

@Malvolio è perché Postgres ha un BOOLEANtipo e MySQL ha un tipo (falso) BOOLEANche può essere un parametro della COALESCE()funzione. Se la domanda fosse stata taggata con sql-agnostico sql-standard, la risposta sarebbe ok.
ypercubeᵀᴹ

@ ypercubeᵀᴹ - eh, cosa posso dirti? Ottieni un database migliore.
Malvolio,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.