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

Bug T-SQL, perangkap, dan praktik terbaik – subkueri

Artikel ini adalah yang kedua dari seri tentang bug T-SQL, perangkap dan praktik terbaik. Kali ini saya fokus pada bug klasik yang melibatkan subquery. Khususnya, saya membahas kesalahan substitusi dan masalah logika tiga nilai. Beberapa topik yang saya bahas dalam seri ini disarankan oleh sesama MVP dalam diskusi yang kami lakukan tentang subjek tersebut. Terima kasih kepada Erland Sommarskog, Aaron Bertrand, Alejandro Mesa, Umachandar Jayachandran (UC), Fabiano Neves Amorim, Milos Radivojevic, Simon Sabin, Adam Machanic, Thomas Grohser, Chan Ming Man, dan Paul White atas saran Anda!

Kesalahan substitusi

Untuk mendemonstrasikan kesalahan substitusi klasik, saya akan menggunakan skenario pesanan pelanggan sederhana. Jalankan kode berikut untuk membuat fungsi pembantu yang disebut GetNums, dan untuk membuat dan mengisi tabel Pelanggan dan Pesanan:

SET NOCOUNT ON;
 
USE tempdb;
GO
 
DROP TABLE IF EXISTS dbo.Orders;
DROP TABLE IF EXISTS dbo.Customers;
DROP FUNCTION IF EXISTS dbo.GetNums;
GO
 
CREATE FUNCTION dbo.GetNums(@low AS BIGINT, @high AS BIGINT) RETURNS TABLE
AS
RETURN
  WITH
    L0   AS (SELECT c FROM (SELECT 1 UNION ALL SELECT 1) AS D(c)),
    L1   AS (SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B),
    L2   AS (SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B),
    L3   AS (SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B),
    L4   AS (SELECT 1 AS c FROM L3 AS A CROSS JOIN L3 AS B),
    L5   AS (SELECT 1 AS c FROM L4 AS A CROSS JOIN L4 AS B),
    Nums AS (SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum
             FROM L5)
  SELECT TOP(@high - @low + 1) @low + rownum - 1 AS n
  FROM Nums
  ORDER BY rownum;
GO
 
CREATE TABLE dbo.Customers
(
  custid INT NOT NULL
    CONSTRAINT PK_Customers PRIMARY KEY,
  companyname VARCHAR(50) NOT NULL
);
 
INSERT INTO dbo.Customers WITH (TABLOCK) (custid, companyname)
  SELECT n AS custid, CONCAT('Cust ', CAST(n AS VARCHAR(10))) AS companyname
  FROM dbo.GetNums(1, 100);
 
CREATE TABLE dbo.Orders
(
  orderid INT NOT NULL IDENTITY
    CONSTRAINT PK_Orders PRIMARY KEY,
  customerid INT NOT NULL,
  filler BINARY(100) NOT NULL -- representing other columns
    CONSTRAINT DFT_Orders_filler DEFAULT(0x)
);
 
INSERT INTO dbo.Orders WITH (TABLOCK) (customerid)
  SELECT
    C.n AS customerid
  FROM dbo.GetNums(1, 10000) AS O
    CROSS JOIN dbo.GetNums(1, 100) AS C
  WHERE C.n NOT IN(17, 59);
 
CREATE INDEX idx_customerid ON dbo.Orders(customerid);

Saat ini, tabel Pelanggan memiliki 100 pelanggan dengan ID pelanggan berurutan dalam kisaran 1 hingga 100. 98 dari pelanggan tersebut memiliki pesanan yang sesuai di tabel Pesanan. Pelanggan dengan ID 17 dan 59 belum melakukan pemesanan apa pun dan oleh karena itu tidak ada di tabel Pesanan.

Anda hanya mencari pelanggan yang melakukan pemesanan, dan Anda mencoba mencapainya menggunakan kueri berikut (sebut saja Kueri 1):

SET NOCOUNT OFF;
 
SELECT custid, companyname
FROM dbo.Customers
WHERE custid IN (SELECT custid FROM dbo.Orders);

Anda seharusnya mendapatkan kembali 98 pelanggan, tetapi Anda malah mendapatkan 100 pelanggan, termasuk yang memiliki ID 17 dan 59:

custid  companyname
------- ------------
1       Cust 1
2       Cust 2
3       Cust 3
...
16      Cust 16
17      Cust 17
18      Cust 18
...
58      Cust 58
59      Cust 59
60      Cust 60
...
98      Cust 98
99      Cust 99
100     Cust 100

(100 rows affected)

Bisakah Anda mencari tahu apa yang salah?

Untuk menambah kebingungan, periksa rencana untuk Query 1 seperti yang ditunjukkan pada Gambar 1.

Gambar 1:Rencana untuk Kueri 1

Plan menunjukkan operator Nested Loops (Left Semi Join) tanpa predikat join, artinya satu-satunya syarat untuk mengembalikan pelanggan adalah memiliki tabel Pesanan yang tidak kosong, seolah-olah kueri yang Anda tulis adalah sebagai berikut:

SELECT custid, companyname
FROM dbo.Customers
WHERE EXISTS (SELECT * FROM dbo.Orders);

Anda mungkin mengharapkan rencana yang serupa dengan yang ditunjukkan pada Gambar 2.

Gambar 2:Rencana yang diharapkan untuk Kueri 1

Dalam paket ini Anda melihat operator Nested Loops (Left Semi Join), dengan pemindaian indeks berkerumun pada Pelanggan sebagai input luar dan pencarian di indeks pada kolom id pelanggan di Pesanan sebagai input dalam. Anda juga melihat referensi luar (parameter berkorelasi) berdasarkan kolom custid di Customers, dan mencari predikat Orders.customerid =Customers.custid.

Jadi mengapa Anda mendapatkan rencana di Gambar 1 dan bukan yang ada di Gambar 2? Jika Anda belum mengetahuinya, perhatikan baik-baik definisi kedua tabel—khususnya nama kolom—dan pada nama kolom yang digunakan dalam kueri. Anda akan melihat bahwa tabel Pelanggan menyimpan ID pelanggan di kolom yang disebut custid, dan tabel Pesanan menyimpan ID pelanggan di kolom yang disebut id pelanggan. Namun, kode menggunakan custid di kueri luar dan dalam. Karena referensi ke custid dalam kueri dalam tidak memenuhi syarat, SQL Server harus menyelesaikan dari tabel mana kolom itu berasal. Menurut standar SQL, SQL Server seharusnya mencari kolom dalam tabel yang ditanyakan dalam lingkup yang sama terlebih dahulu, tetapi karena tidak ada kolom yang disebut custid di Pesanan, maka seharusnya mencarinya di tabel di luar ruang lingkup, dan kali ini ada kecocokan. Jadi secara tidak sengaja, referensi ke custid secara implisit menjadi referensi yang berkorelasi, seolah-olah Anda menulis kueri berikut:

SELECT custid, companyname
FROM dbo.Customers
WHERE custid IN (SELECT Customers.custid FROM dbo.Orders);

Asalkan Pesanan tidak kosong, dan nilai custid luar bukan NULL (tidak dalam kasus kami karena kolom didefinisikan sebagai NOT NULL), Anda akan selalu mendapatkan kecocokan karena Anda membandingkan nilainya dengan nilai itu sendiri . Jadi Query 1 menjadi setara dengan:

SELECT custid, companyname
FROM dbo.Customers
WHERE EXISTS (SELECT * FROM dbo.Orders);

Jika tabel luar mendukung NULL di kolom custid, Kueri 1 akan setara dengan:

SELECT custid, companyname
FROM dbo.Customers
WHERE EXISTS (SELECT * FROM dbo.Orders)
  AND custid IS NOT NULL;

Sekarang Anda memahami mengapa Kueri 1 dioptimalkan dengan paket di Gambar 1, dan mengapa Anda mendapatkan kembali 100 pelanggan.

Beberapa waktu lalu saya mengunjungi pelanggan yang memiliki bug serupa, tetapi sayangnya dengan pernyataan DELETE. Pikirkan sejenak apa artinya ini. Semua baris tabel terhapus dan bukan hanya baris yang awalnya ingin mereka hapus!

Adapun praktik terbaik yang dapat membantu Anda menghindari bug seperti itu, ada dua yang utama. Pertama, sebanyak yang Anda bisa mengontrolnya, pastikan Anda menggunakan nama kolom yang konsisten di seluruh tabel untuk atribut yang mewakili hal yang sama. Kedua, pastikan tabel Anda memenuhi syarat referensi kolom di subkueri, termasuk di subkueri mandiri di mana ini bukan praktik umum. Tentu saja, Anda dapat menggunakan alias tabel jika Anda tidak ingin menggunakan nama tabel lengkap. Menerapkan praktik ini ke kueri kami, misalkan upaya awal Anda menggunakan kode berikut:

SELECT custid, companyname
FROM dbo.Customers
WHERE custid IN (SELECT O.custid FROM dbo.Orders AS O);

Di sini Anda tidak mengizinkan resolusi nama kolom implisit dan oleh karena itu SQL Server menghasilkan kesalahan berikut:

Msg 207, Level 16, State 1, Line 108
Invalid column name 'custid'.

Anda pergi dan memeriksa metadata untuk tabel Pesanan, menyadari bahwa Anda menggunakan nama kolom yang salah, dan memperbaiki kueri (sebut ini Kueri 2), seperti:

SELECT custid, companyname
FROM dbo.Customers
WHERE custid IN (SELECT O.customerid FROM dbo.Orders AS O);

Kali ini Anda mendapatkan hasil yang tepat dengan 98 pelanggan, tidak termasuk pelanggan dengan ID 17 dan 59:

custid  companyname
------- ------------
1       Cust 1
2       Cust 2
3       Cust 3
...
16      Cust 16
18      Cust 18
..
58      Cust 58
60      Cust 60
...
98      Cust 98
99      Cust 99
100     Cust 100

(98 rows affected)

Anda juga mendapatkan rencana yang diharapkan yang ditunjukkan sebelumnya pada Gambar 2.

Selain itu, jelas mengapa Customers.custid adalah referensi luar (parameter berkorelasi) di operator Nested Loops (Left Semi Join) pada Gambar 2. Yang kurang jelas adalah mengapa Expr1004 muncul di rencana sebagai referensi luar juga. Rekan MVP SQL Server Paul White berteori bahwa itu dapat dikaitkan dengan penggunaan informasi dari daun input luar untuk mengisyaratkan mesin penyimpanan untuk menghindari upaya duplikat oleh mekanisme baca-depan. Anda dapat menemukan detailnya di sini.

Masalah logika tiga nilai

Bug umum yang melibatkan subquery berkaitan dengan kasus di mana kueri luar menggunakan predikat NOT IN dan subquery berpotensi mengembalikan NULL di antara nilainya. Misalnya, Anda harus dapat menyimpan pesanan di tabel Pesanan kami dengan NULL sebagai ID pelanggan. Kasus seperti itu akan mewakili pesanan yang tidak terkait dengan pelanggan mana pun; misalnya, pesanan yang mengkompensasi ketidakkonsistenan antara jumlah produk yang sebenarnya dan jumlah yang dicatat dalam database.

Gunakan kode berikut untuk membuat ulang tabel Pesanan dengan kolom custid yang mengizinkan NULL, dan untuk saat ini isi dengan data sampel yang sama seperti sebelumnya (dengan pesanan berdasarkan ID pelanggan 1 hingga 100, tidak termasuk 17 dan 59):

DROP TABLE IF EXISTS dbo.Orders;
GO
 
CREATE TABLE dbo.Orders
(
  orderid INT NOT NULL IDENTITY
    CONSTRAINT PK_Orders PRIMARY KEY,
  custid INT NULL,
  filler BINARY(100) NOT NULL -- representing other columns
    CONSTRAINT DFT_Orders_filler DEFAULT(0x)
);
 
INSERT INTO dbo.Orders WITH (TABLOCK) (custid)
  SELECT
    C.n AS customerid
  FROM dbo.GetNums(1, 10000) AS O
    CROSS JOIN dbo.GetNums(1, 100) AS C
  WHERE C.n NOT IN(17, 59);
 
CREATE INDEX idx_custid ON dbo.Orders(custid);

Perhatikan bahwa saat kita melakukannya, saya mengikuti praktik terbaik yang dibahas di bagian sebelumnya untuk menggunakan nama kolom yang konsisten di seluruh tabel untuk atribut yang sama, dan menamai kolom di tabel Pesanan seperti di tabel Pelanggan.

Misalkan Anda perlu menulis kueri yang mengembalikan pelanggan yang tidak melakukan pemesanan. Anda menemukan solusi sederhana berikut menggunakan predikat NOT IN (sebut saja Query 3, eksekusi pertama):

SELECT custid, companyname
FROM dbo.Customers
WHERE custid NOT IN (SELECT O.custid FROM dbo.Orders AS O);

Kueri ini mengembalikan hasil yang diharapkan dengan pelanggan 17 dan 59:

custid  companyname
------- ------------
17      Cust 17
59      Cust 59

(2 rows affected)

Inventaris dilakukan di gudang perusahaan, dan ditemukan ketidakkonsistenan antara kuantitas aktual beberapa produk dan kuantitas yang tercatat dalam database. Jadi, Anda menambahkan perintah kompensasi dummy untuk memperhitungkan inkonsistensi. Karena tidak ada pelanggan aktual yang terkait dengan pesanan, Anda menggunakan NULL sebagai ID pelanggan. Jalankan kode berikut untuk menambahkan header pesanan seperti itu:

INSERT INTO dbo.Orders(custid) VALUES(NULL);

Jalankan Query 3 untuk kedua kalinya:

SELECT custid, companyname
FROM dbo.Customers
WHERE custid NOT IN (SELECT O.custid FROM dbo.Orders AS O);

Kali ini, Anda mendapatkan hasil kosong:

custid  companyname
------- ------------

(0 rows affected)

Jelas, ada yang salah. Anda tahu bahwa pelanggan 17 dan 59 tidak melakukan pemesanan apa pun, dan memang mereka muncul di tabel Pelanggan tetapi tidak di tabel Pesanan. Namun hasil query mengklaim bahwa tidak ada pelanggan yang tidak melakukan pemesanan. Bisakah Anda mencari tahu di mana bug dan cara memperbaikinya?

Bug ada hubungannya dengan NULL di tabel Pesanan, tentu saja. Untuk SQL, NULL adalah penanda untuk nilai yang hilang yang dapat mewakili pelanggan yang berlaku. SQL tidak tahu bahwa bagi kami NULL mewakili pelanggan yang hilang dan tidak dapat diterapkan (tidak relevan). Untuk semua pelanggan di tabel Pelanggan yang ada di tabel Pesanan, predikat IN menemukan kecocokan yang menghasilkan TRUE dan bagian NOT IN menjadikannya FALSE, oleh karena itu baris pelanggan dibuang. Sejauh ini baik. Tetapi untuk pelanggan 17 dan 59, predikat IN menghasilkan UNKNOWN karena semua perbandingan dengan nilai non-NULL menghasilkan FALSE, dan perbandingan dengan NULL menghasilkan UNKNOWN. Ingat, SQL mengasumsikan bahwa NULL dapat mewakili pelanggan mana pun yang berlaku, sehingga nilai logika UNKNOWN menunjukkan bahwa tidak diketahui apakah ID pelanggan luar sama dengan ID pelanggan NULL dalam. SALAH ATAU SALAH … ATAU TIDAK DIKETAHUI adalah TIDAK DIKETAHUI. Kemudian bagian NOT IN yang diterapkan ke UNKNOWN tetap menghasilkan UNKNOWN.

Dalam istilah bahasa Inggris yang lebih sederhana, Anda meminta untuk mengembalikan pelanggan yang tidak melakukan pemesanan. Jadi wajar saja, kueri membuang semua pelanggan dari tabel Pelanggan yang ada di tabel Pesanan karena diketahui dengan pasti bahwa mereka melakukan pemesanan. Adapun sisanya (17 dan 59 dalam kasus kami) kueri membuangnya sejak ke SQL, sama seperti tidak diketahui apakah mereka memesan, sama tidak diketahui apakah mereka tidak memesan, dan filter membutuhkan kepastian (TRUE) di perintah untuk mengembalikan satu baris. Sungguh acar!

Jadi segera setelah NULL pertama masuk ke tabel Pesanan, sejak saat itu Anda selalu mendapatkan hasil kosong kembali dari kueri NOT IN. Bagaimana dengan kasus di mana Anda sebenarnya tidak memiliki NULL dalam data, tetapi kolom mengizinkan NULL? Seperti yang Anda lihat dalam eksekusi pertama Kueri 3, dalam kasus seperti itu Anda mendapatkan hasil yang benar. Mungkin Anda berpikir bahwa aplikasi tidak akan pernah memasukkan NULL ke dalam data, jadi tidak ada yang perlu Anda khawatirkan. Itu praktik yang buruk karena beberapa alasan. Pertama, jika sebuah kolom didefinisikan sebagai mengizinkan NULL, cukup banyak kepastian bahwa NULL pada akhirnya akan sampai di sana meskipun tidak seharusnya; itu hanya masalah waktu. Bisa jadi akibat mengimpor data yang buruk, bug dalam aplikasi, dan alasan lainnya. Untuk yang lain, bahkan jika data tidak berisi NULL, jika kolom mengizinkannya, pengoptimal harus memperhitungkan kemungkinan bahwa NULL akan ada saat membuat rencana kueri, dan dalam kueri NOT IN kami, ini menimbulkan penalti kinerja . Untuk mendemonstrasikan ini, pertimbangkan rencana untuk eksekusi pertama Query 3 sebelum Anda menambahkan baris dengan NULL, seperti yang ditunjukkan pada Gambar 3.

Gambar 3:Rencana eksekusi pertama Kueri 3

Operator Nested Loops teratas menangani logika Left Anti Semi Join. Itu pada dasarnya tentang mengidentifikasi ketidakcocokan, dan hubungan arus pendek aktivitas batin segera setelah kecocokan ditemukan. Bagian luar loop menarik semua 100 pelanggan dari tabel Pelanggan, maka bagian dalam loop akan dieksekusi 100 kali.

Bagian dalam loop atas menjalankan operator Nested Loops (Inner Join). Bagian luar loop bawah membuat dua baris per pelanggan—satu untuk kasus NULL dan satu lagi untuk ID pelanggan saat ini, dalam urutan ini. Jangan biarkan operator Merge Interval membingungkan Anda. Biasanya digunakan untuk menggabungkan interval yang tumpang tindih, misalnya, predikat seperti col1 BETWEEN 20 AND 30 OR col1 BETWEEN 25 AND 35 dikonversi menjadi col1 BETWEEN 20 AND 35. Ide ini dapat digeneralisasi untuk menghapus duplikat dalam predikat IN. Dalam kasus kami, tidak mungkin ada duplikat. Dalam istilah yang disederhanakan, seperti yang disebutkan, anggap bagian luar loop sebagai membuat dua baris per pelanggan—yang pertama untuk kasus NULL, dan yang kedua untuk ID pelanggan saat ini. Kemudian bagian dalam loop terlebih dahulu melakukan pencarian di indeks idx_custid pada Pesanan untuk mencari NULL. Jika NULL ditemukan, itu tidak mengaktifkan pencarian kedua untuk ID pelanggan saat ini (ingat korsleting yang ditangani oleh loop Anti Semi Join atas). Dalam kasus seperti itu, pelanggan luar dibuang. Tetapi jika NULL tidak ditemukan, loop bawah mengaktifkan pencarian kedua untuk mencari ID pelanggan saat ini di Pesanan. Jika ditemukan, pelanggan luar dibuang. Jika tidak ditemukan, pelanggan luar dikembalikan. Artinya, ketika NULL tidak ada dalam Pesanan, rencana ini melakukan dua pencarian per pelanggan! Hal ini dapat diamati dalam rencana sebagai jumlah baris 200 di input luar dari loop bawah. Akibatnya, berikut adalah statistik I/O yang dilaporkan untuk eksekusi pertama:

Table 'Orders'. Scan count 200, logical reads 603

Rencana eksekusi kedua Query 3, setelah baris dengan NULL ditambahkan ke tabel Orders, ditunjukkan pada Gambar 4.

Gambar 4:Rencana eksekusi kedua Kueri 3

Karena NULL ada di tabel, untuk semua pelanggan, eksekusi pertama dari operator Pencarian Indeks menemukan kecocokan, dan karenanya semua pelanggan dibuang. Jadi yay, kami hanya melakukan satu pencarian per pelanggan dan bukan dua, jadi kali ini Anda mendapatkan 100 pencarian dan bukan 200; namun, pada saat yang sama ini berarti Anda mendapatkan kembali hasil kosong!

Berikut adalah statistik I/O yang dilaporkan untuk eksekusi kedua:

Table 'Orders'. Scan count 100, logical reads 300

Salah satu solusi untuk tugas ini ketika NULL dimungkinkan di antara nilai yang dikembalikan dalam subkueri adalah dengan memfilternya, seperti (sebut saja Solusi 1/Kueri 4):

SELECT custid, companyname
FROM dbo.Customers
WHERE custid NOT IN (SELECT O.custid FROM dbo.Orders AS O WHERE O.custid IS NOT NULL);

Kode ini menghasilkan keluaran yang diharapkan:

custid  companyname
------- ------------
17      Cust 17
59      Cust 59

(2 rows affected)

Kelemahan dari solusi ini adalah Anda harus ingat untuk menambahkan filter. Saya lebih suka solusi menggunakan predikat NOT EXISTS, di mana subquery memiliki korelasi eksplisit yang membandingkan ID pelanggan pesanan dengan ID pelanggan pelanggan, seperti itu (sebut saja Solusi 2/Kueri 5):

SELECT C.custid, C.companyname
FROM dbo.Customers AS C
WHERE NOT EXISTS (SELECT * FROM dbo.Orders AS O WHERE O.custid = C.custid);

Ingat bahwa perbandingan berbasis kesetaraan antara NULL dan apa pun menghasilkan UNKNOWN, dan UNKNOWN akan dibuang oleh filter WHERE. Jadi, jika NULL ada di Pesanan, mereka dihilangkan oleh filter kueri dalam tanpa Anda perlu menambahkan perlakuan NULL eksplisit, dan karenanya Anda tidak perlu khawatir tentang apakah NULL ada atau tidak ada dalam data.

Kueri ini menghasilkan keluaran yang diharapkan:

custid  companyname
------- ------------
17      Cust 17
59      Cust 59

(2 rows affected)

Rencana untuk kedua solusi ditunjukkan pada Gambar 5.

Gambar 5:Rencana untuk Kueri 4 (Solusi 1) dan Kueri 5 (Solusi 2 )

Seperti yang Anda lihat, rencananya hampir identik. Mereka juga cukup efisien, menggunakan pengoptimalan Left Semi Join dengan korsleting. Keduanya hanya melakukan 100 pencarian di indeks idx_custid pada Pesanan, dan dengan operator Top, menerapkan korsleting setelah satu baris disentuh di daun.

Statistik I/O untuk kedua kueri adalah sama:

Table 'Orders'. Scan count 100, logical reads 348

Satu hal yang perlu dipertimbangkan adalah apakah ada kemungkinan tabel luar memiliki NULL di kolom yang berkorelasi (custid dalam kasus kami). Sangat tidak mungkin relevan dalam skenario seperti pesanan pelanggan, tetapi bisa relevan dalam skenario lain. Jika memang demikian, kedua solusi tersebut salah menangani NULL luar.

Untuk mendemonstrasikan ini, jatuhkan dan buat ulang tabel Pelanggan dengan NULL sebagai salah satu ID pelanggan dengan menjalankan kode berikut:

DROP TABLE IF EXISTS dbo.Customers;
GO
 
CREATE TABLE dbo.Customers
(
  custid INT NULL
    CONSTRAINT UNQ_Customers_custid UNIQUE CLUSTERED,
  companyname VARCHAR(50) NOT NULL
);
 
INSERT INTO dbo.Customers WITH (TABLOCK) (custid, companyname)
  SELECT CAST(NULL AS INT) AS custid, 'Cust NULL' AS companyname
  UNION ALL
  SELECT n AS custid, CONCAT('Cust ', CAST(n AS VARCHAR(10))) AS companyname
  FROM dbo.GetNums(1, 100);

Solusi 1 tidak akan mengembalikan NULL luar terlepas dari apakah NULL dalam ada atau tidak.

Solusi 2 akan mengembalikan NULL luar terlepas dari apakah NULL dalam ada atau tidak.

Jika Anda ingin menangani NULL seperti Anda menangani nilai non-NULL, yaitu, kembalikan NULL jika ada di Pelanggan tetapi tidak di Pesanan, dan jangan kembalikan jika ada di keduanya, Anda perlu mengubah logika solusi untuk menggunakan perbedaan perbandingan berbasis alih-alih perbandingan berbasis kesetaraan. Ini dapat dicapai dengan menggabungkan predikat EXISTS dan operator himpunan KECUALI, seperti ini (sebut saja Solusi 3/Kueri 6):

SELECT C.custid, C.companyname
FROM dbo.Customers AS C
WHERE EXISTS (SELECT C.custid EXCEPT SELECT O.custid FROM dbo.Orders AS O);

Karena saat ini ada NULL di Pelanggan dan Pesanan, kueri ini dengan benar tidak mengembalikan NULL. Inilah hasil kueri:

custid  companyname
------- ------------
17      Cust 17
59      Cust 59

(2 rows affected)

Jalankan kode berikut untuk menghapus baris dengan NULL dari tabel Pesanan dan jalankan kembali Solusi 3:

DELETE FROM dbo.Orders WHERE custid IS NULL;
 
SELECT C.custid, C.companyname
FROM dbo.Customers AS C
WHERE EXISTS (SELECT C.custid EXCEPT SELECT O.custid FROM dbo.Orders AS O);

Kali ini, karena NULL ada di Pelanggan tetapi tidak ada di Pesanan, hasilnya menyertakan NULL:

custid  companyname
------- ------------
NULL    Cust NULL
17      Cust 17
59      Cust 59

(3 rows affected)

Rencana untuk solusi ini ditunjukkan pada Gambar 6:

Gambar 6:Rencana untuk Kueri 6 (Solusi 3)

Per pelanggan, paket menggunakan operator Pemindaian Konstan untuk membuat baris dengan pelanggan saat ini, dan menerapkan pencarian tunggal di indeks idx_custid pada Pesanan untuk memeriksa apakah pelanggan ada di Pesanan. Anda berakhir dengan satu pencarian per pelanggan. Karena saat ini kami memiliki 101 pelanggan, kami mendapatkan 101 pencarian.

Berikut adalah statistik I/O untuk kueri ini:

Table 'Orders'. Scan count 101, logical reads 415

Kesimpulan

Bulan ini saya membahas bug, jebakan, dan praktik terbaik terkait subquery. Saya membahas kesalahan substitusi dan masalah logika tiga nilai. Ingatlah untuk menggunakan nama kolom yang konsisten di seluruh tabel, dan untuk selalu membuat tabel memenuhi syarat kolom dalam subkueri, bahkan jika itu adalah kolom mandiri. Ingatlah juga untuk menerapkan batasan NOT NULL saat kolom tidak seharusnya mengizinkan NULL, dan untuk selalu mempertimbangkan NULL jika memungkinkan dalam data Anda. Pastikan Anda menyertakan NULL dalam data sampel Anda saat diizinkan sehingga Anda dapat lebih mudah menangkap bug dalam kode Anda saat mengujinya. Hati-hati dengan predikat NOT IN saat digabungkan dengan subquery. Jika NULL dimungkinkan dalam hasil kueri dalam, predikat NOT EXISTS biasanya merupakan alternatif yang lebih disukai.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Migrasi Proyek Django Anda ke Heroku

  2. Model Data Catur 3D Star Trek

  3. Masalah dengan Fungsi dan Tampilan Jendela

  4. Tolong berhenti menggunakan anti-pola UPSERT ini

  5. Pengantar Data Mining