Balikkan ekspresi Boolean yang dapat mengembalikan UNKNOWN

11

Contoh

Saya punya meja

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

dan ekspresi T-SQL Boolean yang dapat mengevaluasi menjadi BENAR, SALAH atau (karena logika ternary SQL) DIKETAHUI:

SELECT * FROM myTable WHERE myField = 'someValue'

-- yields record 1

Jika saya ingin mendapatkan semua catatan lain , saya tidak bisa begitu saja meniadakan ekspresi

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

-- yields only record 3

Saya tahu mengapa ini terjadi (logika ternary), dan saya tahu bagaimana menyelesaikan masalah khusus ini.

Saya tahu saya hanya bisa menggunakan myField = 'someValue' AND NOT myField IS NULLdan saya mendapatkan ekspresi "tidak bisa dibalik" yang tidak pernah menghasilkan UNKNOWN:

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

-- yields records 2 and 3, hooray!

Kasus Umum

Sekarang, mari kita bicara tentang kasus umum. Katakanlah alih-alih myField = 'someValue'saya memiliki ekspresi kompleks yang melibatkan banyak bidang dan kondisi, mungkin subquery:

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

Apakah ada cara generik untuk "membalikkan" pengungkapan ini? Poin bonus jika berfungsi untuk subekspresi:

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

Saya perlu mendukung SQL Server 2008-2014, tetapi jika ada solusi elegan yang membutuhkan versi yang lebih baru dari 2008, saya tertarik untuk mendengarnya juga.

Heinzi
sumber

Jawaban:

15

Anda bisa menyertakan kondisi dalam ekspresi KASUS yang mengembalikan hasil biner, misalnya 1 atau 0:

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

Meniadakan ekspresi akan memberi Anda semua baris lain dari sumber data yang sama, termasuk yang di mana someColumn nol:

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
;

Sejak SQL Server 2012 Anda juga memiliki fungsi IIF , yang hanya merupakan pembungkus KASUS biner seperti di atas. Jadi, ungkapan KASUS ini:

CASE WHEN someColumn = someValue THEN 1 ELSE 0 END

akan terlihat seperti ini jika ditulis ulang menggunakan IIF:

IIF(someColumn = someValue, 1, 0)

Dan Anda dapat menggunakannya dengan cara yang persis sama dengan ekspresi CASE. Tidak akan ada perbedaan dalam kinerja, hanya kode akan sedikit lebih ringkas, mungkin lebih bersih juga.

Andriy M
sumber
Itu ide yang bagus! Gunakan KASUS untuk "mengubah" ekspresi Boolean menjadi ekspresi yang dapat digunakan, dan kemudian menggunakan perbandingan untuk "mengubah" kembali ke ekspresi Boolean.
Heinzi
10

Pikiran pertama yang terjadi pada saya:

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;

Pengembalian:

hasil

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

Pengembalian:

Hasil yang dinegasikan

Ini bergantung pada cara EXISTSselalu mengembalikan benar atau salah , tidak pernah tidak diketahui . SELECT 1 WHERESayangnya, kebutuhan untuk itu diperlukan, tetapi bisa juga sesuai dengan kebutuhan Anda, misalnya:

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

Lihat EXIS (Transact-SQL)


Contoh yang sedikit lebih rumit yang menunjukkan bagaimana salah satu EXISTSatau CASE/IIFmetode dapat diterapkan untuk membalikkan masing-masing predikat:

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

Kode:

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

Jika Anda tidak keberatan menulis ulang sub-ekspresi di muka, Anda dapat menggunakan COALESCE:

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

Anda harus memastikan bahwa 'notSomeValue'itu berbeda dari 'someValue'; lebih disukai, itu akan menjadi nilai yang sepenuhnya ilegal untuk kolom. (Tidak mungkin NULL, tentu saja.) Ini mudah untuk dinegasikan, bahkan jika Anda memiliki daftar panjang:

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

Lebih bersih, lebih sederhana, dan lebih jelas daripada CASEatau IIF, menurut saya. Kelemahan utama adalah memiliki nilai kedua yang Anda tahu tidak sama, tetapi ini hanya masalah jika Anda tidak tahu nilai aktual di depan. Dalam hal ini, Anda dapat melakukan seperti yang disarankan dan digunakan Hanno BinderCOALESCE(myField, CONCAT('not', 'someValue')) = 'someValue' (di mana 'someValue'sebenarnya akan diparameterisasi).

COALESCE didokumentasikan tersedia dari SQL Server 2005 dan seterusnya.

Perlu diketahui bahwa mengacaukan permintaan Anda seperti ini (menggunakan salah satu metode yang direkomendasikan di sini) dapat mempersulit basis data untuk mengoptimalkan kueri Anda. Untuk kumpulan data besar, IS NULLversi ini cenderung lebih mudah dioptimalkan.

jpmc26
sumber
1
COALESCE(myField, CONCAT('not', 'someValue')) = 'someValue'harus berfungsi untuk "sebagian Nilai" dan data apa pun di tabel.
JimmyB
2

Ada operator set KECUALI bawaan yang, secara efektif, menghapus hasil kueri kedua dari kueri pertama.

select * from table
except
select * from table
where <really complex predicates>
Michael Green
sumber
Semoga saja ini meja kecil :-)
Lennart
-4

Apakah COALESCE tersedia?

SELECT * FROM myTable WHERE NOT COALESCE(myField = 'someValue', FALSE)
Malvolio
sumber
4
Ya, COALESCE tersedia, tetapi tidak, ini tidak akan berfungsi: (a) COALESCE tidak akan menerima ekspresi Boolean (tidak akan ISNULL, omong-omong) dan (b) nilai kebenaran FALSE tidak tersedia langsung dalam SQL sebagai sebuah literal. Cobalah, dan Anda akan mendapatkan kesalahan sintaksis.
Heinzi
@Heinzi - Saya sudah mencobanya, itu berhasil, itu sebabnya saya mempostingnya. Mungkin ini tidak bekerja pada T-SQL, tapi tidak masalah untuk Postgres dan MySQL.
Malvolio
2
@Malvolio: Pertanyaan ini ditandai sql-server, meskipun, tidak mysqlatau postgresql.
Andriy M
@Malvolio itu karena Postgres memiliki BOOLEANtipe dan MySQL memiliki tipe (palsu) BOOLEANyang dapat menjadi parameter COALESCE()fungsi. Jika pertanyaan telah ditandai dengan sql-agnosticatau sql-standard, jawabannya akan baik-baik saja.
ypercubeᵀᴹ
@ ypercubeᵀᴹ - eh, apa yang bisa saya katakan? Dapatkan basis data yang lebih baik.
Malvolio