Artikel ini adalah angsuran ketiga dalam seri tentang bug T-SQL, perangkap dan praktik terbaik. Sebelumnya saya membahas determinisme dan subquery. Kali ini saya fokus pada join. Beberapa bug dan praktik terbaik yang saya bahas di sini adalah hasil survei yang saya lakukan di antara sesama MVP. Terima kasih 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 karena telah memberikan wawasan Anda!
Dalam contoh saya, saya akan menggunakan database sampel yang disebut TSQLV5. Anda dapat menemukan skrip yang membuat dan mengisi database ini di sini, dan diagram ER-nya di sini.
Dalam artikel ini saya fokus pada empat bug umum klasik:COUNT(*) di gabungan luar, agregat double-dipping, kontradiksi ON-WHERE dan kontradiksi OUTER-INNER bergabung. Semua bug ini terkait dengan dasar kueri T-SQL, dan mudah dihindari jika Anda mengikuti praktik terbaik yang sederhana.
COUNT(*) di gabungan luar
Bug pertama kami berkaitan dengan jumlah yang salah yang dilaporkan untuk grup kosong sebagai akibat dari penggunaan gabungan luar dan agregat COUNT(*). Pertimbangkan kueri berikut yang menghitung jumlah pesanan dan total pengiriman per pelanggan:
GUNAKAN TSQLV5; -- http://tsql.solidq.com/SampleDatabases/TSQLV5.zip PILIH custid, COUNT(*) AS numorders, SUM(freight) AS totalfreight FROM Sales.Orders GROUP BY custid ORDER BY custid;
Kueri ini menghasilkan keluaran berikut (disingkat):
jumlah custid totalfreight ------- ---------- ------------- 1 6 225.58 2 4 97.42 3 7 268.52 4 13 471.95 5 18 1559.52 ... 21 7 232.75 23 5 637.94 ... 56 10 862.74 58 6 277.96 ... 87 15 822.48 88 9 194.71 89 14 1353.06 90 7 88.41 91 7 175.74 (89 baris terpengaruh)
Ada 91 pelanggan saat ini hadir di tabel Pelanggan, di mana 89 memesan; maka output dari kueri ini menunjukkan 89 grup pelanggan dan jumlah pesanan yang benar serta total agregat pengiriman. Pelanggan dengan ID 22 dan 57 ada di tabel Pelanggan tetapi belum melakukan pemesanan dan oleh karena itu mereka tidak muncul di hasil.
Misalkan Anda diminta untuk menyertakan pelanggan yang tidak memiliki pesanan terkait dalam hasil kueri. Hal yang wajar untuk dilakukan dalam kasus seperti itu adalah melakukan gabungan luar kiri antara Pelanggan dan Pesanan untuk mempertahankan pelanggan tanpa pesanan. Namun, bug khas saat mengonversi solusi yang ada ke solusi yang menerapkan gabungan adalah membiarkan penghitungan jumlah pesanan sebagai COUNT(*), seperti yang ditunjukkan pada kueri berikut (sebut saja Kueri 1):
PILIH C.custid, COUNT(*) AS numorders, SUM(O.freight) AS totalfreight FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON C.custid =O.custid GROUP BY C.custid PESANAN OLEH C.custid;
Kueri ini menghasilkan keluaran berikut:
jumlah custid totalfreight ------- ---------- ------------- 1 6 225.58 2 4 97.42 3 7 268.52 4 13 471.95 5 18 1559.52 ... 21 7 232.75 22 1 NULL 23 5 637.94 ... 56 10 862.74 57 1 NULL 58 6 277.96 ... 87 15 822.48 88 9 194.71 89 14 1353.06 90 7 88.41 91 7 175.74 (91 baris terpengaruh)Perhatikan bahwa pelanggan 22 dan 57 kali ini muncul di hasil, tetapi jumlah pesanan mereka menunjukkan 1 bukannya 0 karena COUNT(*) menghitung baris dan bukan pesanan. Total pengiriman dilaporkan dengan benar karena SUM(freight) mengabaikan input NULL.
Rencana untuk kueri ini ditunjukkan pada Gambar 1.
Gambar 1:Rencana untuk Kueri 1
Dalam rencana ini Expr1002 mewakili jumlah baris per grup, yang sebagai hasil dari gabungan luar, awalnya disetel ke NULL untuk pelanggan tanpa pesanan yang cocok. Operator Compute Scalar tepat di bawah node root SELECT kemudian mengonversi NULL menjadi 1. Itu adalah hasil dari menghitung baris sebagai lawan dari menghitung pesanan.
Untuk memperbaiki bug ini, Anda ingin menerapkan COUNT agregat ke elemen dari sisi gabungan luar yang tidak dipertahankan, dan Anda ingin memastikan bahwa menggunakan kolom non-NULLable sebagai input. Kolom kunci utama akan menjadi pilihan yang baik. Inilah kueri solusi (sebut saja Kueri 2) dengan bug yang diperbaiki:
PILIH C.custid, COUNT(O.orderid) AS numorders, SUM(O.freight) AS totalfreight FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON C.custid =O.custid GROUP BY C .custid ORDER OLEH C.custid;Inilah output dari kueri ini:
jumlah custid totalfreight ------- ---------- ------------- 1 6 225.58 2 4 97.42 3 7 268.52 4 13 471.95 5 18 1559.52 ... 21 7 232.75 22 0 NULL 23 5 637.94 ... 56 10 862.74 57 0 NULL 58 6 277.96 ... 87 15 822.48 88 9 194.71 89 14 1353.06 90 7 88.41 91 7 175.74 (91 baris terpengaruh)Perhatikan bahwa kali ini pelanggan 22 dan 57 menunjukkan hitungan nol yang benar.
Rencana untuk kueri ini ditunjukkan pada Gambar 2.
Gambar 2:Rencana untuk Kueri 2
Anda juga dapat melihat perubahan dalam rencana, di mana NULL yang mewakili jumlah pelanggan tanpa pesanan yang cocok diubah menjadi 0 dan bukan 1 kali ini.
Saat menggunakan gabungan, berhati-hatilah dalam menerapkan COUNT(*) agregat. Saat menggunakan gabungan luar, biasanya itu bug. Praktik terbaik adalah menerapkan COUNT agregat ke kolom yang tidak dapat NULL dari banyak sisi dari gabungan satu-ke-banyak. Kolom kunci utama adalah pilihan yang baik untuk tujuan ini karena tidak mengizinkan NULL. Ini bisa menjadi praktik yang baik bahkan saat menggunakan inner join, karena Anda tidak pernah tahu apakah di kemudian hari Anda perlu mengubah inner join ke outer join karena perubahan persyaratan.
Agregat celup ganda
Bug kedua kami juga melibatkan pencampuran gabungan dan agregat, kali ini mempertimbangkan nilai sumber beberapa kali. Pertimbangkan kueri berikut sebagai contoh:
PILIH C.custid, COUNT(O.orderid) AS jumlah pesanan, SUM(O.freight) AS totalfreight, CAST(SUM(OD.qty * OD.unitprice * (1 - OD.discount)) SEBAGAI NUMER(12 , 2)) SEBAGAI totalval DARI Penjualan.Pelanggan SEBAGAI C LEFT OUTER JOIN Sales.Pesanan SEBAGAI O ON C.custid =O.custid LEFT OUTER JOIN Sales.OrderDetails AS OD ON O.orderid =OD.orderid GROUP BY C.custid ORDER OLEH C.custid;Kueri ini menggabungkan Pelanggan, Pesanan, dan Detail Pesanan, mengelompokkan baris berdasarkan custid, dan seharusnya menghitung agregat seperti jumlah pesanan, total pengiriman, dan nilai total per pelanggan. Kueri ini menghasilkan keluaran berikut:
jumlah custid totalfreight totalval ------- ---------- ------------- --------- 1 12 419.60 4273.00 2 10 306.59 1402.95 3 17 667.29 7023.98 4 30 1447.14 13390.65 5 52 4835.18 24927.58 ... 87 37 2611.93 15648.70 88 19 546.96 6068.20 89 40 4017.32 27363.61 90 17 262.16 3161.35 91 16 461.53 3531.95Bisakah Anda menemukan bug di sini?
Header pesanan disimpan di tabel Pesanan, dan baris pesanan masing-masing disimpan di tabel DetailPesanan. Saat Anda menggabungkan tajuk pesanan dengan baris pesanan masing-masing, tajuk tersebut diulang dalam hasil gabungan per baris. Akibatnya, agregat COUNT(O.orderid) salah mencerminkan jumlah baris pesanan dan bukan jumlah pesanan. Demikian pula, SUM(O.freight) salah memperhitungkan pengiriman beberapa kali per pesanan—sebanyak jumlah baris pesanan dalam pesanan. Satu-satunya perhitungan agregat yang benar dalam kueri ini adalah yang digunakan untuk menghitung nilai total karena diterapkan pada atribut baris pesanan:SUM(OD.qty * OD.unitprice * (1 – OD.discount).
Untuk mendapatkan jumlah pesanan yang benar, cukup menggunakan agregat jumlah yang berbeda:COUNT(DISTINCT O.orderid). Anda mungkin berpikir bahwa perbaikan yang sama dapat diterapkan pada penghitungan total pengiriman, tetapi ini hanya akan menimbulkan bug baru. Inilah kueri kami dengan agregat berbeda yang diterapkan pada ukuran header pesanan:
PILIH C.custid, COUNT(DISTINCT O.orderid) SEBAGAI numorders, SUM(DISTINCT O.freight) AS totalfreight, CAST(SUM(OD.qty * OD.unitprice * (1 - OD.discount)) SEBAGAI NUMER (12, 2)) SEBAGAI totalval DARI Penjualan.Pelanggan SEBAGAI C LEFT OUTER JOIN Sales.Pesanan SEBAGAI O ON C.custid =O.custid LEFT OUTER JOIN Sales.OrderDetails SEBAGAI OD PADA O.orderid =OD.orderid GROUP BY C. custid ORDER OLEH C.custid;Kueri ini menghasilkan keluaran berikut:
jumlah custid totalfreight totalval ------- ---------- ------------- --------- 1 6 225.58 4273.00 2 4 97.42 1402.95 3 7 268.52 7023.98 4 13 448.23 13390.65 ***** 5 18 1559.52 24927.58 ... 87 15 822.48 15648.70 88 9 194.71 6068.20 89 14 1353.06 27363.61 90 7 87.66 3161.35 ***** 91 7 175<74 3531 /pra>Jumlah pesanan sekarang benar, tetapi nilai total pengiriman tidak. Bisakah Anda menemukan bug baru?
Bug baru lebih sulit dipahami karena hanya muncul ketika pelanggan yang sama memiliki setidaknya satu kasus di mana beberapa pesanan memiliki nilai pengiriman yang sama persis. Dalam kasus seperti itu, Anda sekarang memperhitungkan pengiriman hanya sekali per pelanggan, dan bukan sekali per pesanan seperti yang seharusnya.
Gunakan kueri berikut (memerlukan SQL Server 2017 atau yang lebih baru) untuk mengidentifikasi nilai pengiriman yang tidak berbeda untuk pelanggan yang sama:
DENGAN C AS ( SELECT custid, Freight, STRING_AGG(CAST(orderid AS VARCHAR(MAX)), ', ') WITHIN GROUP(ORDER BY orderid) AS orders FROM Sales.Orders GROUP BY custid, Freight HAVING COUNT(* )> 1 ) PILIH custid, STRING_AGG(CONCAT('(freight:', Freight, ', orders:', orders, ')'), ', ') sebagai duplikat FROM C GROUP BY custid;Kueri ini menghasilkan keluaran berikut:
duplikat custid ------- -------------------------------------- - 4 (pengiriman:23,72, pesanan:10743, 10953) 90 (pengiriman:0,75, pesanan:10615, 11005)Dengan temuan ini, Anda menyadari bahwa kueri dengan bug melaporkan nilai pengiriman total yang salah untuk pelanggan 4 dan 90. Kueri melaporkan nilai pengiriman total yang benar untuk pelanggan lainnya karena nilai pengiriman mereka kebetulan unik.
Untuk memperbaiki bug, Anda perlu memisahkan perhitungan agregat pesanan dan baris pesanan ke langkah yang berbeda menggunakan ekspresi tabel, seperti:
DENGAN O AS ( SELECT custid, COUNT(orderid) AS numorders, SUM(freight) AS totalfreight FROM Sales.Orders GROUP BY custid ), OD AS ( SELECT O.custid, CAST(SUM(OD.qty * OD. unitprice * (1 - OD.discount)) SEBAGAI NUMERIK(12, 2)) SEBAGAI totalval DARI Penjualan.Pesanan SEBAGAI INNER JOIN Sales.OrderDetails SEBAGAI OD PADA O.orderid =OD.orderid GROUP BY O.custid ) SELECT C. custid, O.numorders, O.totalfreight, OD.totalval DARI Penjualan.Pelanggan AS C LEFT OUTER JOIN O ON C.custid =O.custid LEFT OUTER JOIN OD PADA C.custid =OD.custid ORDER OLEH C.custid;Kueri ini menghasilkan keluaran berikut:
jumlah custid totalfreight totalval ------- ---------- ------------- --------- 1 6 225.58 4273.00 2 4 97.42 1402.95 3 7 268.52 7023.98 4 13 471.95 13390.65 ***** 5 18 1559.52 24927.58 ... 87 15 822.48 15648.70 88 9 194.71 6068.20 89 14 1353.06 27363.61 90 7 88.41 3161.35 ***** 91 7 175<74 3531. /pra>Perhatikan total nilai pengiriman untuk pelanggan 4 dan 90 sekarang lebih tinggi. Ini adalah angka yang benar.
Praktik terbaik di sini adalah berhati-hati saat bergabung dan menggabungkan data. Anda ingin waspada terhadap kasus seperti itu saat menggabungkan beberapa tabel, dan menerapkan agregat ke ukuran dari tabel yang bukan merupakan tepi, atau daun, tabel dalam gabungan. Dalam kasus seperti itu, Anda biasanya perlu menerapkan perhitungan agregat dalam ekspresi tabel dan kemudian menggabungkan ekspresi tabel.
Jadi bug agregat pencelupan ganda diperbaiki. Namun, ada kemungkinan bug lain dalam kueri ini. Bisakah Anda menemukannya? Saya akan memberikan detail tentang bug potensial seperti kasus keempat yang akan saya bahas nanti di bawah “kontradiksi gabungan OUTER-INNER”.
kontradiksi DI MANA
Bug ketiga kami adalah hasil dari kebingungan peran yang seharusnya dimainkan oleh klausa ON dan WHERE. Sebagai contoh, misalkan Anda diberi tugas untuk mencocokkan pelanggan dan pesanan yang mereka lakukan sejak 12 Februari 2019, tetapi juga menyertakan pelanggan keluaran yang tidak melakukan pemesanan sejak saat itu. Anda mencoba menyelesaikan tugas menggunakan kueri berikut (sebut saja Kueri 3):
PILIH C.custid, C.companyname, O.orderid, O.orderdate FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON O.custid =C.custid WHERE O.orderdate>='20190212';Saat menggunakan gabungan dalam, ON dan WHERE memainkan peran penyaringan yang sama, dan oleh karena itu, tidak masalah bagaimana Anda mengatur predikat di antara klausa ini. Namun, saat menggunakan gabungan luar seperti dalam kasus kami, klausa ini memiliki arti yang berbeda.
Klausa ON memainkan peran yang cocok, artinya semua baris dari sisi gabungan yang dipertahankan (Pelanggan dalam kasus kami) akan dikembalikan. Yang memiliki kecocokan berdasarkan predikat AKTIF terhubung dengan kecocokannya, dan akibatnya, diulang per kecocokan. Yang tidak memiliki kecocokan dikembalikan dengan NULL sebagai placeholder di atribut sisi yang tidak diawetkan.
Sebaliknya, klausa WHERE, memainkan peran penyaringan yang lebih sederhana—selalu. Ini berarti bahwa baris yang predikat penyaringannya dievaluasi menjadi true dikembalikan, dan sisanya dibuang. Akibatnya, beberapa baris dari sisi gabungan yang dipertahankan dapat dihapus sama sekali.
Ingat bahwa atribut dari sisi gabungan luar yang tidak diawetkan (Pesanan dalam kasus kami) ditandai sebagai NULL untuk baris luar (tidak cocok). Setiap kali Anda menerapkan filter yang melibatkan elemen dari sisi gabungan yang tidak diawetkan, predikat filter dievaluasi menjadi tidak diketahui untuk semua baris luar, yang mengakibatkan penghapusannya. Ini sesuai dengan logika predikat tiga nilai yang diikuti SQL. Secara efektif, join menjadi inner join sebagai hasilnya. Satu-satunya pengecualian untuk aturan ini adalah ketika Anda secara khusus mencari NULL dalam elemen dari sisi yang tidak diawetkan untuk mengidentifikasi ketidakcocokan (elemen IS NULL).
Permintaan kereta kami menghasilkan output berikut:
nama perusahaan custid orderid orderdate ------- --------------- -------- ---------- 1 Pelanggan NRZBB 11011 2019-04-09 1 Pelanggan NRZBB 10952 16-03-2019 2 Pelanggan MLTDN 10926 2019-03-04 4 Pelanggan HFBZG 11016 2019-04-10 4 Pelanggan HFBZG 10953 16-03-2019 4 Pelanggan HFBZG 10920 2019-03- 03 5 Pelanggan HGVLZ 10924 2019-03-04 6 Pelanggan XHXJV 11058 2019-04-29 6 Pelanggan XHXJV 10956 2019-03-17 8 Pelanggan QUHWH 10970 2019-03-24 ... 20 Pelanggan THHDP 10979 2019-03-26 20 Pelanggan THHDP 10968 2019-03-23 20 Pelanggan THHDP 10895 2019-02-18 24 Pelanggan CYZTN 11050 27-04-2019 24 Pelanggan CYZTN 11001 2019-04-06 24 Pelanggan CYZTN 10993 2019-04-01 ... (195 baris terpengaruh)Output yang diinginkan seharusnya memiliki 213 baris termasuk 195 baris yang mewakili pesanan yang dilakukan sejak 12 Februari 2019, dan 18 baris tambahan yang mewakili pelanggan yang belum melakukan pemesanan sejak saat itu. Seperti yang Anda lihat, output sebenarnya tidak termasuk pelanggan yang belum melakukan pemesanan sejak tanggal yang ditentukan.
Rencana untuk kueri ini ditunjukkan pada Gambar 3.
Gambar 3:Rencana untuk Kueri 3
Amati bahwa pengoptimal mendeteksi kontradiksi, dan secara internal mengonversi gabungan luar menjadi gabungan dalam. Itu bagus untuk dilihat, tetapi pada saat yang sama ini merupakan indikasi yang jelas bahwa ada bug dalam kueri.
Saya telah melihat kasus di mana orang mencoba memperbaiki bug dengan menambahkan predikat OR O.orderid IS NULL ke klausa WHERE, seperti:
PILIH C.custid, C.companyname, O.orderid, O.orderdate FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON O.custid =C.custid WHERE O.orderdate>='20190212' ATAU O.orderid NULL;Satu-satunya predikat yang cocok adalah yang membandingkan ID pelanggan dari kedua sisi. Jadi join itu sendiri mengembalikan pelanggan yang melakukan pemesanan secara umum, bersama dengan pesanan yang cocok, serta pelanggan yang tidak melakukan pemesanan sama sekali, dengan NULL di atribut pesanan mereka. Kemudian predikat penyaringan menyaring pelanggan yang melakukan pemesanan sejak tanggal yang ditentukan, serta pelanggan yang belum melakukan pemesanan sama sekali (pelanggan 22 dan 57). Kueri tidak berisi pelanggan yang melakukan beberapa pesanan, tetapi tidak sejak tanggal yang ditentukan!
Kueri ini menghasilkan keluaran berikut:
nama perusahaan custid orderid orderdate ------- --------------- -------- ---------- 1 Pelanggan NRZBB 11011 2019-04-09 1 Pelanggan NRZBB 10952 16-03-2019 2 Pelanggan MLTDN 10926 2019-03-04 4 Pelanggan HFBZG 11016 2019-04-10 4 Pelanggan HFBZG 10953 16-03-2019 4 Pelanggan HFBZG 10920 2019-03- 03 5 Pelanggan HGVLZ 10924 2019-03-04 6 Pelanggan XHXJV 11058 2019-04-29 6 Pelanggan XHXJV 10956 2019-03-17 8 Pelanggan QUHWH 10970 2019-03-24 ... 20 Pelanggan THHDP 10979 2019-03-26 20 Pelanggan THHDP 10968 2019-03-23 20 Pelanggan THHDP 10895 2019-02-18 22 Pelanggan DTDMN NULL NULL 24 Pelanggan CYZTN 11050 2019-04-27 24 Pelanggan CYZTN 11001 2019-04-06 24 Pelanggan CYZTN 10993 2019-04-01 . .. (197 baris terpengaruh)Untuk memperbaiki bug dengan benar, Anda memerlukan predikat yang membandingkan ID pelanggan dari kedua sisi, dan predikat dengan tanggal pesanan untuk dianggap sebagai predikat yang cocok. Untuk mencapai ini, keduanya perlu ditentukan dalam klausa ON, seperti (sebut ini Query 4):
PILIH C.custid, C.companyname, O.orderid, O.orderdate FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON O.custid =C.custid AND O.orderdate>='20190212';Kueri ini menghasilkan keluaran berikut:
nama perusahaan custid orderid orderdate ------- --------------- -------- ---------- 1 Pelanggan NRZBB 11011 2019-04-09 1 Pelanggan NRZBB 10952 2019-03-16 2 Pelanggan MLTDN 10926 2019-03-04 3 Pelanggan KBUDE NULL NULL 4 Pelanggan HFBZG 11016 2019-04-10 4 Pelanggan HFBZG 10953 16-03-2019 4 Pelanggan HFBZG 10920 2019-03-03 5 Pelanggan HGVLZ 10924 2019-03-04 6 Pelanggan XHXJV 11058 2019-04-29 6 Pelanggan XHXJV 10956 2019-03-17 7 Pelanggan QXVLA NULL NULL 8 Pelanggan QUHWH 10970 2019-03-24 ... 20 Pelanggan THHDP 10979 2019-03-26 20 Pelanggan THHDP 10968 2019-03-23 20 Pelanggan THHDP 10895 2019-02-18 21 Pelanggan KIDPX NULL NULL 22 Pelanggan DTDMN NULL NULL 23 Pelanggan WVFAF NULL NULL 24 Pelanggan CYZTN 11050 2019-04- 27 24 Pelanggan CYZTN 11001 2019-04-06 24 Pelanggan CYZTN 10993 01-04-2019 ... (213 baris terpengaruh)Rencana untuk kueri ini ditunjukkan pada Gambar 4.
Gambar 4:Rencana untuk Kueri 4
Seperti yang Anda lihat, pengoptimal menangani gabungan kali ini sebagai gabungan luar.
Ini adalah kueri yang sangat sederhana yang saya gunakan untuk tujuan ilustrasi. Dengan kueri yang jauh lebih rumit dan kompleks, bahkan pengembang berpengalaman pun dapat kesulitan menentukan apakah predikat termasuk dalam klausa ON atau klausa WHERE. Yang membuat saya mudah adalah bertanya pada diri sendiri apakah predikat tersebut merupakan predikat yang cocok atau yang menyaring. Jika yang pertama, itu termasuk dalam klausa ON; jika yang terakhir, itu termasuk dalam klausa WHERE.
OUTER-INNER bergabung dengan kontradiksi
Bug keempat dan terakhir kami adalah variasi dari bug ketiga. Ini biasanya terjadi dalam kueri multi-gabung tempat Anda mencampur jenis gabungan. Sebagai contoh, anggaplah Anda perlu menggabungkan tabel Pelanggan, Pesanan, Detail Pesanan, Produk, dan Pemasok untuk mengidentifikasi pasangan pelanggan-pemasok yang memiliki aktivitas bersama. Anda menulis kueri berikut (sebut saja Permintaan 5):
PILIH C.custid, C.nama perusahaan SEBAGAI pelanggan, S.pemasok, S.nama perusahaan SEBAGAI pemasok DARI Penjualan.Pelanggan SEBAGAI C INNER JOIN Sales.Orders AS O ON O.custid =C.custid INNER JOIN Sales.OrderDetails AS OD ON OD.orderid =O.orderid INNER JOIN Production.Products AS P ON P.productid =OD.productid INNER JOIN Production.Supplier AS S ON S.supplierid =P.supplierid;Kueri ini menghasilkan output berikut dengan 1.236 baris:
pelanggan custid pemasok pemasok ------- --------------- ----------- ---------- ----- 1 Pelanggan NRZBB 1 Pemasok SWRXU 1 Pelanggan NRZBB 3 Pemasok STUAZ 1 Pelanggan NRZBB 7 Pemasok GQRCV ... 21 Pelanggan KIDPX 24 Pemasok JNNES 21 Pelanggan KIDPX 25 Pemasok ERVYZ 21 Pelanggan KIDPX 28 Pemasok OAVQT 23 Pelanggan WVFAF 3 Pemasok STUAZ 23 Pelanggan WVFAF 7 Pemasok GQRCV 23 Pelanggan WVFAF 8 Pemasok BWGYE ... 56 Pelanggan QNIVZ 26 Pemasok ZWZDM 56 Pelanggan QNIVZ 28 Pemasok OAVQT 56 Pelanggan QNIVZ 29 Pemasok OGLRK 58 Pelanggan AHXHT 1 Pemasok SWRXU 58 Pelanggan AHXHT 5 Pemasok EQPNC 58 Pelanggan AHXHT 6 Pemasok QWUSF ... (1236 baris terpengaruh)Rencana untuk kueri ini ditunjukkan pada Gambar 5.
Gambar 5:Rencana untuk Kueri 5
Semua gabungan dalam rencana diproses sebagai gabungan dalam seperti yang Anda harapkan.
Anda juga dapat mengamati dalam rencana bahwa pengoptimal menerapkan pengoptimalan pemesanan gabungan. Dengan gabungan dalam, pengoptimal mengetahui bahwa ia dapat mengatur ulang urutan fisik gabungan dengan cara apa pun yang diinginkan sambil mempertahankan makna kueri asli, sehingga memiliki banyak fleksibilitas. Di sini, pengoptimalan berbasis biayanya menghasilkan urutan:join(Pelanggan, gabung(Pesanan, gabung(gabung(Pemasok, Produk), OrderDetails))).
Misalkan Anda mendapatkan persyaratan untuk mengubah kueri sedemikian rupa sehingga mencakup pelanggan yang belum melakukan pemesanan. Ingatlah bahwa saat ini kami memiliki dua pelanggan seperti itu (dengan ID 22 dan 57), jadi hasil yang diinginkan seharusnya memiliki 1.238 baris. Bug umum dalam kasus seperti itu adalah mengubah gabungan dalam antara Pelanggan dan Pesanan menjadi gabungan luar kiri, tetapi membiarkan semua gabungan lainnya sebagai gabungan dalam, seperti:
PILIH C.custid, C.nama perusahaan SEBAGAI pelanggan, S.pemasok, S.nama perusahaan SEBAGAI pemasok DARI Penjualan.Pelanggan SEBAGAI C LEFT OUTER JOIN Sales.Orders AS O ON O.custid =C.custid INNER JOIN Sales. OrderDetails AS OD ON OD.orderid =O.orderid INNER JOIN Production.Products AS P ON P.productid =OD.productid INNER JOIN Production.Supplier AS S ON S.supplierid =P.supplierid;Ketika gabungan luar kiri kemudian diikuti oleh gabungan dalam atau luar kanan, dan predikat gabungan membandingkan sesuatu dari sisi yang tidak dipertahankan dari gabungan luar kiri dengan beberapa elemen lain, hasil dari predikat adalah nilai logika yang tidak diketahui, dan luar asli baris dibuang. Gabungan luar kiri secara efektif menjadi Gabungan dalam.
Akibatnya, kueri ini menghasilkan keluaran yang sama seperti untuk Kueri 5, hanya mengembalikan 1.236 baris. Juga di sini pengoptimal mendeteksi kontradiksi, dan mengubah gabungan luar menjadi gabungan dalam, menghasilkan rencana yang sama yang ditunjukkan sebelumnya pada Gambar 5.
Upaya umum untuk memperbaiki bug adalah dengan membuat semua gabungan kiri luar bergabung, seperti:
PILIH DISTINCT C.custid, C.companyname AS customer, S.supplierid, S.companyname AS supplier FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON O.custid =C.custid LEFT OUTER JOIN Sales .OrderDetails AS OD ON OD.orderid =O.orderid LEFT OUTER JOIN Production.Products AS P ON P.productid =OD.productid LEFT OUTER JOIN Production.Supplier AS S ON S.supplierid =P.supplierid;Kueri ini menghasilkan keluaran berikut, yang mencakup pelanggan 22 dan 57:
pelanggan custid pemasok pemasok ------- --------------- ----------- ---------- ----- 1 Pelanggan NRZBB 1 Pemasok SWRXU 1 Pelanggan NRZBB 3 Pemasok STUAZ 1 Pelanggan NRZBB 7 Pemasok GQRCV ... 21 Pelanggan KIDPX 24 Pemasok JNNES 21 Pelanggan KIDPX 25 Pemasok ERVYZ 21 Pelanggan KIDPX 28 Pemasok OAVQT 22 Pelanggan DTDMN NULL NULL 23 Pelanggan WVFAF 3 Pemasok STUAZ 23 Pelanggan WVFAF 7 Pemasok GQRCV 23 Pelanggan WVFAF 8 Pemasok BWGYE ... 56 Pelanggan QNIVZ 26 Pemasok ZWZDM 56 Pelanggan QNIVZ 28 Pemasok OAVQT 56 Pelanggan QNIVZ 29 Pemasok OGLRK 57 Pelanggan WVAXS NULL NULL 58 Pelanggan AHXHT 1 Pemasok SWRXU 58 Pelanggan AHXHT 5 Pemasok EQPNC 58 Pelanggan AHXHT 6 Pemasok QWUSF ... (1238 baris affe ctted)Namun, ada dua masalah dengan solusi ini. Misalkan selain Pelanggan, Anda dapat memiliki baris di tabel lain dalam kueri tanpa baris yang cocok di tabel berikutnya, dan dalam kasus seperti itu Anda tidak ingin menyimpan baris luar tersebut. Misalnya, bagaimana jika di lingkungan Anda diizinkan untuk membuat header untuk pesanan, dan di kemudian hari mengisinya dengan baris pesanan. Misalkan dalam kasus seperti itu, kueri tidak seharusnya mengembalikan header pesanan kosong tersebut. Namun, kueri seharusnya mengembalikan pelanggan tanpa pesanan. Karena gabungan antara Orders dan OrderDetails adalah gabungan luar kiri, kueri ini akan mengembalikan pesanan kosong tersebut, meskipun seharusnya tidak.
Masalah lainnya adalah ketika menggunakan gabungan luar, Anda memberlakukan lebih banyak batasan pada pengoptimal dalam hal penataan ulang yang diizinkan untuk dijelajahi sebagai bagian dari pengoptimalan pengurutan gabungannya. Pengoptimal dapat mengatur ulang gabungan A LEFT OUTER JOIN B ke B RIGHT OUTER JOIN A, tetapi itu adalah satu-satunya penataan ulang yang diizinkan untuk dijelajahi. Dengan gabungan dalam, pengoptimal juga dapat menyusun ulang tabel di luar hanya membalik sisi, misalnya, dapat menyusun ulang bergabung(bergabung(bergabung(A,B), C), D), E)))) untuk bergabung(A, join(B, join(join(E, D), C))) seperti yang ditunjukkan sebelumnya pada Gambar 5.
Jika Anda memikirkannya, yang sebenarnya Anda cari adalah bergabung dengan Pelanggan dengan hasil gabungan dalam di antara tabel lainnya. Jelas, Anda dapat mencapai ini dengan ekspresi tabel. Namun, T-SQL mendukung trik lain. Apa yang benar-benar menentukan urutan gabungan logis bukanlah urutan tabel dalam klausa FROM, melainkan urutan klausa ON. Namun, agar kueri valid, setiap klausa ON harus muncul tepat di bawah dua unit yang digabungkannya. Jadi, untuk menganggap penggabungan antara Pelanggan dan yang lainnya sebagai yang terakhir, yang perlu Anda lakukan hanyalah memindahkan klausa AKTIF yang menghubungkan Pelanggan dan yang lainnya agar muncul terakhir, seperti:
PILIH DISTINCT C.custid, C.companyname AS customer, S.supplierid, S.companyname AS supplier FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O -- pindah dari sini ------- ---------------- Penjualan INNER JOIN.OrderDetails SEBAGAI OD -- ON OD.orderid =O.orderid -- INNER JOIN Production.Products AS P -- ON P.productid =OD .productid -- INNER JOIN Production.Supplier AS S -- ON S.supplierid =P.supplierid -- ON O.custid =C.custid; -- <-- ke sini --Sekarang urutan gabungan logisnya adalah:leftjoin(Pelanggan, gabung(join(join(Orders, OrderDetails), Products), Suppliers)). Kali ini, Anda akan mempertahankan pelanggan yang belum melakukan pemesanan, tetapi Anda tidak akan mempertahankan header pesanan yang tidak memiliki baris pesanan yang cocok. Selain itu, Anda mengizinkan pengoptimal fleksibilitas pemesanan gabungan penuh di gabungan dalam antara Pesanan, Detail Pesanan, Produk, dan Pemasok.
Satu-satunya kelemahan sintaks ini adalah keterbacaan. Berita baiknya adalah ini dapat dengan mudah diperbaiki dengan menggunakan tanda kurung, seperti ini (sebut saja Kueri 6):
SELECT DISTINCT C.custid, C.companyname AS customer, S.supplierid, S.companyname AS supplier FROM Sales.Customers AS C LEFT OUTER JOIN ( Sales.Orders AS O INNER JOIN Sales.OrderDetails AS OD ON OD.orderid =O.orderid INNER JOIN Production.Products AS P ON P.productid =OD.productid INNER JOIN Production.Supplier AS S ON S.supplierid =P.supplierid ) ON O.custid =C.custid;Jangan bingung penggunaan tanda kurung di sini dengan tabel turunan. Ini bukan tabel turunan, melainkan hanya cara untuk memisahkan beberapa operator tabel ke unit mereka sendiri, untuk kejelasan. Bahasa tidak benar-benar membutuhkan tanda kurung ini, tetapi tanda kurung tersebut sangat disarankan agar mudah dibaca.
Rencana untuk kueri ini ditunjukkan pada Gambar 6.
Gambar 6:Rencana untuk Kueri 6
Perhatikan bahwa kali ini gabungan antara Pelanggan dan yang lainnya diproses sebagai gabungan luar, dan pengoptimal menerapkan pengoptimalan pemesanan gabungan.
Kesimpulan
Dalam artikel ini saya membahas empat bug klasik yang terkait dengan join. Saat menggunakan gabungan luar, menghitung agregat COUNT(*) biasanya menghasilkan bug. Praktik terbaik adalah menerapkan agregat ke kolom yang tidak dapat NULL dari sisi gabungan yang tidak diawetkan.
Saat menggabungkan beberapa tabel dan melibatkan perhitungan agregat, jika Anda menerapkan agregat ke tabel nonleaf di gabungan, biasanya itu adalah bug yang mengakibatkan agregat double-dipping. Praktik terbaik kemudian adalah menerapkan agregat dalam ekspresi tabel dan menggabungkan ekspresi tabel.
It’s common to confuse the meanings of the ON and WHERE clauses. With inner joins, they’re both filters, so it doesn’t really matter how you organize your predicates within these clauses. However, with outer joins the ON clause serves a matching role whereas the WHERE clause serves a filtering role. Understanding this helps you figure out how to organize your predicates within these clauses.
In multi-join queries, a left outer join that is subsequently followed by an inner join, or a right outer join, where you compare an element from the nonpreserved side of the join with others (other than the IS NULL test), the outer rows of the left outer join are discarded. To avoid this bug, you want to apply the left outer join last, and this can be achieved by shifting the ON clause that connects the preserved side of this join with the rest to appear last. Use parentheses for clarity even though they are not required.