Database
 sql >> Teknologi Basis Data >  >> RDS >> Database

Kompleksitas NULL – Bagian 2

Artikel ini adalah yang kedua dalam seri kompleksitas NULL. Bulan lalu saya memperkenalkan NULL sebagai penanda SQL untuk segala jenis nilai yang hilang. Saya menjelaskan bahwa SQL tidak memberi Anda kemampuan untuk membedakan antara tidak ada dan berlaku (Nilai-A) dan hilang dan tidak dapat diterapkan (I-nilai) penanda. Saya juga menjelaskan bagaimana perbandingan yang melibatkan NULL bekerja dengan konstanta, variabel, parameter, dan kolom. Bulan ini saya melanjutkan diskusi dengan membahas inkonsistensi perlakuan NULL dalam elemen T-SQL yang berbeda.

Saya akan terus menggunakan database sampel TSQLV5 seperti bulan lalu di beberapa contoh saya. Anda dapat menemukan skrip yang membuat dan mengisi database ini di sini, dan diagram ER-nya di sini.

Inkonsistensi perawatan NULL

Seperti yang sudah Anda kumpulkan, perawatan NULL bukanlah hal yang sepele. Beberapa kebingungan dan kerumitan berkaitan dengan fakta bahwa perlakuan terhadap NULL dapat menjadi tidak konsisten antara berbagai elemen T-SQL untuk operasi serupa. Di bagian mendatang saya menjelaskan penanganan NULL dalam perhitungan linier versus agregat, klausa ON/WHERE/HAVING, CHECK constraint versus opsi CHECK, elemen IF/WHILE/CASE, pernyataan MERGE, perbedaan dan pengelompokan, serta pemesanan dan keunikan.

Perhitungan linier versus agregat

T-SQL, dan hal yang sama berlaku untuk SQL standar, menggunakan logika penanganan NULL yang berbeda saat menerapkan fungsi agregat aktual seperti SUM, MIN, dan MAX di seluruh baris versus saat menerapkan komputasi yang sama dengan yang linier di seluruh kolom. Untuk mendemonstrasikan perbedaan ini, saya akan menggunakan dua tabel contoh yang disebut #T1 dan #T2 yang Anda buat dan isi dengan menjalankan kode berikut:

DROP TABLE IF EXISTS #T1, #T2;
 
SELECT * INTO #T1 FROM ( VALUES(10, 5, NULL) ) AS D(col1, col2, col3);
 
SELECT * INTO #T2 FROM ( VALUES(10),(5),(NULL) ) AS D(col1);

Tabel #T1 memiliki tiga kolom yang disebut col1, col2 dan col3. Saat ini memiliki satu baris dengan nilai kolom masing-masing 10, 5 dan NULL:

SELECT * FROM #T1;
col1        col2        col3
----------- ----------- -----------
10          5           NULL

Tabel #T2 memiliki satu kolom yang disebut col1. Saat ini memiliki tiga baris dengan nilai 10, 5 dan NULL di col1:

SELECT * FROM #T2;
col1
-----------
10
5
NULL

Saat menerapkan apa yang pada akhirnya merupakan perhitungan agregat seperti penambahan sebagai satu linier melintasi kolom, kehadiran input NULL menghasilkan hasil NULL. Kueri berikut menunjukkan perilaku ini:

SELECT col1 + col2 + col3 AS total
FROM #T1;

Kueri ini menghasilkan keluaran berikut:

total
-----------
NULL

Sebaliknya, fungsi agregat aktual, yang diterapkan di seluruh baris, dirancang untuk mengabaikan input NULL. Kueri berikut menunjukkan perilaku ini menggunakan fungsi SUM:

SELECT SUM(col1) AS total
FROM #T2;

Kueri ini menghasilkan keluaran berikut:

total
-----------
15

Warning: Null value is eliminated by an aggregate or other SET operation.

Perhatikan peringatan yang diamanatkan oleh standar SQL yang menunjukkan adanya input NULL yang diabaikan. Anda dapat menekan peringatan tersebut dengan mematikan opsi sesi ANSI_WARNINGS.

Demikian pula, ketika diterapkan pada ekspresi input, fungsi COUNT menghitung jumlah baris dengan nilai input non-NULL (berlawanan dengan COUNT(*) yang hanya menghitung jumlah baris). Misalnya, mengganti SUM(col1) dengan COUNT(col1) dalam kueri di atas mengembalikan hitungan 2.

Anehnya, jika Anda menerapkan COUNT agregat ke kolom yang didefinisikan sebagai tidak mengizinkan NULL, pengoptimal mengonversi ekspresi COUNT() menjadi COUNT(*). Hal ini memungkinkan penggunaan indeks apa pun untuk tujuan penghitungan dibandingkan dengan mengharuskan penggunaan indeks yang berisi kolom yang dimaksud. Itu satu lagi alasan selain memastikan konsistensi dan integritas data Anda yang seharusnya mendorong Anda untuk menerapkan batasan seperti NOT NULL dan lainnya. Kendala tersebut memungkinkan pengoptimal lebih fleksibel dalam mempertimbangkan alternatif yang lebih optimal, dan menghindari pekerjaan yang tidak perlu.

Berdasarkan logika ini, fungsi AVG membagi jumlah nilai non-NULL dengan jumlah nilai non-NULL. Pertimbangkan kueri berikut sebagai contoh:

SELECT AVG(1.0 * col1) AS avgall
FROM #T2;

Di sini jumlah nilai col1 non-NULL 15 dibagi dengan jumlah nilai non-NULL 2. Anda mengalikan col1 dengan literal numerik 1.0 untuk memaksa konversi implisit dari nilai input integer ke numerik untuk mendapatkan pembagian numerik dan bukan integer divisi. Kueri ini menghasilkan keluaran berikut:

avgall
---------
7.500000

Demikian pula, agregat MIN dan MAX mengabaikan input NULL. Pertimbangkan kueri berikut:

SELECT MIN(col1) AS mincol1, MAX(col1) AS maxcol1
FROM #T2;

Kueri ini menghasilkan keluaran berikut:

mincol1     maxcol1
----------- -----------
5           10

Mencoba menerapkan perhitungan linier tetapi meniru semantik fungsi agregat (abaikan NULL) tidak cukup. Meniru SUM, COUNT, dan AVG tidak terlalu rumit, tetapi Anda harus memeriksa setiap masukan untuk NULL, seperti:

SELECT col1, col2, col3,
  CASE
    WHEN COALESCE(col1, col2, col3) IS NULL THEN NULL
    ELSE COALESCE(col1, 0) + COALESCE(col2, 0) + COALESCE(col3, 0)
  END AS sumall,
  CASE WHEN col1 IS NOT NULL THEN 1 ELSE 0 END
    + CASE WHEN col2 IS NOT NULL THEN 1 ELSE 0 END
    + CASE WHEN col3 IS NOT NULL THEN 1 ELSE 0 END AS cntall,
  CASE
    WHEN COALESCE(col1, col2, col3) IS NULL THEN NULL
    ELSE 1.0 * (COALESCE(col1, 0) + COALESCE(col2, 0) + COALESCE(col3, 0))
           / (CASE WHEN col1 IS NOT NULL THEN 1 ELSE 0 END
                + CASE WHEN col2 IS NOT NULL THEN 1 ELSE 0 END
                + CASE WHEN col3 IS NOT NULL THEN 1 ELSE 0 END)
  END AS avgall
FROM #T1;

Kueri ini menghasilkan keluaran berikut:

col1        col2        col3        sumall      cntall      avgall
----------- ----------- ----------- ----------- ----------- ---------------
10          5           NULL        15          2           7.500000000000

Mencoba menerapkan minimum atau maksimum sebagai perhitungan linier ke lebih dari dua kolom input cukup rumit bahkan sebelum Anda menambahkan logika untuk mengabaikan NULL karena melibatkan beberapa ekspresi CASE bersarang baik secara langsung maupun tidak langsung (ketika Anda menggunakan kembali alias kolom). Misalnya, berikut adalah kueri yang menghitung maksimum antara col1, col2 dan col3 di #T1, tanpa bagian yang mengabaikan NULL:

SELECT col1, col2, col3, 
  CASE WHEN col1 IS NULL OR col2 IS NULL OR col3 IS NULL THEN NULL ELSE max2 END AS maxall
FROM #T1
  CROSS APPLY (VALUES(CASE WHEN col1 >= col2 THEN col1 ELSE col2 END)) AS A1(max1)
  CROSS APPLY (VALUES(CASE WHEN max1 >= col3 THEN max1 ELSE col3 END)) AS A2(max2);

Kueri ini menghasilkan keluaran berikut:

col1        col2        col3        maxall
----------- ----------- ----------- -----------
10          5           NULL        NULL

Jika Anda memeriksa rencana kueri, Anda akan menemukan ekspresi diperluas berikut yang menghitung hasil akhir:

[Expr1005] = Scalar Operator(CASE WHEN CASE WHEN [#T1].[col1] IS NOT NULL THEN [#T1].[col1] ELSE 
  CASE WHEN [#T1].[col2] IS NOT NULL THEN [#T1].[col2] 
    ELSE [#T1].[col3] END END IS NULL THEN NULL ELSE 
  CASE WHEN CASE WHEN [#T1].[col1]>=[#T1].[col2] THEN [#T1].[col1] 
    ELSE [#T1].[col2] END>=[#T1].[col3] THEN 
  CASE WHEN [#T1].[col1]>=[#T1].[col2] THEN [#T1].[col1] 
    ELSE [#T1].[col2] END ELSE [#T1].[col3] END END)

Dan saat itulah hanya ada tiga kolom yang terlibat. Bayangkan jika ada selusin kolom yang terlibat!

Sekarang tambahkan logika ini untuk mengabaikan NULL:

SELECT col1, col2, col3, max2 AS maxall
FROM #T1
  CROSS APPLY (VALUES(CASE WHEN col1 >= col2 OR col2 IS NULL THEN col1 ELSE col2 END)) AS A1(max1)
  CROSS APPLY (VALUES(CASE WHEN max1 >= col3 OR col3 IS NULL THEN max1 ELSE col3 END)) AS A2(max2);

Kueri ini menghasilkan keluaran berikut:

col1        col2        col3        maxall
----------- ----------- ----------- -----------
10          5           NULL        10

Oracle memiliki sepasang fungsi yang disebut GREATEST dan LEAST yang masing-masing menerapkan perhitungan minimum dan maksimum, sebagai yang linier terhadap nilai input. Fungsi-fungsi ini mengembalikan NULL yang diberikan input NULL seperti kebanyakan perhitungan linier. Ada item umpan balik terbuka yang meminta untuk mendapatkan fungsi serupa di T-SQL, tetapi permintaan ini tidak dialihkan dalam perubahan situs umpan balik terbaru mereka. Jika Microsoft menambahkan fungsi seperti itu ke T-SQL, akan sangat bagus jika ada opsi yang mengontrol apakah akan mengabaikan NULL atau tidak.

Sementara itu, ada teknik yang jauh lebih elegan dibandingkan dengan yang disebutkan di atas yang menghitung semua jenis agregat sebagai yang linier melintasi kolom menggunakan semantik fungsi agregat aktual yang mengabaikan NULL. Anda menggunakan kombinasi operator CROSS APPLY dan kueri tabel turunan terhadap konstruktor nilai tabel yang memutar kolom ke baris dan menerapkan agregat sebagai fungsi agregat aktual. Berikut adalah contoh yang menunjukkan perhitungan MIN dan MAX, tetapi Anda dapat menggunakan teknik ini dengan fungsi agregat apa pun yang Anda suka:

SELECT col1, col2, col3, maxall, minall
FROM #T1 CROSS APPLY
  (SELECT MAX(mycol), MIN(mycol)
   FROM (VALUES(col1),(col2),(col3)) AS D1(mycol)) AS D2(maxall, minall);

Kueri ini menghasilkan keluaran berikut:

col1        col2        col3        maxall      minall
----------- ----------- ----------- ----------- -----------
10          5           NULL        10          5

Bagaimana jika Anda menginginkan sebaliknya? Bagaimana jika Anda perlu menghitung agregat di seluruh baris, tetapi menghasilkan NULL jika ada input NULL? Misalnya, Anda perlu menjumlahkan semua nilai col1 dari #T1, tetapi mengembalikan NULL jika salah satu inputnya NULL. Ini dapat dicapai dengan teknik berikut:

SELECT SUM(col1) * NULLIF(MIN(CASE WHEN col1 IS NULL THEN 0 ELSE 1 END), 0) AS sumall
FROM #T2;

Anda menerapkan agregat MIN ke ekspresi CASE yang mengembalikan nol untuk input NULL dan satu untuk input non-NULL. Jika ada input NULL, hasil dari fungsi MIN adalah 0, jika tidak 1. Kemudian menggunakan fungsi NULLIF, Anda mengubah hasil 0 menjadi NULL. Anda kemudian mengalikan hasil fungsi NULLIF dengan jumlah aslinya. Jika ada input NULL, Anda mengalikan jumlah asli dengan NULL yang menghasilkan NULL. Jika tidak ada input NULL, Anda mengalikan hasil jumlah awal dengan 1, menghasilkan jumlah asli.

Kembali ke perhitungan linier yang menghasilkan NULL untuk input NULL apa pun, logika yang sama berlaku untuk penggabungan string menggunakan operator +, seperti yang ditunjukkan oleh kueri berikut:

USE TSQLV5;
 
SELECT empid, country, region, city,
  country + N',' + region + N',' + city AS emplocation
FROM HR.Employees;

Kueri ini menghasilkan keluaran berikut:

empid       country         region          city            emplocation
----------- --------------- --------------- --------------- ----------------
1           USA             WA              Seattle         USA,WA,Seattle
2           USA             WA              Tacoma          USA,WA,Tacoma
3           USA             WA              Kirkland        USA,WA,Kirkland
4           USA             WA              Redmond         USA,WA,Redmond
5           UK              NULL            London          NULL
6           UK              NULL            London          NULL
7           UK              NULL            London          NULL
8           USA             WA              Seattle         USA,WA,Seattle
9           UK              NULL            London          NULL

Anda ingin menggabungkan bagian lokasi karyawan menjadi satu string, menggunakan koma sebagai pemisah. Tetapi Anda ingin mengabaikan input NULL. Sebaliknya, ketika salah satu input adalah NULL, Anda mendapatkan NULL sebagai hasilnya. Beberapa menonaktifkan opsi sesi CONCAT_NULL_YIELDS_NULL, yang menyebabkan input NULL dikonversi menjadi string kosong untuk tujuan penggabungan, tetapi opsi ini tidak disarankan karena menerapkan perilaku tidak standar. Selain itu, Anda akan dibiarkan dengan beberapa pemisah berurutan ketika ada input NULL, yang biasanya bukan perilaku yang diinginkan. Pilihan lain adalah mengganti input NULL secara eksplisit dengan string kosong menggunakan fungsi ISNULL atau COALESCE, tetapi ini biasanya menghasilkan kode verbose yang panjang. Opsi yang jauh lebih elegan adalah menggunakan fungsi CONCAT_WS, yang diperkenalkan di SQL Server 2017. Fungsi ini menggabungkan input, mengabaikan NULL, menggunakan pemisah yang disediakan sebagai input pertama. Berikut kueri solusi menggunakan fungsi ini:

SELECT empid, country, region, city,
  CONCAT_WS(N',', country, region, city) AS emplocation
FROM HR.Employees;

Kueri ini menghasilkan keluaran berikut:

empid       country         region          city            emplocation
----------- --------------- --------------- --------------- ----------------
1           USA             WA              Seattle         USA,WA,Seattle
2           USA             WA              Tacoma          USA,WA,Tacoma
3           USA             WA              Kirkland        USA,WA,Kirkland
4           USA             WA              Redmond         USA,WA,Redmond
5           UK              NULL            London          UK,London
6           UK              NULL            London          UK,London
7           UK              NULL            London          UK,London
8           USA             WA              Seattle         USA,WA,Seattle
9           UK              NULL            London          UK,London

DI/DI MANA/TELAH

Saat menggunakan klausa kueri WHERE, HAVING, dan ON untuk tujuan pemfilteran/pencocokan, penting untuk diingat bahwa klausa tersebut menggunakan logika predikat tiga nilai. Ketika Anda memiliki logika tiga nilai yang terlibat, Anda ingin mengidentifikasi secara akurat bagaimana klausa menangani kasus TRUE, FALSE, dan UNKNOWN. Ketiga klausa ini dirancang untuk menerima kasus TRUE, dan menolak kasus FALSE dan UNKNOWN.

Untuk mendemonstrasikan perilaku ini, saya akan menggunakan tabel bernama Kontak yang Anda buat dan isi dengan menjalankan kode berikut:.

DROP TABLE IF EXISTS dbo.Contacts;
GO
 
CREATE TABLE dbo.Contacts
(
  id INT NOT NULL 
    CONSTRAINT PK_Contacts PRIMARY KEY,
  name VARCHAR(10) NOT NULL,
  hourlyrate NUMERIC(12, 2) NULL
    CONSTRAINT CHK_Contacts_hourlyrate CHECK(hourlyrate > 0.00)
);
 
INSERT INTO dbo.Contacts(id, name, hourlyrate) VALUES
  (1, 'A', 100.00),(2, 'B', 200.00),(3, 'C', NULL);

Perhatikan bahwa kontak 1 dan 2 memiliki tarif per jam yang berlaku, dan kontak 3 tidak, jadi tarif per jamnya diatur ke NULL. Pertimbangkan kueri berikut untuk mencari kontak dengan tarif per jam positif:

SELECT id, name, hourlyrate
FROM dbo.Contacts
WHERE hourlyrate > 0.00;

Predikat ini mengevaluasi ke TRUE untuk kontak 1 dan 2, dan UNKNOWN untuk kontak 3, maka output hanya berisi kontak 1 dan 2:

id          name       hourlyrate
----------- ---------- -----------
1           A          100.00
2           B          200.00

Pemikiran di sini adalah bahwa ketika Anda yakin bahwa predikat itu benar, Anda ingin mengembalikan baris, jika tidak, Anda ingin membuangnya. Ini mungkin tampak sepele pada awalnya, sampai Anda menyadari bahwa beberapa elemen bahasa yang juga menggunakan predikat bekerja secara berbeda.

PERIKSA batasan versus opsi PERIKSA

Batasan CHECK adalah alat yang Anda gunakan untuk menegakkan integritas dalam tabel berdasarkan predikat. Predikat dievaluasi saat Anda mencoba menyisipkan atau memperbarui baris dalam tabel. Tidak seperti pemfilteran kueri dan klausa pencocokan yang menerima kasus TRUE dan menolak kasus FALSE dan UNKNOWN, batasan CHECK dirancang untuk menerima kasus TRUE dan UNKNOWN dan menolak kasus FALSE. Pemikiran di sini adalah ketika Anda yakin bahwa predikat itu salah, Anda ingin menolak upaya perubahan, jika tidak, Anda ingin mengizinkannya.

Jika Anda memeriksa definisi tabel Kontak kami, Anda akan melihat bahwa tabel tersebut memiliki batasan PERIKSA berikut, menolak kontak dengan tarif per jam yang tidak positif:

CONSTRAINT CHK_Contacts_hourlyrate CHECK(hourlyrate > 0.00)

Perhatikan bahwa batasan menggunakan predikat yang sama seperti yang Anda gunakan di filter kueri sebelumnya.

Coba tambahkan kontak dengan tarif per jam yang positif:

INSERT INTO dbo.Contacts(id, name, hourlyrate) VALUES (4, 'D', 150.00);

Upaya ini berhasil.

Coba tambahkan kontak dengan tarif NULL per jam:

INSERT INTO dbo.Contacts(id, name, hourlyrate) VALUES (5, 'E', NULL);

Upaya ini juga berhasil, karena batasan CHECK dirancang untuk menerima kasus TRUE dan UNKNOWN. Itulah kasus ketika filter kueri dan batasan CHECK dirancang untuk bekerja secara berbeda.

Coba tambahkan kontak dengan tarif per jam yang tidak positif:

INSERT INTO dbo.Contacts(id, name, hourlyrate) VALUES (6, 'F', -100.00);

Upaya ini gagal dengan kesalahan berikut:

Msg 547, Level 16, State 0, Line 454
Pernyataan INSERT bertentangan dengan batasan CHECK "CHK_Contacts_hourlyrate". Konflik terjadi pada database "TSQLV5", tabel "dbo.Contacts", kolom 'hourlyrate'.

T-SQL juga memungkinkan Anda untuk menerapkan integritas modifikasi melalui tampilan menggunakan opsi CHECK. Beberapa orang menganggap opsi ini memiliki tujuan yang mirip dengan batasan CHECK selama Anda menerapkan modifikasi melalui tampilan. Misalnya, perhatikan tampilan berikut, yang menggunakan filter berdasarkan predikat tarif per jam> 0,00 dan ditentukan dengan opsi PERIKSA:

CREATE OR ALTER VIEW dbo.MyContacts
AS
SELECT id, name, hourlyrate
FROM dbo.Contacts
WHERE hourlyrate > 0.00
WITH CHECK OPTION;

Ternyata, tidak seperti batasan CHECK, opsi tampilan CHECK dirancang untuk menerima kasus TRUE dan menolak kasus FALSE dan UNKNOWN. Jadi sebenarnya ini dirancang untuk berperilaku lebih seperti filter kueri biasanya juga untuk tujuan menegakkan integritas.

Coba masukkan baris dengan tarif per jam positif melalui tampilan:

INSERT INTO dbo.MyContacts(id, name, hourlyrate) VALUES (7, 'G', 300.00);

Upaya ini berhasil.

Coba masukkan baris dengan tarif NULL per jam melalui tampilan:

INSERT INTO dbo.MyContacts(id, name, hourlyrate) VALUES (8, 'H', NULL);

Upaya ini gagal dengan kesalahan berikut:

Msg 550, Level 16, State 1, Line 473
Upaya penyisipan atau pembaruan gagal karena tampilan target menentukan WITH CHECK OPTION atau mencakup tampilan yang menetapkan WITH CHECK OPTION dan satu atau lebih baris yang dihasilkan dari operasi tidak memenuhi syarat di bawah batasan OPSI PERIKSA.

Pemikiran di sini adalah bahwa setelah Anda menambahkan opsi CHECK ke tampilan, Anda hanya ingin mengizinkan modifikasi yang menghasilkan baris yang dikembalikan oleh tampilan. Itu sedikit berbeda dari pemikiran dengan batasan CHECK—tolak perubahan yang Anda yakin predikatnya salah. Ini bisa sedikit membingungkan. Jika Anda ingin tampilan mengizinkan modifikasi yang menyetel tarif per jam ke NULL, Anda memerlukan filter kueri untuk mengizinkannya juga dengan menambahkan OR tarif per jam IS NULL. Anda hanya perlu menyadari bahwa batasan CHECK dan opsi CHECK dirancang untuk bekerja secara berbeda sehubungan dengan kasus UNKNOWN. Yang pertama menerimanya sedangkan yang kedua menolaknya.

Buat kueri tabel Kontak setelah semua perubahan di atas:

SELECT id, name, hourlyrate
FROM dbo.Contacts;

Anda harus mendapatkan output berikut pada saat ini:

id          name       hourlyrate
----------- ---------- -----------
1           A          100.00
2           B          200.00
3           C          NULL
4           D          150.00
5           E          NULL
7           G          300.00

JIKA/SELAMA/KASUS

Elemen bahasa IF, WHILE dan CASE bekerja dengan predikat.

Pernyataan IF dirancang sebagai berikut:

IF <predicate>
  <statement or BEGIN-END block when TRUE>
ELSE
  <statement or BEGIN-END block when FALSE or UNKNOWN>

Ini intuitif untuk mengharapkan memiliki blok TRUE mengikuti klausa IF dan blok FALSE mengikuti klausa ELSE, tetapi Anda perlu menyadari bahwa klausa ELSE benar-benar diaktifkan ketika predikatnya FALSE atau UNKNOWN. Secara teoritis, bahasa logika tiga nilai bisa memiliki pernyataan IF dengan pemisahan tiga kasus. Sesuatu seperti ini:

IF <predicate>
  WHEN TRUE
    <statement or BEGIN-END block when TRUE>
  WHEN FALSE
    <statement or BEGIN-END block when FALSE>
  WHEN UNKNOWN
    <statement or BEGIN-END block when UNKNOWN>

Dan bahkan mengizinkan kombinasi hasil logis sehingga jika Anda ingin menggabungkan FALSE dan UNKNOWN menjadi satu bagian, Anda dapat menggunakan sesuatu seperti ini:

IF <predicate>
  WHEN TRUE
    <statement or BEGIN-END block when TRUE>
  WHEN FALSE OR UNKNOWN
    <statement or BEGIN-END block when FALSE OR UNKNOWN>

Sementara itu, Anda dapat meniru konstruksi seperti itu dengan membuat pernyataan IF-ELSE, dan secara eksplisit mencari NULL dalam operan dengan operator IS NULL.

Pernyataan WHILE hanya memiliki blok TRUE. Ini dirancang sebagai berikut:

WHILE <predicate>
  <statement or BEGIN-END block when TRUE>

Pernyataan atau blok BEGIN-END yang membentuk badan loop diaktifkan saat predikatnya adalah TURE. Segera setelah predikatnya FALSE atau UNKNOWN, kontrol diteruskan ke pernyataan mengikuti loop WHILE.

Tidak seperti IF dan WHILE, yang merupakan pernyataan yang mengeksekusi kode, CASE adalah ekspresi yang mengembalikan nilai. Sintaks dari dicari Ekspresi CASE adalah sebagai berikut:

CASE
  WHEN <predicate 1> THEN <expression 1 when TRUE>
  WHEN <predicate 2> THEN <expression 2 when TRUE >
  ...
  WHEN <predicate n> THEN <expression n when TRUE >
  ELSE <else expression when all are FALSE or UNKNOWN>
END

Ekspresi CASE dirancang untuk mengembalikan ekspresi yang mengikuti klausa THEN yang sesuai dengan predikat WHEN pertama yang bernilai TRUE. Jika ada klausa ELSE, itu diaktifkan jika tidak ada predikat WHEN yang TRUE (semuanya FALSE atau UNKNOWN). Tidak adanya klausa ELSE eksplisit, ELSE NULL implisit digunakan. Jika Anda ingin menangani kasus UNKNOWN secara terpisah, Anda dapat secara eksplisit mencari NULL di operand predikat menggunakan operator IS NULL.

sederhana Ekspresi CASE menggunakan perbandingan implisit berbasis kesetaraan antara ekspresi sumber dan ekspresi yang dibandingkan:

CASE <source expression>
  WHEN <comp expression 1> THEN <result expression 1 when TRUE>
  WHEN <comp expression 2> THEN <result expression 2 when TRUE >
  ...
  WHEN <comp expression n> THEN <result expression n when TRUE >
  ELSE <else result expression when all are FALSE or UNKNOWN>
END

Ekspresi CASE sederhana dirancang mirip dengan ekspresi CASE yang dicari dalam hal penanganan logika tiga nilai, tetapi karena perbandingan menggunakan perbandingan berbasis kesetaraan implisit, Anda tidak dapat menangani kasus UNKNOWN secara terpisah. Upaya untuk menggunakan NULL di salah satu ekspresi yang dibandingkan dalam klausa WHEN tidak ada artinya karena perbandingan tidak akan menghasilkan TRUE bahkan ketika ekspresi sumbernya NULL. Perhatikan contoh berikut:

DECLARE @input AS INT = NULL;
 
SELECT CASE @input WHEN NULL THEN 'Input is NULL' ELSE 'Input is not NULL' END;

Ini akan dikonversi secara implisit sebagai berikut:

DECLARE @input AS INT = NULL;
 
SELECT CASE WHEN @input = NULL THEN 'Input is NULL' ELSE 'Input is not NULL' END;

Akibatnya, hasilnya adalah:

Masukan bukan NULL

Untuk mendeteksi input NULL, Anda perlu menggunakan sintaks ekspresi CASE yang dicari dan operator IS NULL, seperti:

DECLARE @input AS INT = NULL;
 
SELECT CASE WHEN @input IS NULL THEN 'Input is NULL' ELSE 'Input is not NULL' END;

Kali ini hasilnya adalah:

Masukan adalah NULL

GABUNG

Pernyataan MERGE digunakan untuk menggabungkan data dari sumber ke target. Anda menggunakan predikat gabungan untuk mengidentifikasi kasus berikut dan menerapkan tindakan terhadap target:

  • Baris sumber dicocokkan dengan baris target (diaktifkan saat ditemukan kecocokan untuk baris sumber yang predikat gabungannya BENAR):terapkan UPDATE atau DELETE terhadap target
  • Baris sumber tidak cocok dengan baris target (diaktifkan bila tidak ada kecocokan yang ditemukan untuk baris sumber yang predikat gabungannya BENAR, melainkan untuk semua predikatnya SALAH atau TIDAK DIKETAHUI):terapkan INSERT terhadap target
  • Baris target tidak cocok dengan baris sumber (diaktifkan bila tidak ada kecocokan yang ditemukan untuk baris target yang predikat gabungannya BENAR, melainkan untuk semua predikatnya FALSE atau UNKNOWN):terapkan UPDATE atau DELETE terhadap target

Ketiga skenario memisahkan TRUE ke satu grup dan FALSE atau UNKNOWN ke grup lain. Anda tidak mendapatkan bagian terpisah untuk menangani TRUE, menangani FALSE, dan menangani kasus UNKNOWN.

Untuk mendemonstrasikan ini, saya akan menggunakan tabel bernama T3 yang Anda buat dan isi dengan menjalankan kode berikut:

DROP TABLE IF EXISTS dbo.T3;
GO
 
CREATE TABLE dbo.T3(col1 INT NULL, col2 INT NULL, CONSTRAINT UNQ_T3 UNIQUE(col1));
 
INSERT INTO dbo.T3(col1) VALUES(1),(2),(NULL);

Perhatikan pernyataan MERGE berikut:

MERGE INTO dbo.T3 AS TGT
USING (VALUES(1, 100), (3, 300)) AS SRC(col1, col2)
  ON SRC.col1 = TGT.col1
WHEN MATCHED THEN UPDATE
  SET TGT.col2 = SRC.col2
WHEN NOT MATCHED THEN INSERT(col1, col2) VALUES(SRC.col1, SRC.col2)
WHEN NOT MATCHED BY SOURCE THEN UPDATE
  SET col2 = -1;
 
SELECT col1, col2 FROM dbo.T3;

Baris sumber di mana col1 adalah 1 dicocokkan dengan baris target di mana col1 adalah 1 (predikatnya BENAR) dan oleh karena itu col2 baris target disetel ke 100.

Baris sumber di mana col1 adalah 3 tidak cocok dengan baris target mana pun (untuk semua predikatnya FALSE atau UNKNOWN) dan oleh karena itu baris baru dimasukkan ke T3 dengan 3 sebagai nilai col1 dan 300 sebagai nilai col2.

Baris target di mana col1 adalah 2 dan di mana col1 adalah NULL tidak cocok dengan baris sumber mana pun (untuk semua baris, predikatnya adalah FALSE atau UNKNOWN) dan oleh karena itu dalam kedua kasus col2 di baris target disetel ke -1.

Kueri terhadap T3 mengembalikan output berikut setelah mengeksekusi pernyataan MERGE di atas:

col1        col2
----------- -----------
1           100
2           -1
NULL        -1
3           300

Jauhkan meja T3 sekitar; itu digunakan nanti.

Kebedaan dan pengelompokan

Tidak seperti perbandingan yang dilakukan dengan menggunakan operator persamaan dan ketidaksamaan, perbandingan yang dilakukan untuk perbedaan dan tujuan pengelompokan mengelompokkan NULL bersama-sama. Satu NULL dianggap tidak berbeda dari NULL lain, tetapi NULL dianggap berbeda dari nilai non-NULL. Akibatnya, menerapkan klausa DISTINCT menghapus kemunculan duplikat NULL. Kueri berikut menunjukkan hal ini:

SELECT DISTINCT country, region FROM HR.Employees;

Kueri ini menghasilkan keluaran berikut:

country         region
--------------- ---------------
UK              NULL
USA             WA

Ada beberapa karyawan dengan negara AS dan wilayah NULL, dan setelah penghapusan duplikat, hasilnya hanya menunjukkan satu kemunculan kombinasi.

Seperti perbedaan, pengelompokan juga mengelompokkan NULL bersama-sama, seperti yang ditunjukkan oleh kueri berikut:

SELECT country, region, COUNT(*) AS numemps
FROM HR.Employees
GROUP BY country, region;

Kueri ini menghasilkan keluaran berikut:

country         region          numemps
--------------- --------------- -----------
UK              NULL            4
USA             WA              5

Sekali lagi, keempat karyawan dengan negara Inggris dan wilayah NULL dikelompokkan bersama.

Memesan

Pemesanan memperlakukan beberapa NULL sebagai memiliki nilai pemesanan yang sama. Standar SQL menyerahkannya kepada implementasi untuk memilih apakah akan memesan NULL terlebih dahulu atau terakhir dibandingkan dengan nilai non-NULL. Microsoft memilih untuk mempertimbangkan NULL sebagai memiliki nilai pemesanan yang lebih rendah dibandingkan dengan non-NULL di SQL Server, jadi saat menggunakan arah urutan menaik, T-SQL memesan NULL terlebih dahulu. Kueri berikut menunjukkan hal ini:

SELECT id, name, hourlyrate
FROM dbo.Contacts
ORDER BY hourlyrate;

Kueri ini menghasilkan keluaran berikut:

id          name       hourlyrate
----------- ---------- -----------
3           C          NULL
5           E          NULL
1           A          100.00
4           D          150.00
2           B          200.00
7           G          300.00

Bulan depan saya akan menambahkan lebih banyak tentang topik ini, membahas elemen standar yang memberi Anda kendali atas perilaku pengurutan NULL dan solusi untuk elemen tersebut di T-SQL.

Keunikan

Saat menerapkan keunikan pada kolom NULLable menggunakan batasan UNIK atau indeks unik, T-SQL memperlakukan NULL seperti nilai non-NULL. Itu menolak NULL duplikat seolah-olah satu NULL tidak unik dari NULL lainnya.

Ingatlah bahwa tabel T3 kita memiliki batasan UNIK yang ditentukan pada col1. Berikut definisinya:

CONSTRAINT UNQ_T3 UNIQUE(col1)

Kueri T3 untuk melihat isinya saat ini:

SELECT * FROM dbo.T3;

Jika Anda menjalankan semua modifikasi terhadap T3 dari contoh sebelumnya di artikel ini, Anda akan mendapatkan output berikut:

col1        col2
----------- -----------
1           100
2           -1
NULL        -1
3           300

Coba tambahkan baris kedua dengan NULL di col1:

INSERT INTO dbo.T3(col1, col2) VALUES(NULL, 400);

Anda mendapatkan kesalahan berikut:

Msg 2627, Level 14, State 1, Line 558
Pelanggaran batasan UNIQUE KEY 'UNQ_T3'. Tidak dapat menyisipkan kunci duplikat di objek 'dbo.T3'. Nilai kunci duplikat adalah ().

Perilaku ini sebenarnya tidak standar. Bulan depan saya akan menjelaskan spesifikasi standar, dan bagaimana menirunya di T-SQL.

Kesimpulan

Di bagian kedua dari seri tentang kompleksitas NULL ini, saya berfokus pada inkonsistensi perlakuan NULL di antara elemen T-SQL yang berbeda. Saya membahas perhitungan linier versus agregat, klausa penyaringan dan pencocokan, batasan CHECK versus opsi CHECK, elemen JIKA, WHILE dan CASE, pernyataan MERGE, perbedaan dan pengelompokan, pemesanan, dan keunikan. Ketidakkonsistenan yang saya bahas lebih lanjut menekankan betapa pentingnya memahami dengan benar perlakuan NULL di platform yang Anda gunakan, untuk memastikan bahwa Anda menulis kode yang benar dan kuat. Bulan depan saya akan melanjutkan seri dengan membahas opsi perawatan NULL standar SQL yang tidak tersedia di T-SQL, dan memberikan solusi yang didukung di T-SQL.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Bagaimana cara menghapus batasan unik dalam SQL?

  2. Bagaimana Membandingkan Dua Baris dari Tabel yang Sama

  3. Notasi Barker

  4. Apa? Memahami Indeks Hash

  5. Cara Membuat Dokumen Excel dari Program Java Menggunakan Apache POI