Invierte una expresión booleana que puede devolver DESCONOCIDO

11

Ejemplo

Tengo una mesa

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

y una expresión booleana T-SQL que puede evaluar como VERDADERO, FALSO o (debido a la lógica ternaria de SQL) DESCONOCIDO:

SELECT * FROM myTable WHERE myField = 'someValue'

-- yields record 1

Si quiero obtener todos los demás registros , no puedo simplemente negar la expresión

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

-- yields only record 3

Sé cómo sucede esto (lógica ternaria) y sé cómo resolver este problema específico.

Sé que puedo usar myField = 'someValue' AND NOT myField IS NULLy obtengo una expresión "invertible" que nunca produce DESCONOCIDO:

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

-- yields records 2 and 3, hooray!

Caso general

Ahora, hablemos del caso general. Digamos que en lugar de myField = 'someValue'tener una expresión compleja que involucra muchos campos y condiciones, quizás subconsultas:

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

¿Hay alguna forma genérica de "invertir" esta expresión? Puntos de bonificación si funciona para subexpresiones:

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

Necesito admitir SQL Server 2008-2014, pero si hay una solución elegante que requiera una versión más nueva que la de 2008, también estoy interesado en conocerla.

Heinzi
fuente

Respuestas:

15

Puede encerrar la condición en una expresión CASE que devuelve un resultado binario, por ejemplo 1 o 0:

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

Negar la expresión le dará todas las otras filas del mismo origen de datos, incluidas aquellas en las que someColumn es 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 SQL Server 2012 también tiene la función IIF , que es solo un contenedor alrededor de un CASO binario como el anterior. Entonces, esta expresión de CASO:

CASE WHEN someColumn = someValue THEN 1 ELSE 0 END

se verá así si se reescribe usando IIF:

IIF(someColumn = someValue, 1, 0)

Y puede usarlo exactamente de la misma manera que la expresión CASE. No habrá diferencia en el rendimiento, solo el código será un poco más conciso, posiblemente más limpio de esa manera también.

Andriy M
fuente
Esa es una buena idea! Use CASE para "convertir" una expresión booleana en una expresión con la que uno pueda trabajar, y luego use una comparación para "convertirla" nuevamente en una expresión booleana.
Heinzi
10

El primer pensamiento que se me ocurre:

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;

Devoluciones:

resultado

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

Devoluciones:

Resultado negado

Esto se basa en la forma en que EXISTSsiempre devuelve verdadero o falso , nunca desconocido . SELECT 1 WHEREDesafortunadamente, la necesidad de esto es necesaria, pero podría ser viable para sus requisitos, por ejemplo:

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

Ver EXISTE (Transact-SQL)


Un ejemplo práctico ligeramente más complejo que muestra cómo ya sea EXISTSo CASE/IIFmétodos podría aplicarse a invertir predicados individuales:

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

Si no le importa reescribir las subexpresiones por adelantado, puede usar COALESCE:

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

Usted debe asegurarse de que 'notSomeValue'es distinta de 'someValue'; preferiblemente, sería un valor completamente ilegal para la columna. (No puede ser NULL, tampoco, por supuesto). Esto es fácil de negar, incluso si tiene una lista larga:

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

Más limpio, más simple y más obvio que CASEo IIF, en mi opinión. El principal inconveniente es que tener un segundo valor que usted sabe no es igual, pero esto solo es realmente un problema si no conoce el valor real por adelantado. En ese caso, puede hacer lo que Hanno Binder sugiere y usar COALESCE(myField, CONCAT('not', 'someValue')) = 'someValue'(donde 'someValue'realmente estaría parametrizado).

COALESCE está documentado para estar disponible desde SQL Server 2005 en adelante.

Tenga en cuenta que jugar con su consulta de esta manera (usando cualquiera de los métodos recomendados aquí) puede dificultar que la base de datos optimice su consulta. Para grandes conjuntos de datos, IS NULLes probable que la versión sea más fácil de optimizar.

jpmc26
fuente
1
COALESCE(myField, CONCAT('not', 'someValue')) = 'someValue'debería funcionar para cualquier "someValue" y cualquier dato en la tabla.
JimmyB
2

Existe el operador de conjunto EXCEPT incorporado que, efectivamente, elimina los resultados de una segunda consulta de la primera consulta.

select * from table
except
select * from table
where <really complex predicates>
Michael Green
fuente
Esperemos que sea una mesa pequeña :-)
Lennart
-4

¿COALESCE está disponible?

SELECT * FROM myTable WHERE NOT COALESCE(myField = 'someValue', FALSE)
Malvolio
fuente
44
Sí, COALESCE está disponible, pero no, esto no funcionará: (a) COALESCE no aceptará una expresión booleana (tampoco ISNULL, por cierto) y (b) el valor de verdad FALSE no está directamente disponible en SQL como un literal Pruébelo y obtendrá un error de sintaxis.
Heinzi
@Heinzi: lo probé, funcionó, por eso lo publiqué. Tal vez no funcione en T-SQL, pero está bien en Postgres y MySQL.
Malvolio
2
@Malvolio: Sin embargo, la pregunta está etiquetada sql-server, no mysqlo postgresql.
Andriy M
@Malvolio se debe a que Postgres tiene un BOOLEANtipo y MySQL tiene un tipo (falso) BOOLEANque pueden ser parámetros de la COALESCE()función. Si la pregunta hubiera sido etiquetada con sql-agnostico sql-standard, la respuesta estaría bien.
ypercubeᵀᴹ
@ ypercubeᵀᴹ - eh, ¿qué puedo decirte? Obtén una mejor base de datos.
Malvolio