यह लेख टी-एसक्यूएल बग, नुकसान और सर्वोत्तम प्रथाओं के बारे में श्रृंखला में तीसरी किस्त है। पहले मैंने नियतत्ववाद और उपश्रेणियों को कवर किया था। इस बार मैं जॉइन पर फोकस कर रहा हूं। कुछ बग और सर्वोत्तम प्रथाएं जिन्हें मैंने यहां कवर किया है, वे एक सर्वेक्षण का परिणाम हैं जो मैंने साथी एमवीपी के बीच किया था। धन्यवाद एरलैंड सोमरस्कोग, आरोन बर्ट्रेंड, एलेजांद्रो मेसा, उमाचंदर जयचंद्रन (यूसी), फैबियानो नेव्स अमोरिम, मिलोस रेडिवोजेविक, साइमन सबिन, एडम मचानिक, थॉमस ग्रोसर, चैन मिंग मैन और पॉल व्हाइट को आपकी अंतर्दृष्टि प्रदान करने के लिए!
अपने उदाहरणों में मैं TSQLV5 नामक एक नमूना डेटाबेस का उपयोग करूँगा। आप इस डेटाबेस को बनाने और भरने वाली स्क्रिप्ट यहां और इसके ईआर आरेख यहां पा सकते हैं।
इस लेख में मैं चार क्लासिक सामान्य बगों पर ध्यान केंद्रित करता हूं:COUNT(*) बाहरी जोड़ों में, डबल-डिपिंग एग्रीगेट्स, ऑन-व्हेयर विरोधाभास और OUTER-INNER अंतर्विरोध में शामिल होते हैं। ये सभी बग टी-एसक्यूएल क्वेरी बुनियादी बातों से संबंधित हैं, और यदि आप सरल सर्वोत्तम-प्रथाओं का पालन करते हैं तो इनसे बचना आसान है।
COUNT(*) बाहरी जॉइन में
बाहरी जुड़ाव और COUNT(*) समुच्चय का उपयोग करने के परिणामस्वरूप खाली समूहों के लिए रिपोर्ट की गई गलत गणनाओं के साथ हमारा पहला बग है। प्रति ग्राहक ऑर्डर की संख्या और कुल भाड़ा की गणना करने वाली निम्नलिखित क्वेरी पर विचार करें:
USE TSQLV5; -- http://tsql.solidq.com/SampleDatabases/TSQLV5.zip SELECT custid, COUNT(*) AS numorders, SUM(freight) AS totalfreight FROM Sales.Orders GROUP BY custid ORDER BY custid;
यह क्वेरी निम्न आउटपुट उत्पन्न करती है (संक्षिप्त रूप में):
custid numorders 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 rows affected)
ग्राहक तालिका में वर्तमान में 91 ग्राहक मौजूद हैं, जिनमें से 89 ने ऑर्डर दिए हैं; इसलिए इस क्वेरी का आउटपुट 89 ग्राहक समूहों और उनकी सही ऑर्डर गणना और कुल माल ढुलाई दिखाता है। आईडी 22 और 57 वाले ग्राहक ग्राहक तालिका में मौजूद हैं, लेकिन उन्होंने कोई ऑर्डर नहीं दिया है और इसलिए वे परिणाम में दिखाई नहीं देते हैं।
मान लीजिए कि आपसे उन ग्राहकों को शामिल करने का अनुरोध किया जाता है जिनके पास क्वेरी परिणाम में कोई संबंधित आदेश नहीं है। ऐसे मामले में करने के लिए स्वाभाविक बात यह है कि बिना ऑर्डर के ग्राहकों को संरक्षित करने के लिए ग्राहकों और ऑर्डर के बीच बाएं बाहरी जुड़ाव करना है। हालांकि, मौजूदा समाधान को जॉइन लागू करने वाले समाधान में परिवर्तित करते समय एक विशिष्ट बग ऑर्डर गणना की गणना को COUNT(*) के रूप में छोड़ना है, जैसा कि निम्नलिखित क्वेरी में दिखाया गया है (इसे क्वेरी 1 कहते हैं):
SELECT 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 ORDER BY C.custid;
यह क्वेरी निम्न आउटपुट उत्पन्न करती है:
custid numorders 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 rows affected)
ध्यान दें कि ग्राहक 22 और 57 इस बार परिणाम में दिखाई देते हैं, लेकिन उनकी ऑर्डर संख्या 0 के बजाय 1 दिखाती है क्योंकि COUNT(*) पंक्तियों की गणना करता है न कि ऑर्डर की। कुल भाड़ा सही ढंग से रिपोर्ट किया जाता है क्योंकि SUM(माल) NULL इनपुट पर ध्यान नहीं देता है।
इस क्वेरी की योजना चित्र 1 में दिखाई गई है।
चित्र 1:प्रश्न 1 के लिए योजना
इस योजना में Expr1002 प्रति समूह पंक्तियों की संख्या का प्रतिनिधित्व करता है, जो बाहरी जुड़ाव के परिणामस्वरूप, बिना मिलान वाले ऑर्डर वाले ग्राहकों के लिए शुरू में NULL पर सेट होता है। रूट सेलेक्ट नोड के ठीक नीचे कंप्यूट स्केलर ऑपरेटर फिर NULL को 1 में बदल देता है। यह गिनती के आदेशों के विपरीत पंक्तियों की गिनती का परिणाम है।
इस बग को ठीक करने के लिए, आप बाहरी जुड़ाव के गैर-संरक्षित पक्ष से किसी तत्व पर COUNT समुच्चय लागू करना चाहते हैं, और आप यह सुनिश्चित करना चाहते हैं कि इनपुट के रूप में एक गैर-शून्य कॉलम का उपयोग करें। प्राथमिक कुंजी कॉलम एक अच्छा विकल्प होगा। यहाँ समाधान क्वेरी है (इसे क्वेरी 2 कहते हैं) बग फिक्स के साथ:
SELECT 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 BY C.custid;
यहाँ इस क्वेरी का आउटपुट दिया गया है:
custid numorders 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 rows affected)
ध्यान दें कि इस बार ग्राहक 22 और 57 शून्य की सही गिनती दिखाते हैं।
इस क्वेरी की योजना चित्र 2 में दिखाई गई है।
चित्र 2:क्वेरी 2 के लिए योजना
आप योजना में बदलाव भी देख सकते हैं, जहां बिना मेल खाने वाले ऑर्डर वाले ग्राहक के लिए गिनती का प्रतिनिधित्व करने वाला एक NULL इस बार 0 में बदल जाता है न कि 1 में।
जॉइन का उपयोग करते समय, COUNT(*) समुच्चय को लागू करने से सावधान रहें। बाहरी जुड़ाव का उपयोग करते समय, यह आमतौर पर एक बग होता है। एक-से-अनेक जॉइन के कई पक्षों से COUNT एग्रीगेट को गैर-नलेबल कॉलम पर लागू करना सबसे अच्छा अभ्यास है। इस उद्देश्य के लिए प्राथमिक कुंजी कॉलम एक अच्छा विकल्प है क्योंकि यह एनयूएलएल की अनुमति नहीं देता है। इनर जॉइन का उपयोग करते समय भी यह एक अच्छा अभ्यास हो सकता है, क्योंकि आप कभी नहीं जानते कि बाद में आवश्यकताओं में बदलाव के कारण आपको इनर जॉइन को बाहरी जॉइन में बदलने की आवश्यकता होगी या नहीं।
डबल-डिपिंग एग्रीगेट्स
हमारे दूसरे बग में जॉइन और एग्रीगेट्स को मिलाना भी शामिल है, इस बार स्रोत मानों को कई बार ध्यान में रखते हुए। एक उदाहरण के रूप में निम्नलिखित प्रश्न पर विचार करें:
SELECT C.custid, COUNT(O.orderid) AS numorders, SUM(O.freight) AS totalfreight, CAST(SUM(OD.qty * OD.unitprice * (1 - OD.discount)) AS NUMERIC(12, 2)) AS totalval FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON C.custid = O.custid LEFT OUTER JOIN Sales.OrderDetails AS OD ON O.orderid = OD.orderid GROUP BY C.custid ORDER BY C.custid;
यह क्वेरी ग्राहकों, ऑर्डर और ऑर्डर विवरण से जुड़ती है, कस्टिड द्वारा पंक्तियों को समूहित करती है, और ऑर्डर गणना, कुल भाड़ा और प्रति ग्राहक कुल मूल्य जैसे योगों की गणना करने के लिए माना जाता है। यह क्वेरी निम्न आउटपुट उत्पन्न करती है:
custid numorders 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.95
क्या आप यहां बग ढूंढ सकते हैं?
ऑर्डर हेडर को ऑर्डर टेबल में स्टोर किया जाता है, और उनकी संबंधित ऑर्डर लाइन को ऑर्डर डिटेल टेबल में स्टोर किया जाता है। जब आप ऑर्डर हेडर को उनकी संबंधित ऑर्डर लाइन के साथ जोड़ते हैं, तो हेडर को जॉइन प्रति लाइन के परिणाम में दोहराया जाता है। परिणामस्वरूप, COUNT(O.orderid) समुच्चय ऑर्डर लाइनों की संख्या को गलत तरीके से दर्शाता है न कि ऑर्डर की संख्या को। इसी तरह, एसयूएम (ओ.फ्रेट) गलत तरीके से प्रति ऑर्डर माल ढुलाई को कई बार ध्यान में रखता है-आदेश के भीतर ऑर्डर लाइनों की संख्या। इस क्वेरी में एकमात्र सही कुल गणना वह है जिसका उपयोग कुल मूल्य की गणना करने के लिए किया जाता है क्योंकि यह ऑर्डर लाइनों की विशेषताओं पर लागू होता है:SUM(OD.qty * OD.unitprice * (1 – OD.discount)।
सही क्रम संख्या प्राप्त करने के लिए, यह एक अलग गणना कुल का उपयोग करने के लिए पर्याप्त है:COUNT(DISTINCT O.orderid)। आप सोच सकते हैं कि कुल भाड़े की गणना के लिए एक ही फिक्स लागू किया जा सकता है, लेकिन यह केवल एक नया बग पेश करेगा। ऑर्डर हेडर के उपायों पर लागू अलग-अलग समुच्चय के साथ हमारी क्वेरी यहां दी गई है:
SELECT C.custid, COUNT(DISTINCT O.orderid) AS numorders, SUM(DISTINCT O.freight) AS totalfreight, CAST(SUM(OD.qty * OD.unitprice * (1 - OD.discount)) AS NUMERIC(12, 2)) AS totalval FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON C.custid = O.custid LEFT OUTER JOIN Sales.OrderDetails AS OD ON O.orderid = OD.orderid GROUP BY C.custid ORDER BY C.custid;
यह क्वेरी निम्न आउटपुट उत्पन्न करती है:
custid numorders 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.95
ऑर्डर काउंट अब सही हैं, लेकिन कुल फ्रेट वैल्यू नहीं हैं। क्या आप नई बग खोज सकते हैं?
नया बग अधिक मायावी है क्योंकि यह केवल तभी प्रकट होता है जब एक ही ग्राहक के पास कम से कम एक मामला होता है जहां एक से अधिक ऑर्डर में समान फ्रेट वैल्यू होती है। ऐसे मामले में, अब आप प्रति ग्राहक केवल एक बार भाड़ा खाते में ले रहे हैं, न कि एक बार प्रति आदेश जैसा आपको करना चाहिए।
एक ही ग्राहक के लिए गैर-विशिष्ट फ्रेट मानों की पहचान करने के लिए निम्न क्वेरी (SQL Server 2017 या इसके बाद के संस्करण की आवश्यकता है) का उपयोग करें:
WITH 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 ) SELECT custid, STRING_AGG(CONCAT('(freight: ', freight, ', orders: ', orders, ')'), ', ') as duplicates FROM C GROUP BY custid;
यह क्वेरी निम्न आउटपुट उत्पन्न करती है:
custid duplicates ------- --------------------------------------- 4 (freight: 23.72, orders: 10743, 10953) 90 (freight: 0.75, orders: 10615, 11005)
इन निष्कर्षों के साथ, आपको पता चलता है कि बग वाली क्वेरी ने ग्राहकों 4 और 90 के लिए गलत कुल फ्रेट वैल्यू की सूचना दी। क्वेरी ने बाकी ग्राहकों के लिए सही कुल फ्रेट वैल्यू की सूचना दी, क्योंकि उनका फ्रेट वैल्यू अद्वितीय था।
बग को ठीक करने के लिए आपको टेबल एक्सप्रेशन का उपयोग करके ऑर्डर और ऑर्डर लाइनों के योगों की गणना को अलग-अलग चरणों में अलग करना होगा, जैसे:
WITH 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)) AS NUMERIC(12, 2)) AS totalval FROM Sales.Orders AS O INNER JOIN Sales.OrderDetails AS OD ON O.orderid = OD.orderid GROUP BY O.custid ) SELECT C.custid, O.numorders, O.totalfreight, OD.totalval FROM Sales.Customers AS C LEFT OUTER JOIN O ON C.custid = O.custid LEFT OUTER JOIN OD ON C.custid = OD.custid ORDER BY C.custid;
यह क्वेरी निम्न आउटपुट उत्पन्न करती है:
custid numorders 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.95
ग्राहकों 4 और 90 के लिए कुल माल भाड़ा मान अब अधिक है। ये सही संख्याएं हैं।
डेटा को जोड़ने और एकत्र करने के दौरान यहां सबसे अच्छा अभ्यास सावधान रहना है। आप ऐसे मामलों के प्रति सतर्क रहना चाहते हैं जब कई तालिकाओं में शामिल हों, और एक तालिका से उपायों के लिए समुच्चय लागू करना जो कि किनारों, या पत्ती, तालिका में शामिल नहीं है। ऐसे मामले में, आपको आमतौर पर टेबल एक्सप्रेशन के भीतर एग्रीगेट कंप्यूटेशंस को लागू करने और फिर टेबल एक्सप्रेशन में शामिल होने की आवश्यकता होती है।
तो डबल-डिपिंग एग्रीगेट्स बग फिक्स है। हालांकि, इस क्वेरी में संभावित रूप से एक और बग है। क्या आप इसका पता लगा सकते हैं? मैं चौथे मामले के रूप में इस तरह के संभावित बग के बारे में विवरण प्रदान करूंगा, जिसे मैं बाद में "OUTER-INNER जॉइन विरोधाभास" के अंतर्गत कवर करूंगा।
ऑन-व्हेयर विरोधाभास
हमारा तीसरा बग उन भूमिकाओं को भ्रमित करने का परिणाम है जो ON और WHERE क्लॉज को निभानी चाहिए। एक उदाहरण के रूप में, मान लीजिए कि आपको 12 फरवरी, 2019 से ग्राहकों और उनके द्वारा दिए गए ऑर्डर से मिलान करने का कार्य दिया गया था, लेकिन उन आउटपुट ग्राहकों में भी शामिल हैं, जिन्होंने तब से ऑर्डर नहीं दिए थे। आप निम्न क्वेरी का उपयोग करके कार्य को हल करने का प्रयास करते हैं (इसे क्वेरी 3 कहते हैं):
SELECT 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';
इनर जॉइन का उपयोग करते समय, ON और WHERE दोनों समान फ़िल्टरिंग भूमिकाएँ निभाते हैं, और इसलिए इससे कोई फ़र्क नहीं पड़ता कि आप इन क्लॉज़ के बीच विधेय को कैसे व्यवस्थित करते हैं। हालांकि, हमारे मामले में बाहरी जुड़ाव का उपयोग करते समय, इन खंडों के अलग-अलग अर्थ होते हैं।
ON क्लॉज एक मेल खाने वाली भूमिका निभाता है, जिसका अर्थ है कि शामिल होने के संरक्षित पक्ष से सभी पंक्तियाँ (हमारे मामले में ग्राहक) वापस आने वाली हैं। जिनके पास ON विधेय के आधार पर मैच होते हैं, वे अपने मैचों से जुड़े होते हैं, और परिणामस्वरूप, प्रति मैच दोहराया जाता है। जिनके पास कोई मिलान नहीं है, उन्हें गैर-संरक्षित पक्ष की विशेषताओं में प्लेसहोल्डर के रूप में NULLs के साथ लौटा दिया जाता है।
इसके विपरीत, WHERE क्लॉज, एक आसान फ़िल्टरिंग भूमिका निभाता है—हमेशा। इसका मतलब यह है कि जिन पंक्तियों के लिए फ़िल्टरिंग विधेय का मूल्यांकन करता है, उन्हें वापस कर दिया जाता है, और बाकी सभी को छोड़ दिया जाता है। नतीजतन, शामिल होने के संरक्षित पक्ष से कुछ पंक्तियों को पूरी तरह से हटाया जा सकता है।
याद रखें कि बाहरी जुड़ाव (हमारे मामले में आदेश) के गैर-संरक्षित पक्ष की विशेषताओं को बाहरी पंक्तियों (गैर-मिलान) के लिए NULLs के रूप में चिह्नित किया गया है। जब भी आप शामिल होने के गैर-संरक्षित पक्ष से किसी तत्व को शामिल करने वाला फ़िल्टर लागू करते हैं, तो फ़िल्टर विधेय सभी बाहरी पंक्तियों के लिए अज्ञात का मूल्यांकन करता है, जिसके परिणामस्वरूप उन्हें हटा दिया जाता है। यह तीन-मूल्यवान विधेय तर्क के अनुरूप है जो SQL अनुसरण करता है। प्रभावी रूप से, परिणाम के रूप में शामिल होना एक आंतरिक जुड़ाव बन जाता है। इस नियम का एक अपवाद तब होता है जब आप गैर-संरक्षित पक्ष से किसी तत्व में विशेष रूप से एक NULL की तलाश करते हैं ताकि गैर-मिलानों की पहचान की जा सके (तत्व IS NULL)।
हमारी छोटी गाड़ी क्वेरी निम्न आउटपुट उत्पन्न करती है:
custid companyname orderid orderdate ------- --------------- -------- ---------- 1 Customer NRZBB 11011 2019-04-09 1 Customer NRZBB 10952 2019-03-16 2 Customer MLTDN 10926 2019-03-04 4 Customer HFBZG 11016 2019-04-10 4 Customer HFBZG 10953 2019-03-16 4 Customer HFBZG 10920 2019-03-03 5 Customer HGVLZ 10924 2019-03-04 6 Customer XHXJV 11058 2019-04-29 6 Customer XHXJV 10956 2019-03-17 8 Customer QUHWH 10970 2019-03-24 ... 20 Customer THHDP 10979 2019-03-26 20 Customer THHDP 10968 2019-03-23 20 Customer THHDP 10895 2019-02-18 24 Customer CYZTN 11050 2019-04-27 24 Customer CYZTN 11001 2019-04-06 24 Customer CYZTN 10993 2019-04-01 ... (195 rows affected)
वांछित आउटपुट में 213 पंक्तियाँ होनी चाहिए, जिसमें 195 पंक्तियाँ शामिल हैं जो 12 फरवरी, 2019 से रखे गए आदेशों का प्रतिनिधित्व करती हैं, और 18 अतिरिक्त पंक्तियाँ उन ग्राहकों का प्रतिनिधित्व करती हैं जिन्होंने तब से ऑर्डर नहीं दिया है। जैसा कि आप देख सकते हैं, वास्तविक आउटपुट में वे ग्राहक शामिल नहीं हैं जिन्होंने निर्दिष्ट तिथि से ऑर्डर नहीं दिए हैं।
इस क्वेरी की योजना चित्र 3 में दिखाई गई है।
चित्र 3:क्वेरी 3 के लिए योजना
ध्यान दें कि ऑप्टिमाइज़र ने विरोधाभास का पता लगाया, और आंतरिक रूप से बाहरी जुड़ाव को आंतरिक जुड़ाव में बदल दिया। यह देखना अच्छा है, लेकिन साथ ही यह एक स्पष्ट संकेत है कि क्वेरी में एक बग है।
मैंने ऐसे मामले देखे हैं जहां लोगों ने WHERE क्लॉज में विधेय या O.orderid IS NULL जोड़कर बग को ठीक करने का प्रयास किया, जैसे:
SELECT 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' OR O.orderid IS NULL;
दोनों पक्षों से ग्राहक आईडी की तुलना करने वाला एकमात्र मिलान विधेय है। इसलिए जॉइन स्वयं उन ग्राहकों को लौटाता है, जिन्होंने अपने मिलान वाले ऑर्डर के साथ-साथ सामान्य रूप से ऑर्डर दिए थे, साथ ही ऐसे ग्राहक जिन्होंने ऑर्डर नहीं दिए थे, उनके ऑर्डर विशेषताओं में एनयूएलएल के साथ। फिर फ़िल्टरिंग उन ग्राहकों को फ़िल्टर करता है जिन्होंने निर्दिष्ट तिथि से ऑर्डर दिए हैं, साथ ही ऐसे ग्राहक जिन्होंने ऑर्डर बिल्कुल नहीं दिए हैं (ग्राहक 22 और 57)। क्वेरी में कुछ ऑर्डर देने वाले ग्राहक नहीं हैं, लेकिन निर्दिष्ट तिथि से नहीं!
यह क्वेरी निम्न आउटपुट उत्पन्न करती है:
custid companyname orderid orderdate ------- --------------- -------- ---------- 1 Customer NRZBB 11011 2019-04-09 1 Customer NRZBB 10952 2019-03-16 2 Customer MLTDN 10926 2019-03-04 4 Customer HFBZG 11016 2019-04-10 4 Customer HFBZG 10953 2019-03-16 4 Customer HFBZG 10920 2019-03-03 5 Customer HGVLZ 10924 2019-03-04 6 Customer XHXJV 11058 2019-04-29 6 Customer XHXJV 10956 2019-03-17 8 Customer QUHWH 10970 2019-03-24 ... 20 Customer THHDP 10979 2019-03-26 20 Customer THHDP 10968 2019-03-23 20 Customer THHDP 10895 2019-02-18 22 Customer DTDMN NULL NULL 24 Customer CYZTN 11050 2019-04-27 24 Customer CYZTN 11001 2019-04-06 24 Customer CYZTN 10993 2019-04-01 ... (197 rows affected)
बग को सही ढंग से ठीक करने के लिए, आपको दोनों पक्षों से ग्राहक आईडी की तुलना करने वाले विधेय की आवश्यकता है, और ऑर्डर तिथि के विरुद्ध एक को मेल खाने वाले विधेय के रूप में माना जाना चाहिए। इसे प्राप्त करने के लिए, दोनों को ON क्लॉज में निर्दिष्ट करने की आवश्यकता है, जैसे (इस क्वेरी 4 को कॉल करें):
SELECT 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';
यह क्वेरी निम्न आउटपुट उत्पन्न करती है:
custid companyname orderid orderdate ------- --------------- -------- ---------- 1 Customer NRZBB 11011 2019-04-09 1 Customer NRZBB 10952 2019-03-16 2 Customer MLTDN 10926 2019-03-04 3 Customer KBUDE NULL NULL 4 Customer HFBZG 11016 2019-04-10 4 Customer HFBZG 10953 2019-03-16 4 Customer HFBZG 10920 2019-03-03 5 Customer HGVLZ 10924 2019-03-04 6 Customer XHXJV 11058 2019-04-29 6 Customer XHXJV 10956 2019-03-17 7 Customer QXVLA NULL NULL 8 Customer QUHWH 10970 2019-03-24 ... 20 Customer THHDP 10979 2019-03-26 20 Customer THHDP 10968 2019-03-23 20 Customer THHDP 10895 2019-02-18 21 Customer KIDPX NULL NULL 22 Customer DTDMN NULL NULL 23 Customer WVFAF NULL NULL 24 Customer CYZTN 11050 2019-04-27 24 Customer CYZTN 11001 2019-04-06 24 Customer CYZTN 10993 2019-04-01 ... (213 rows affected)
इस क्वेरी की योजना चित्र 4 में दिखाई गई है।
चित्र 4:क्वेरी 4 के लिए योजना
जैसा कि आप देख सकते हैं, ऑप्टिमाइज़र ने इस बार जॉइन को बाहरी जॉइन के रूप में हैंडल किया।
यह एक बहुत ही सरल प्रश्न है जिसका उपयोग मैंने चित्रण उद्देश्यों के लिए किया है। अधिक विस्तृत और जटिल प्रश्नों के साथ, अनुभवी डेवलपर्स को भी यह पता लगाने में कठिनाई हो सकती है कि कोई विधेय ON क्लॉज में है या WHERE क्लॉज में है। जो चीज मेरे लिए चीजों को आसान बनाती है, वह यह है कि मैं बस अपने आप से पूछूं कि क्या विधेय एक मेल खाने वाला विधेय है या छानने वाला। यदि पूर्व, यह ON क्लॉज में है; यदि बाद वाला है, तो यह WHERE क्लॉज में आता है।
OUTER-INNER अंतर्विरोध में शामिल हों
हमारा चौथा और आखिरी बग एक तरह से तीसरे बग का वेरिएशन है। यह आम तौर पर मल्टी-जॉइन क्वेरी में होता है जहां आप जॉइन टाइप्स को मिलाते हैं। एक उदाहरण के रूप में, मान लीजिए कि आपको ग्राहक-आपूर्तिकर्ता जोड़े की पहचान करने के लिए ग्राहक, ऑर्डर, ऑर्डर विवरण, उत्पाद और आपूर्तिकर्ता तालिकाओं में शामिल होने की आवश्यकता है, जिनकी संयुक्त गतिविधि थी। आप निम्नलिखित प्रश्न लिखते हैं (इसे प्रश्न 5 कहते हैं):
SELECT DISTINCT C.custid, C.companyname AS customer, S.supplierid, S.companyname AS supplier FROM Sales.Customers AS 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.Suppliers AS S ON S.supplierid = P.supplierid;पर S आपूर्ति करते हैं।
यह क्वेरी 1,236 पंक्तियों के साथ निम्न आउटपुट उत्पन्न करती है:
custid customer supplierid supplier ------- --------------- ----------- --------------- 1 Customer NRZBB 1 Supplier SWRXU 1 Customer NRZBB 3 Supplier STUAZ 1 Customer NRZBB 7 Supplier GQRCV ... 21 Customer KIDPX 24 Supplier JNNES 21 Customer KIDPX 25 Supplier ERVYZ 21 Customer KIDPX 28 Supplier OAVQT 23 Customer WVFAF 3 Supplier STUAZ 23 Customer WVFAF 7 Supplier GQRCV 23 Customer WVFAF 8 Supplier BWGYE ... 56 Customer QNIVZ 26 Supplier ZWZDM 56 Customer QNIVZ 28 Supplier OAVQT 56 Customer QNIVZ 29 Supplier OGLRK 58 Customer AHXHT 1 Supplier SWRXU 58 Customer AHXHT 5 Supplier EQPNC 58 Customer AHXHT 6 Supplier QWUSF ... (1236 rows affected)
इस क्वेरी की योजना चित्र 5 में दिखाई गई है।
चित्र 5:प्रश्न 5 के लिए योजना
योजना में शामिल होने वाले सभी लोगों को आपकी अपेक्षानुसार आंतरिक जुड़ाव के रूप में संसाधित किया जाता है।
आप योजना में यह भी देख सकते हैं कि ऑप्टिमाइज़र ने जॉइन-ऑर्डरिंग ऑप्टिमाइज़ेशन लागू किया है। आंतरिक जुड़ाव के साथ, ऑप्टिमाइज़र जानता है कि यह मूल क्वेरी के अर्थ को संरक्षित करते हुए किसी भी तरह से जुड़ने के भौतिक क्रम को पुनर्व्यवस्थित कर सकता है, इसलिए इसमें बहुत लचीलापन है। यहां, इसके लागत-आधारित अनुकूलन के परिणामस्वरूप ऑर्डर मिला:शामिल हों (ग्राहक, शामिल हों (आदेश, शामिल हों (शामिल हों (आपूर्तिकर्ता, उत्पाद), ऑर्डर विवरण)))।
मान लीजिए कि आपको क्वेरी को इस तरह बदलने की आवश्यकता है कि इसमें ऐसे ग्राहक शामिल हों जिन्होंने ऑर्डर नहीं दिए हैं। याद रखें कि वर्तमान में हमारे पास ऐसे दो ग्राहक हैं (आईडी 22 और 57 के साथ), इसलिए वांछित परिणाम में 1,238 पंक्तियां होनी चाहिए। ऐसे मामले में एक सामान्य बग ग्राहकों और ऑर्डर के बीच के आंतरिक जुड़ाव को बाएं बाहरी जोड़ में बदलना है, लेकिन बाकी सभी जुड़ावों को आंतरिक के रूप में छोड़ना है, जैसे:
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 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.Suppliers AS S ON S.supplierid = P.supplierid;
जब एक बायां बाहरी जुड़ाव बाद में आंतरिक या दाएं बाहरी जुड़ता है, और शामिल होने की भविष्यवाणी बाएं बाहरी के गैर-संरक्षित पक्ष से किसी अन्य तत्व के साथ तुलना करती है, तो विधेय का परिणाम तार्किक मूल्य अज्ञात है, और मूल बाहरी पंक्तियों को त्याग दिया जाता है। बायां बाहरी जुड़ाव प्रभावी रूप से आंतरिक जुड़ाव बन जाता है।
नतीजतन, यह क्वेरी क्वेरी 5 के लिए समान आउटपुट उत्पन्न करती है, केवल 1,236 पंक्तियों को लौटाती है। साथ ही यहां ऑप्टिमाइज़र विरोधाभास का पता लगाता है, और बाहरी जुड़ाव को एक आंतरिक जुड़ाव में परिवर्तित करता है, वही योजना बनाता है जो पहले चित्र 5 में दिखाया गया है।
बग को ठीक करने का एक सामान्य प्रयास है सभी जॉइन को लेफ्ट आउटर जॉइन करना, जैसे:
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 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.Suppliers AS S ON S.supplierid = P.supplierid;
यह क्वेरी निम्न आउटपुट उत्पन्न करती है, जिसमें ग्राहक 22 और 57 शामिल हैं:
custid customer supplierid supplier ------- --------------- ----------- --------------- 1 Customer NRZBB 1 Supplier SWRXU 1 Customer NRZBB 3 Supplier STUAZ 1 Customer NRZBB 7 Supplier GQRCV ... 21 Customer KIDPX 24 Supplier JNNES 21 Customer KIDPX 25 Supplier ERVYZ 21 Customer KIDPX 28 Supplier OAVQT 22 Customer DTDMN NULL NULL 23 Customer WVFAF 3 Supplier STUAZ 23 Customer WVFAF 7 Supplier GQRCV 23 Customer WVFAF 8 Supplier BWGYE ... 56 Customer QNIVZ 26 Supplier ZWZDM 56 Customer QNIVZ 28 Supplier OAVQT 56 Customer QNIVZ 29 Supplier OGLRK 57 Customer WVAXS NULL NULL 58 Customer AHXHT 1 Supplier SWRXU 58 Customer AHXHT 5 Supplier EQPNC 58 Customer AHXHT 6 Supplier QWUSF ... (1238 rows affected)
हालाँकि, इस समाधान के साथ दो समस्याएं हैं। मान लीजिए कि ग्राहकों के अलावा आपके पास क्वेरी में किसी अन्य तालिका में पंक्तियाँ हो सकती हैं और बाद की तालिका में कोई मिलान पंक्तियाँ नहीं हैं, और ऐसी स्थिति में आप उन बाहरी पंक्तियों को नहीं रखना चाहते हैं। उदाहरण के लिए, क्या होगा यदि आपके वातावरण में किसी ऑर्डर के लिए हेडर बनाने की अनुमति दी गई हो, और बाद में इसे ऑर्डर लाइनों से भरें। मान लीजिए कि ऐसे मामले में, क्वेरी को ऐसे खाली ऑर्डर हेडर वापस नहीं करना चाहिए। फिर भी, क्वेरी बिना ऑर्डर के ग्राहकों को लौटाने वाली है। चूंकि ऑर्डर और ऑर्डर डिटेल्स के बीच जुड़ाव एक बायां बाहरी जुड़ाव है, इसलिए यह क्वेरी ऐसे खाली ऑर्डर लौटाएगी, भले ही ऐसा नहीं होना चाहिए।
एक और समस्या यह है कि बाहरी जुड़ाव का उपयोग करते समय, आप ऑप्टिमाइज़र पर पुनर्व्यवस्था के संदर्भ में अधिक प्रतिबंध लगाते हैं, जिसे इसके जुड़ने-आदेश अनुकूलन के हिस्से के रूप में तलाशने की अनुमति है। ऑप्टिमाइज़र जॉइन ए लेफ्ट आउटर जॉइन बी को बी राइट आउटर जॉइन ए में पुनर्व्यवस्थित कर सकता है, लेकिन यह बहुत ही एकमात्र पुनर्व्यवस्था है जिसे इसे एक्सप्लोर करने की अनुमति है। इनर जॉइन के साथ, ऑप्टिमाइज़र केवल फ़्लिपिंग पक्षों से परे तालिकाओं को फिर से व्यवस्थित कर सकता है, उदाहरण के लिए, यह ज्वाइन (जॉइन (जॉइन (जॉइन (ए, बी), सी), डी), ई))) जॉइन (ए, join(B, join(join(E, D), C))) जैसा कि पहले चित्र 5 में दिखाया गया है।
यदि आप इसके बारे में सोचते हैं, तो आप वास्तव में बाकी तालिकाओं के बीच आंतरिक जुड़ाव के परिणाम के साथ ग्राहकों को छोड़ देना चाहते हैं। जाहिर है, आप इसे टेबल एक्सप्रेशन से हासिल कर सकते हैं। हालांकि, टी-एसक्यूएल एक और चाल का समर्थन करता है। जो वास्तव में लॉजिकल जॉइन ऑर्डरिंग को निर्धारित करता है, वह FROM क्लॉज में तालिकाओं का क्रम नहीं है, बल्कि ON क्लॉज का क्रम है। हालाँकि, क्वेरी के मान्य होने के लिए, प्रत्येक ON क्लॉज़ को उन दो इकाइयों के ठीक नीचे दिखाई देना चाहिए, जिनमें वह शामिल हो रहा है। इसलिए, ग्राहकों और बाकी के बीच जुड़ाव को अंतिम मानने के लिए, आपको केवल ON क्लॉज को स्थानांतरित करने की आवश्यकता है जो ग्राहकों को जोड़ता है और बाकी को अंतिम रूप से प्रदर्शित करता है, जैसे:
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 -- move from here ----------------------- 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.Suppliers AS S -- ON S.supplierid = P.supplierid -- ON O.custid = C.custid; -- <-- to here --
अब तार्किक जॉइन ऑर्डरिंग है:लेफ्टजॉइन (ग्राहक, शामिल हों (शामिल हों (ऑर्डर, ऑर्डर विवरण), उत्पाद), आपूर्तिकर्ता))। इस बार, आप उन ग्राहकों को रखेंगे जिन्होंने ऑर्डर नहीं दिए हैं, लेकिन आप ऐसे ऑर्डर हेडर नहीं रखेंगे जिनमें मेल खाने वाली ऑर्डर लाइन नहीं हैं। साथ ही, आप ऑप्टिमाइज़र को ऑर्डर, ऑर्डर विवरण, उत्पादों और आपूर्तिकर्ताओं के बीच आंतरिक जुड़ाव में पूर्ण जॉइन-ऑर्डरिंग लचीलेपन की अनुमति देते हैं।
इस वाक्य रचना का एक दोष पठनीयता है। अच्छी खबर यह है कि इसे कोष्ठकों का उपयोग करके आसानी से ठीक किया जा सकता है, जैसे (इस प्रश्न 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.Suppliers AS S ON S.supplierid = P.supplierid ) ON O.custid = C.custid;
व्युत्पन्न तालिका के साथ यहां कोष्ठक के उपयोग को भ्रमित न करें। यह एक व्युत्पन्न तालिका नहीं है, बल्कि स्पष्टता के लिए कुछ टेबल ऑपरेटरों को अपनी इकाई में अलग करने का एक तरीका है। भाषा को वास्तव में इन कोष्ठकों की आवश्यकता नहीं है, लेकिन पठनीयता के लिए उनकी दृढ़ता से अनुशंसा की जाती है।
इस क्वेरी की योजना चित्र 6 में दिखाई गई है।
चित्र 6:प्रश्न 6 के लिए योजना
ध्यान दें कि इस बार ग्राहकों और बाकी के बीच जुड़ाव को बाहरी जुड़ाव के रूप में संसाधित किया जाता है, और यह कि ऑप्टिमाइज़र ने जॉइन-ऑर्डरिंग ऑप्टिमाइज़ेशन लागू किया है।
निष्कर्ष
इस लेख में मैंने जुड़ने से संबंधित चार क्लासिक बग को कवर किया है। बाहरी जॉइन का उपयोग करते समय, COUNT(*) समुच्चय की गणना करने से आमतौर पर एक बग उत्पन्न होता है। सबसे अच्छा अभ्यास यह है कि जॉइन के गैर-संरक्षित पक्ष से एक गैर-नलबल कॉलम में समुच्चय को लागू किया जाए।
When joining multiple tables and involving aggregate calculations, if you apply the aggregates to a nonleaf table in the joins, it’s usually a bug resulting in double-dipping aggregates. The best practice is then to apply the aggregates within table expressions and joining the table expressions.
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.