sql >> Databasteknik >  >> RDS >> Database

T-SQL-buggar, fallgropar och bästa praxis – går med

Den här artikeln är den tredje delen i en serie om T-SQL-buggar, fallgropar och bästa praxis. Tidigare täckte jag determinism och delfrågor. Den här gången fokuserar jag på sammanfogningar. Några av de buggar och bästa praxis som jag tar upp här är ett resultat av en undersökning som jag gjorde bland andra MVP:er. Tack Erland Sommarskog, Aaron Bertrand, Alejandro Mesa, Umachandar Jayachandran (UC), Fabiano Neves Amorim, Milos Radivojevic, Simon Sabin, Adam Machanic, Thomas Grohser, Chan Ming Man och Paul White för era insikter!

I mina exempel kommer jag att använda en exempeldatabas som heter TSQLV5. Du kan hitta skriptet som skapar och fyller denna databas här, och dess ER-diagram här.

I den här artikeln fokuserar jag på fyra klassiska vanliga buggar:COUNT(*) i yttre kopplingar, dubbel-doppande aggregat, ON-WHERE-motsägelse och OUTTER-INNER kopplingsmotsägelse. Alla dessa buggar är relaterade till T-SQL-förfrågningsgrunderna och är lätta att undvika om du följer enkla bästa praxis.

COUNT(*) i yttre kopplingar

Vår första bugg har att göra med felaktiga räkningar som rapporterats för tomma grupper som ett resultat av användning av en yttre koppling och COUNT(*)-aggregatet. Tänk på följande fråga som beräknar antalet beställningar och total frakt per kund:

 ANVÄND TSQLV5; -- http://tsql.solidq.com/SampleDatabases/TSQLV5.zip SELECT custid, COUNT(*) AS numorders, SUM(fraight) AS totalfreight FROM Sales.Orders GROUP BY custid ORDER BY custid;

Denna fråga genererar följande utdata (förkortat):

 kundnummer totalfrakt ------- ---------- ------------- 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 13903.7 (71 89 14 13903.7) 

Det finns för närvarande 91 kunder i tabellen Kunder, av vilka 89 har lagt beställningar; därför visar resultatet av denna fråga 89 kundgrupper och deras korrekta orderantal och totala fraktaggregat. Kunder med ID 22 och 57 finns i Kundtabellen men har inte lagt några beställningar och därför dyker de inte upp i resultatet.

Anta att du uppmanas att inkludera kunder som inte har några relaterade beställningar i frågeresultatet. Det naturliga att göra i ett sådant fall är att utföra en vänster yttre sammanfogning mellan kunder och beställningar för att bevara kunder utan beställningar. En typisk bugg när man konverterar den befintliga lösningen till en som tillämpar kopplingen är att lämna beräkningen av orderantalet som COUNT(*), som visas i följande fråga (kalla det Fråga 1):

 VÄLJ C.custid, COUNT(*) AS nummorders, SUM(O.freight) AS totalfrakt FRÅN Försäljning.Kunder SOM C VÄNSTER YTTRE JOIN Sales.Orders AS O ON C.custid =O.custid GROUP BY C.custid BESTÄLLNING AV C.custid;

Den här frågan genererar följande utdata:

 kundnummer totalfrakt ------- ---------- ------------- 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 19 19 s. 71 48 88 9 19 19 s. före> 

Observera att kunderna 22 och 57 denna gång visas i resultatet, men deras orderantal visar 1 istället för 0 eftersom COUNT(*) räknar rader och inte order. Den totala frakten rapporteras korrekt eftersom SUM(frakt) ignorerar NULL-indata.

Planen för denna fråga visas i figur 1.

Figur 1:Plan för fråga 1

I denna plan representerar Expr1002 antalet rader per grupp, som, som ett resultat av den yttre sammanfogningen, initialt är satt till NULL för kunder utan matchande beställningar. Operatören Compute Scalar precis under rotnoden SELECT konverterar sedan NULL till 1. Det är resultatet av att räkna rader i motsats till att räkna order.

För att fixa det här felet vill du använda COUNT-aggregatet på ett element från den icke-bevarade sidan av den yttre sammanfogningen, och du vill se till att använda en icke-NULLbar kolumn som indata. Den primära nyckelkolumnen skulle vara ett bra val. Här är lösningsfrågan (kalla den Query 2) med felet fixat:

 VÄLJ C.custid, COUNT(O.orderid) AS nummorders, SUM(O.freight) AS totalfrakt FRÅN Försäljning.Kunder SOM C VÄNSTER YTTRE JOIN Sales.Orders AS O ON C.custid =O.custid GROUP BY C .custid BESTÄLLNING AV C.custid;

Här är resultatet av den här frågan:

 kundnummer totalfrakt ------- ---------- ------------- 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 189 s. 71. rad 88 9 19 19 s. 71. före> 

Observera att den här gången visar kunder 22 och 57 det korrekta antalet noll.

Planen för denna fråga visas i figur 2.

Figur 2:Plan för fråga 2

Du kan också se ändringen i planen, där en NULL som representerar antalet för en kund utan matchande beställningar konverteras till 0 och inte 1 den här gången.

När du använder kopplingar, var försiktig med att använda COUNT(*)-aggregatet. När du använder yttre sammanfogningar är det vanligtvis ett fel. Den bästa praxisen är att tillämpa COUNT-aggregatet på en icke-NULLbar kolumn från många sidan av en-till-många-kopplingen. Den primära nyckelkolumnen är ett bra val för detta ändamål eftersom den inte tillåter NULL. Detta kan vara en bra praxis även när du använder inre kopplingar, eftersom du aldrig vet om du vid ett senare tillfälle kommer att behöva ändra en inre koppling till en yttre på grund av ändrade krav.

Dubbeldoppande aggregat

Vår andra bugg involverar också att blanda sammanfogningar och aggregat, denna gång tar hänsyn till källvärden flera gånger. Betrakta följande fråga som ett exempel:

 SELECT C.custid, COUNT(O.orderid) AS nummorders, SUM(O.freight) AS totalfrakt, CAST(SUM(OD.kv. * OD.enhetspris * (1 - OD.rabatt)) SOM NUMERIC(12) , 2)) SOM totalt FRÅN Försäljning.Kunder SOM C VÄNSTER YTTRE JOIN Försäljning.Beställningar SOM O PÅ C.custid =O.custid VÄNSTER YTTRE JOIN Sales.OrderDetails SOM OD PÅ O.orderid =OD.orderid GRUPP PER C.custid ORDER AV C.custid;

Den här frågan sammanfogar kunder, beställningar och orderdetaljer, grupperar raderna efter custid och är tänkt att beräkna aggregat som orderantal, total frakt och totalt värde per kund. Den här frågan genererar följande utdata:

 custid numorders totalfrakt totalt ------- ---------- -------------------- ---------- 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

Kan du upptäcka felet här?

Orderrubriker lagras i tabellen Order och deras respektive orderrader lagras i tabellen OrderDetails. När du sammanfogar orderrubriker med deras respektive orderrader, upprepas rubriken i resultatet av sammanfogningen per rad. Som ett resultat återspeglar COUNT(O.orderid)-aggregatet felaktigt antalet orderrader och inte antalet order. På samma sätt tar SUM(O.frakt) felaktigt hänsyn till frakten flera gånger per order – lika många som antalet orderrader inom ordern. Den enda korrekta sammanlagda beräkningen i den här frågan är den som används för att beräkna det totala värdet eftersom den tillämpas på attributen för orderraderna:SUM(OD.qty * OD.unitprice * (1 – OD.discount).

För att få rätt orderantal räcker det att använda ett distinkt antal aggregat:COUNT(DISTINCT O.orderid). Du kanske tror att samma fix kan tillämpas på beräkningen av den totala frakten, men detta skulle bara introducera en ny bugg. Här är vår fråga med distinkta aggregat som tillämpas på orderhuvudets mått:

 SELECT C.custid, COUNT(DISTINCT O.orderid) AS nummorders, SUM(DISTINCT O.freight) AS totalfrakt, CAST(SUM(OD.kv. * OD.enhetspris * (1 - OD.rabatt)) SOM NUMERISK (12, 2)) SOM totalt FRÅN Försäljning.Kunder SOM C VÄNSTER YTTRE JOIN Sälj.Beställningar AS O ON C.custid =O.custid VÄNSTER YTTRE JOIN Sales.OrderDetails AS OD ON O.orderid =OD.orderid GRUPP AV C. custid BESTÄLLNING AV C.custid;

Den här frågan genererar följande utdata:

 custid numorders totalfrakt totalt ------- ---------- -------------------- ---------- 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

Beställningssiffrorna är nu korrekta, men de totala fraktvärdena är det inte. Kan du upptäcka den nya buggen?

Det nya felet är mer svårfångat eftersom det bara visar sig när samma kund har minst ett fall där flera beställningar råkar ha exakt samma fraktvärden. I ett sådant fall tar du nu hänsyn till frakten endast en gång per kund, och inte en gång per order som du borde.

Använd följande fråga (kräver SQL Server 2017 eller högre) för att identifiera otydliga fraktvärden för samma kund:

 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, frakt HAVING COUNT(* )> 1 ) SELECT custid, STRING_AGG(CONCAT('(frakt:', frakt, ', orders:', orders, ')'), ', ') som dubbletter FRÅN C GROUP BY custid;

Den här frågan genererar följande utdata:

 custid dubbletter ------- -------------------------------------------- - 4 (frakt:23,72, beställningar:10743, 10953) 90 (frakt:0,75, beställningar:10615, 11005)

Med dessa resultat inser du att frågan med felet rapporterade felaktiga totala fraktvärden för kunder 4 och 90. Frågan rapporterade korrekta totala fraktvärden för resten av kunderna eftersom deras fraktvärden råkade vara unika.

För att fixa felet måste du separera beräkningen av aggregat av order och orderrader till olika steg med hjälp av tabelluttryck, som så:

 WITH O AS ( SELECT custid, COUNT(orderid) AS nummorders, SUM(fraight) AS totalfrakt FRÅN Sales.Orders GROUP BY custid ), OD AS ( SELECT O.custid, CAST(SUM(OD.qty * OD. enhetspris * (1 - OD.rabatt)) SOM NUMERISK(12, 2)) SOM totalt FRÅN Försäljning.Beställningar SOM O INNER JOIN Sales.OrderDetails SOM OD PÅ O.orderid =OD.orderid GRUPPER AV O.custid ) VÄLJ C. custid, O.numorders, O.totalfreight, OD.totalval FRÅN Försäljning.Kunder SOM C VÄNSTER YTTRE JOIN O PÅ C.custid =O.custid VÄNSTER YTTRE JOIN OD PÅ C.custid =OD.custid BESTÄLLNING AV C.custid;

Den här frågan genererar följande utdata:

 custid numorders totalfrakt totalt ------- ---------- -------------------- ---------- 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 892333333333636363.61 9071 9081.41 3116.3131.31.31.3121.32111111111.31.31.31.311.311.31.31.31.31.311.3111111111.3111111111111111111111111111111111111111111111111111111111111111011111011111010101011 /pre> 

Observera att de totala fraktvärdena för kunder 4 och 90 nu är högre. Det här är de korrekta siffrorna.

Den bästa praxisen här är att vara uppmärksam när du ansluter och aggregerar data. Du vill vara uppmärksam på sådana fall när du sammanfogar flera tabeller och tillämpar aggregat på mått från en tabell som inte är en kant- eller bladtabell i sammanfogningarna. I ett sådant fall behöver du vanligtvis tillämpa de aggregerade beräkningarna inom tabelluttryck och sedan sammanfoga tabelluttrycken.

Så buggen med dubbeldoppande aggregat är åtgärdad. Det finns dock potentiellt en annan bugg i den här frågan. Kan du se det? Jag kommer att tillhandahålla detaljerna om en sådan potentiell bugg som det fjärde fallet jag kommer att täcka senare under "YTTRE-INRE kopplingsmotsägelse."

ON-WHERE motsägelse

Vår tredje bugg är ett resultat av att vi blandar ihop rollerna som ON- och WHERE-satserna ska spela. Anta som ett exempel att du fick en uppgift att matcha kunder och beställningar som de lagt sedan den 12 februari 2019, men även inkludera kunder som inte har lagt beställningar sedan dess. Du försöker lösa uppgiften med följande fråga (kalla den Fråga 3):

 VÄLJ C.custid, C.companyname, O.orderid, O.orderdate FROM Sales.Customers SOM C VÄNSTER YTTRE JOIN Sales.Orders AS O ON O.custid =C.custid WHERE O.orderdate>='20190212';

När du använder en inre join spelar både ON och WHERE samma filtreringsroller, och därför spelar det ingen roll hur du organiserar predikaten mellan dessa satser. Men när man använder en yttre sammanfogning som i vårt fall har dessa satser olika betydelser.

ON-satsen spelar en matchande roll, vilket innebär att alla rader från den bevarade sidan av join (kunder i vårt fall) kommer att returneras. De som har matchningar baserade på ON-predikatet är kopplade till sina matchningar, och som ett resultat upprepas per match. De som inte har några matchningar returneras med NULLs som platshållare i den icke-bevarade sidans attribut.

Omvänt spelar WHERE-satsen en enklare filtreringsroll – alltid. Detta innebär att rader för vilka filtreringspredikatet utvärderas till sant returneras och resten kasseras. Som ett resultat kan några av raderna från den bevarade sidan av sammanfogningen tas bort helt och hållet.

Kom ihåg att attribut från den icke-bevarade sidan av den yttre sammanfogningen (ordrar i vårt fall) är markerade som NULL för yttre rader (icke-matchningar). När du tillämpar ett filter som involverar ett element från den icke-bevarade sidan av kopplingen, utvärderas filterpredikatet till okänt för alla yttre rader, vilket resulterar i att de tas bort. Detta är i överensstämmelse med den trevärdiga predikatlogiken som SQL följer. I praktiken blir sammanfogningen en inre sammanfogning som ett resultat. Det enda undantaget från denna regel är när du specifikt letar efter en NULL i ett element från den icke-bevarade sidan för att identifiera icke-matchningar (element ÄR NULL).

Vår buggy-fråga genererar följande utdata:

 custid företagsnamn orderid orderdate ------- --------------- -------- ---------- 1 kund NRZBB 11011 2019-04-09 1 Kund NRZBB 10952 2019-03-16 2 Kund MLTDN 10926 2019-03-04 4 Kund HFBZG 11016 2019-04-10 5 Kund 2019-04-10 9BZ 4 Kund 3-100 9 HF 10 9 HF 10-10 03 5 Kund HGVLZ 10924 2019-03-04 6 Kund XHXJV 11058 2019-04-29 6 Kund XHXJV 10956 2019-03-17 8 Kund QUHWH 10197 10197 101970 ... 020 9 02 0 2 0 2 0 2 0 2 0 2 0 2 0 2 Kund THHDP 10968 2019-03-23 ​​20 Kund THHDP 10895 2019-02-18 24 Kund CYZTN 11050 2019-04-27 24 Kund CYZTN 11001 2019-204 ... Kund 9-204 ... påverkas)

Den önskade utgången är tänkt att ha 213 rader inklusive 195 rader som representerar beställningar som har lagts sedan 12 februari 2019, och 18 ytterligare rader som representerar kunder som inte har lagt beställningar sedan dess. Som du kan se inkluderar den faktiska produktionen inte de kunder som inte har lagt beställningar sedan det angivna datumet.

Planen för denna fråga visas i figur 3.

Figur 3:Plan för fråga 3

Observera att optimeraren upptäckte motsägelsen och internt konverterade den yttre kopplingen till en inre koppling. Det är bra att se, men det är samtidigt en tydlig indikation på att det finns en bugg i frågan.

Jag har sett fall där människor försökte fixa felet genom att lägga till predikatet OR O.orderid IS NULL till WHERE-satsen, som så:

 VÄLJ C.custid, C.companyname, O.orderid, O.orderdate FROM Sales.Customers SOM C VÄNSTER YTTRE JOIN Sales.Orders AS O ON O.custid =C.custid WHERE O.orderdate>='20190212' ELLER O.orderid ÄR NULL;

Det enda matchande predikatet är det som jämför kund-ID:n från de två sidorna. Så själva joinen returnerar kunder som lagt beställningar i allmänhet, tillsammans med deras matchande beställningar, såväl som kunder som inte lagt beställningar alls, med NULLs i sina beställningsattribut. Sedan predikar filtreringen filterkunder som lagt beställningar sedan det angivna datumet, samt kunder som inte har lagt beställningar alls (kunder 22 och 57). Frågan saknar kunder som gjort några beställningar, men inte sedan det angivna datumet!

Den här frågan genererar följande utdata:

 custid företagsnamn orderid orderdate ------- --------------- -------- ---------- 1 kund NRZBB 11011 2019-04-09 1 Kund NRZBB 10952 2019-03-16 2 Kund MLTDN 10926 2019-03-04 4 Kund HFBZG 11016 2019-04-10 5 Kund 2019-04-10 9BZ 4 Kund 3-100 9 HF 10 9 HF 10-10 03 5 Kund HGVLZ 10924 2019-03-04 6 Kund XHXJV 11058 2019-04-29 6 Kund XHXJV 10956 2019-03-17 8 Kund QUHWH 10197 10197 101970 ... 020 9 02 0 2 0 2 0 2 0 2 0 2 0 2 0 2 Kund THHDP 10968 2019-03-23 ​​20 Kund THHDP 10895 2019-02-18 22 Kund DTDMN NULL NULL 24 Kund CYZTN 11050 2019-04-27 24 Kund 1100Z 1100Z 1100Z 1100Z 1100Z 1100Z 1100Z 1100Z 1100Z 1100Z1100Z .. (197 rader påverkade)

För att rätta till felet behöver du både predikatet som jämför kund-ID:n från de två sidorna, och det mot beställningsdatumet för att anses matchande predikat. För att uppnå detta måste båda specificeras i ON-satsen, som så (kalla denna fråga 4):

 VÄLJ C.custid, C.companyname, O.orderid, O.orderdate FROM Sales.Customers SOM C VÄNSTER YTTRE JOIN Sales.Orders AS O ON O.custid =C.custid AND O.orderdate>='20190212';

Den här frågan genererar följande utdata:

 custid företagsnamn orderid orderdate ------- --------------- -------- ---------- 1 kund NRZBB 11011 2019-04-09 1 Kund NRZBB 10952 2019-03-16 2 Kund MLTDN 10926 2019-03-04 3 Kund KBUDE NULL NULL 4 Kund HFBZG 110916 4 Kund 20ZG 410916 410916 410916 410916 410916 410916 4-20Z 10920 2019-03-03 5 Kund HGVLZ 10924 2019-03-04 6 Kund XHXJV 11058 2019-04-29 6 Kund XHXJV 10956 2019-03-17 QNULL 7 QNULL 9 H 0H 9 H 0H 2019-03-17 2019-03-17 ... 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 NULL 23 27 24 Kund CYZTN 11001 2019-04-06 24 Kund CYZTN 10993 2019-04-01 ... (213 rader påverkade)

Planen för denna fråga visas i figur 4.

Figur 4:Plan för fråga 4

Som du kan se hanterade optimeraren kopplingen som en yttre koppling den här gången.

Detta är en mycket enkel fråga som jag använde i illustrationssyfte. Med mycket mer utarbetade och komplexa frågor kan även erfarna utvecklare ha svårt att ta reda på om ett predikat hör hemma i ON-satsen eller i WHERE-satsen. Det som gör det lätt för mig är att helt enkelt fråga mig själv om predikatet är ett matchande predikat eller ett filtrerande. Om den förra hör den hemma i ON-satsen; om det senare hör hemma i WHERE-satsen.

YTTRE-INRE sammanfogning motsägelse

Vår fjärde och sista bugg är på sätt och vis en variant av den tredje buggen. Det händer vanligtvis i frågor med flera kopplingar där du blandar kopplingstyper. Anta som ett exempel att du behöver gå med i tabellerna Kunder, Order, Orderdetaljer, Produkter och Leverantörer för att identifiera kund-leverantörspar som hade gemensam aktivitet. Du skriver följande fråga (kalla den Fråga 5):

 VÄLJ DISTINCT C.custid, C.companyname AS kund, S.supplierid, S.companyname AS leverantör FRÅN 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;

Den här frågan genererar följande utdata med 1 236 rader:

 custid kund leverantörsid leverantör ------- --------------- ------------------ ---------- ----- 1 Kund NRZBB 1 Leverantör SWRXU 1 Kund NRZBB 3 Leverantör STUAZ 1 Kund NRZBB 7 Leverantör GQRCV ... 21 Kund KIDPX 24 Leverantör JNNES 21 Kund KIDPX 25 Leverantör ERVYZ 21 Leverantör ERVYZ 21 Leverantör ERVYZ 21 Kund 0 23 Kund WVFAF 7 Leverantör GQRCV 23 Kund WVFAF 8 Leverantör BWGYE ... 56 Kund QNIVZ 26 Leverantör ZWZDM 56 Kund QNIVZ 28 Leverantör OAVQT 56 Kund QNIVZ 29 Leverantör OGLliAHX 58 Leverantör OGLliAHX 58 Leverantör 1 QWUSF ... (1236 rader påverkade)

Planen för denna fråga visas i figur 5.

Figur 5:Plan för fråga 5

Alla kopplingar i planen behandlas som inre kopplingar som du kan förvänta dig.

Du kan också observera i planen att optimeraren tillämpade anslutningsbeställningsoptimering. Med inre kopplingar vet optimeraren att den kan ordna om den fysiska ordningen för kopplingarna på vilket sätt som helst samtidigt som den ursprungliga frågans betydelse bevaras, så den har mycket flexibilitet. Här resulterade dess kostnadsbaserade optimering i ordern:join(Customers, join(Orders, join(join(Suppliers, Products), OrderDetails))).

Anta att du får ett krav på att ändra frågan så att den inkluderar kunder som inte har lagt beställningar. Kom ihåg att vi för närvarande har två sådana kunder (med ID 22 och 57), så det önskade resultatet är tänkt att ha 1 238 rader. En vanlig bugg i ett sådant fall är att ändra den inre kopplingen mellan kunder och beställningar till en vänster yttre koppling, men att lämna alla övriga kopplingar som inre, som så:

 VÄLJ DISTINKT C.custid, C.companyname AS kund, S.supplierid, S.companyname AS leverantör FRÅN Sales.Customers AS C VÄNSTER YTTRE JOIN Sales.Orders AS O ON O.custid =C.custid INNER JOIN Försäljning. 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;

När en vänster yttre koppling därefter följs av inre eller höger yttre koppling, och kopplingspredikatet jämför något från den icke-bevarade sidan av den vänstra yttre kopplingen med något annat element, blir resultatet av predikatet det logiska värdet okänt, och det ursprungliga yttre kopplingen rader kasseras. Den vänstra yttre sammanfogningen blir i praktiken en inre sammanfogning.

Som ett resultat genererar denna fråga samma utdata som för fråga 5, och returnerar endast 1 236 rader. Även här upptäcker optimeraren motsägelsen och omvandlar den yttre kopplingen till en inre koppling, vilket genererar samma plan som tidigare i figur 5.

Ett vanligt försök att fixa felet är att göra alla joins som lämnas yttre join, som så:

 VÄLJ DISTINKT C.custid, C.companyname AS kund, S.supplierid, S.companyname AS leverantör FRÅN Sales.Customers AS C VÄNSTER YTTRE JOIN Sales.Orders AS O ON O.custid =C.custid VÄNSTER YTTRE JOIN Försäljning .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;

Den här frågan genererar följande utdata, som inkluderar kunder 22 och 57:

 custid kund leverantörsid leverantör ------- --------------- ------------------ ---------- ----- 1 Kund NRZBB 1 Leverantör SWRXU 1 Kund NRZBB 3 Leverantör STUAZ 1 Kund NRZBB 7 Leverantör GQRCV ... 21 Kund KIDPX 24 Leverantör JNNES 21 Kund KIDPX 25 Leverantör ERVYZ 2 NUKIX1 Kund 2 DMKIX1 Kund 2 NULLTN Kund WVFAF 3 Leverantör STUAZ 23 Kund WVFAF 7 Leverantör GQRCV 23 Kund WVFAF 8 Leverantör BWGYE ... 56 Kund QNIVZ 26 Leverantör ZWZDM 56 Kund QNIVZ 28 Leverantör OAVQT 56 Kund QNAXIVZ 5 WWRK 5 Leverantör 5 WVRK 5 Leverantör 5 WWRK 5 Kund AHXHT 5 Leverantör EQPNC 58 Kund AHXHT 6 Leverantör QWUSF ... (1238 rader affe cted)

Det finns dock två problem med denna lösning. Anta att du förutom kunder kan ha rader i en annan tabell i frågan utan matchande rader i en efterföljande tabell, och att du i ett sådant fall inte vill behålla de yttre raderna. Till exempel, tänk om det i din miljö var tillåtet att skapa en rubrik för en order, och vid ett senare tillfälle fylla den med orderrader. Antag att frågan i ett sådant fall inte är tänkt att returnera sådana tomma orderrubriker. Ändå är frågan tänkt att returnera kunder utan beställningar. Eftersom kopplingen mellan Orders och OrderDetails är en vänster yttre koppling, kommer denna fråga att returnera sådana tomma beställningar, även om den inte borde göra det.

Ett annat problem är att när du använder outer joins lägger du fler begränsningar på optimeraren när det gäller omarrangemang som den får utforska som en del av sin join-order-optimering. Optimizern kan omarrangera sammanfogningen A LEFT OUTER JOIN B till B RIGHT OUTER JOIN A, men det är i stort sett den enda omarrangeringen den är tillåten att utforska. Med inre joins kan optimeraren också ändra ordning på tabeller utöver att bara vända sidor, till exempel kan den omordna join(join(join(join(A, B), C), D), E)))) till join(A, join(B, join(join(E,D), C))) som visas tidigare i figur 5.

Om du tänker efter är det du egentligen är ute efter att gå med i kunder med resultatet av de inre sammanfogningarna mellan resten av borden. Uppenbarligen kan du uppnå detta med tabelluttryck. T-SQL stöder dock ett annat trick. Det som verkligen avgör logisk sammanfogningsordning är inte exakt ordningen på tabellerna i FROM-satsen, snarare ordningen på ON-satserna. Men för att frågan ska vara giltig måste varje ON-klausul visas precis under de två enheter som den ansluter till. Så, för att betrakta kopplingen mellan kunder och resten som sist, är allt du behöver göra att flytta ON-klausulen som förbinder kunder och resten för att visas sist, så här:

 VÄLJ DISTINCT C.custid, C.companyname AS kund, S.supplierid, S.companyname AS leverantör FRÅN Sales.Customers AS C VÄNSTER YTTRE JOIN Sales.Orders AS O -- flytta härifrån ------- ---------------- 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; -- <-- hit --

Nu är den logiska sammanfogningsordern:leftjoin(kunder, join(join(join(Order, OrderDetails), Products), Suppliers)). Den här gången kommer du att behålla kunder som inte har gjort beställningar, men du kommer inte att behålla beställningsrubriker som inte har matchande beställningsrader. Dessutom tillåter du optimeraren full flexibilitet för kopplingsbeställning i de inre kopplingarna mellan beställningar, orderdetaljer, produkter och leverantörer.

Den enda nackdelen med denna syntax är läsbarhet. Den goda nyheten är att detta enkelt kan fixas genom att använda parenteser, som så (kalla denna fråga 6):

 VÄLJ DISTINKT C.custid, C.companyname AS kund, S.supplierid, S.companyname AS leverantör 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;

Blanda inte ihop användningen av parentes här med en härledd tabell. Detta är inte en härledd tabell, utan bara ett sätt att separera några av tabelloperatörerna till sin egen enhet, för tydlighetens skull. Språket behöver egentligen inte dessa parenteser, men de rekommenderas starkt för läsbarhet.

Planen för denna fråga visas i figur 6.

Figur 6:Plan för fråga 6

Observera att den här gången bearbetas kopplingen mellan kunder och resten som en yttre koppling, och att optimeraren tillämpade optimering av kopplingsbeställning.

Slutsats

I den här artikeln täckte jag fyra klassiska buggar relaterade till joins. När du använder yttre kopplingar resulterar beräkning av COUNT(*)-aggregatet vanligtvis i en bugg. Den bästa praxisen är att tillämpa aggregatet på en icke-NULL-bar kolumn från den icke-bevarade sidan av kopplingen.

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.


  1. Hur får man den senaste posten i varje grupp med GROUP BY?

  2. CURDATE() Exempel – MySQL

  3. Hur man tar bort efterföljande blanksteg i MySQL

  4. PIVOT-fråga på distinkta poster