Инвертировать логическое выражение, которое может возвращать UNKNOWN


11

пример

У меня есть стол

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

и логическое выражение T-SQL, которое может принимать значения ИСТИНА, ЛОЖЬ или (из-за троичной логики SQL) НЕИЗВЕСТНО:

SELECT * FROM myTable WHERE myField = 'someValue'

-- yields record 1

Если я хочу получить все остальные записи , я не могу просто отрицать выражение

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

-- yields only record 3

Я знаю, как это происходит (троичная логика), и я знаю, как решить эту конкретную проблему.

Я знаю, что могу просто использовать, myField = 'someValue' AND NOT myField IS NULLи я получаю «обратимое» выражение, которое никогда не приводит к НЕИЗВЕСТНОМУ:

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

-- yields records 2 and 3, hooray!

Общий случай

Теперь поговорим об общем случае. Допустим, вместо myField = 'someValue'меня есть какое-то сложное выражение, включающее множество полей и условий, может быть, подзапросы:

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

Есть ли общий способ «инвертировать» это выражение? Бонусные баллы, если это работает для подвыражений:

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

Мне нужно поддерживать SQL Server 2008-2014, но если есть элегантное решение, требующее более новой версии, чем 2008, мне тоже интересно об этом услышать.

Ответы:


15

Вы можете заключить условие в выражение CASE, которое возвращает двоичный результат, например 1 или 0:

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

Отрицание выражения даст вам все остальные строки из того же источника данных, включая те, в которых 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
;

Начиная с SQL Server 2012 у вас также есть функция IIF , которая является просто оболочкой для двоичного CASE, как описано выше. Итак, это CASE выражение:

CASE WHEN someColumn = someValue THEN 1 ELSE 0 END

будет выглядеть так, если переписать с использованием IIF:

IIF(someColumn = someValue, 1, 0)

И вы можете использовать его точно так же, как выражение CASE. Не будет никакой разницы в производительности, только код будет немного более кратким, возможно, и более чистым.


Это хорошая идея! Используйте CASE, чтобы «преобразовать» логическое выражение в выражение, с которым можно работать, а затем используйте сравнение, чтобы «преобразовать» его обратно в логическое выражение.
Хайнци

10

Первая мысль, которая приходит мне в голову:

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;

Возвращает:

результат

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

Возвращает:

Отрицательный результат

Это зависит от того, как EXISTSвсегда возвращает истину или ложь , никогда неизвестно . К SELECT 1 WHEREсожалению, это необходимо, но оно может быть выполнено по вашему требованию, например:

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

Смотрите EXISTS (Transact-SQL)


Немного более сложный обработанный пример, показывающий, как можно применять методы EXISTSили CASE/IIFметоды для инвертирования отдельных предикатов:

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

Код:

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

Если вы не возражаете переписать подвыражения заранее, вы можете использовать COALESCE:

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

Вы должны убедиться, что 'notSomeValue'это отличается от 'someValue'; предпочтительно, это будет какое-то совершенно недопустимое значение для столбца. (Конечно, и не может быть NULL.) Это легко отрицать, даже если у вас длинный список:

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

Чище, проще и очевиднее, чем CASEили IIF, на мой взгляд. Основным недостатком является наличие второго значения, которое, как вы знаете, не равно, но это действительно проблема, если вы не знаете фактическое значение заранее. В этом случае вы можете делать то, что предлагает и использует Ханно БиндерCOALESCE(myField, CONCAT('not', 'someValue')) = 'someValue' (где на 'someValue'самом деле будет параметризоваться).

COALESCE документально подтверждено, чтобы быть доступным с SQL Server 2005 и далее.

Имейте в виду, что использование такого запроса (используя любой из методов, рекомендованных здесь) может затруднить оптимизацию запроса для базы данных. Для больших наборов данных IS NULLверсию, вероятно, легче оптимизировать.


1
COALESCE(myField, CONCAT('not', 'someValue')) = 'someValue'должен работать для любого "someValue" и любых данных в таблице.
JimmyB

2

Есть встроенный оператор набора EXCEPT, который эффективно удаляет результаты второго запроса из первого запроса.

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

Будем надеяться, что это маленький столик :-)
Леннарт

-4

COALESCE доступен?

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

4
Да, COALESCE доступен, но нет, это не сработает: (a) COALESCE не примет булево выражение (кстати, и не ISNULL) и (b) значение истины FALSE не доступно напрямую в SQL, так как буквальный. Попробуйте, и вы получите синтаксическую ошибку.
Хайнци

@ Heinzi - я попробовал, это сработало, поэтому я опубликовал это. Может быть, он не работает на T-SQL, но это хорошо на Postgres и MySQL.
Мальволио

2
@Malvolio: Вопрос будет помечен sql-server, однако, не mysqlили postgresql.
Андрей М

@Malvolio потому, что у Postgres есть BOOLEANтип, а у MySQL (поддельный) BOOLEANтип, который может быть параметром COALESCE()функции. Если бы вопрос был помечен знаком sql-agnosticили sql-standard, ответ был бы в порядке.
ypercubeᵀᴹ

@ ypercubeᵀᴹ - ну что я могу тебе сказать? Получите лучшую базу данных.
Мальволио
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.