Inverter uma expressão booleana que pode retornar DESCONHECIDO

11

Exemplo

Eu tenho uma mesa

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

e uma expressão booleana T-SQL que pode ser avaliada como TRUE, FALSE ou (devido à lógica ternária do SQL) UNKNOWN:

SELECT * FROM myTable WHERE myField = 'someValue'

-- yields record 1

Se eu quiser obter todos os outros registros , não posso simplesmente negar a expressão

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

-- yields only record 3

Eu sei como isso acontece (lógica ternária) e sei como resolver esse problema específico.

Eu sei que posso apenas usar myField = 'someValue' AND NOT myField IS NULLe recebo uma expressão "invertível" que nunca produz UNKNOWN:

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

-- yields records 2 and 3, hooray!

Caso Geral

Agora, vamos falar sobre o caso geral. Digamos que em vez de myField = 'someValue'eu tenho uma expressão complexa envolvendo muitos campos e condições, talvez subconsultas:

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

Existe uma maneira genérica de "inverter" essa expulsão? Pontos de bônus se funcionar para subexpressões:

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

Preciso dar suporte ao SQL Server 2008-2014, mas se houver uma solução elegante que exija uma versão mais recente que 2008, também estou interessado em saber sobre isso.

Heinzi
fonte

Respostas:

15

Você pode colocar a condição em uma expressão CASE que retorne um resultado binário, por exemplo 1 ou 0:

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

Negar a expressão fornecerá todas as outras linhas da mesma fonte de dados, incluindo aquelas em que someColumn é nulo:

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
;

Desde o SQL Server 2012, você também tem a função IIF , que é apenas um invólucro em torno de um CASE binário como acima. Portanto, esta expressão CASE:

CASE WHEN someColumn = someValue THEN 1 ELSE 0 END

terá esta aparência se reescrita usando IIF:

IIF(someColumn = someValue, 1, 0)

E você pode usá-lo exatamente da mesma maneira que a expressão CASE. Não haverá diferença no desempenho, apenas o código será um pouco mais conciso, possivelmente mais limpo também.

Andriy M
fonte
Essa é uma boa ideia! Use CASE para "converter" uma expressão booleana em uma expressão com a qual possa trabalhar e, em seguida, use uma comparação para "convertê-la" em uma expressão booleana.
Heinzi 21/03
10

O primeiro pensamento que me ocorre:

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;

Devoluções:

resultado

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

Devoluções:

Resultado Negado

Isso depende da maneira como EXISTSsempre retorna verdadeiro ou falso , nunca desconhecido . SELECT 1 WHEREInfelizmente, a necessidade é necessária, mas pode ser viável para sua exigência, por exemplo:

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

Consulte EXISTS (Transact-SQL)


Um exemplo trabalhado um pouco mais complexo, mostrando como um EXISTSou outro CASE/IIFmétodos podem ser aplicados para inverter predicados individuais:

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

Código:

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

Se você não se importa em reescrever as sub-expressões antecipadamente, pode usar COALESCE:

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

Você deve se certificar de que 'notSomeValue'é diferente de 'someValue'; de preferência, seria algum valor completamente ilegal para a coluna. (Também não pode ser NULL, é claro.) Isso é fácil negar, mesmo se você tiver uma lista longa:

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

Mais limpo, mais simples e mais óbvio do que CASEou IIF, na minha opinião. A principal desvantagem é ter um segundo valor que você sabe que não é igual, mas isso só é realmente um problema se você não souber o valor real antecipadamente. Nesse caso, você pode fazer o que Hanno Binder sugere e usar COALESCE(myField, CONCAT('not', 'someValue')) = 'someValue'(onde 'someValue'seria realmente parametrizado).

COALESCE está documentado para estar disponível a partir do SQL Server 2005.

Lembre-se de que mexer com sua consulta dessa maneira (usando qualquer um dos métodos recomendados aqui) pode dificultar a otimização do banco de dados. Para conjuntos de dados grandes, IS NULLé provável que a versão seja mais fácil de otimizar.

jpmc26
fonte
1
COALESCE(myField, CONCAT('not', 'someValue')) = 'someValue'deve funcionar para qualquer "someValue" e qualquer dado na tabela.
22716 JimmyB
2

Existe o operador EXCEPT set embutido que, efetivamente, remove os resultados de uma segunda consulta da primeira.

select * from table
except
select * from table
where <really complex predicates>
Michael Green
fonte
Vamos esperar que seja uma pequena mesa :-)
Lennart
-4

O COALESCE está disponível?

SELECT * FROM myTable WHERE NOT COALESCE(myField = 'someValue', FALSE)
Malvolio
fonte
4
Sim, COALESCE está disponível, mas não, isso não funcionará: (a) COALESCE não aceitará uma expressão booleana (nem ISNULL, a propósito) e (b) o valor de verdade FALSE não está diretamente disponível no SQL como um literal. Experimente e você receberá um erro de sintaxe.
Heinzi 21/03
@ Heinzi - Eu tentei, funcionou, é por isso que eu postei. Talvez não funcione no T-SQL, mas é bom no Postgres e no MySQL.
Malvolio 21/03
2
@ Malvolio: A questão está marcada sql-server, porém, não mysqlou postgresql.
22316 Andriy M
@ Malvolio é porque o Postgres tem um BOOLEANtipo e o MySQL tem um BOOLEANtipo (falsificado) que pode ser parâmetros da COALESCE()função. Se a pergunta tivesse sido marcada com sql-agnosticou sql-standard, a resposta seria boa.
ypercubeᵀᴹ
@ ypercubeᵀᴹ - eh, o que posso lhe dizer? Obtenha um banco de dados melhor.
Malvolio 23/03