Denne artikel er den tredje del i en serie om T-SQL-fejl, faldgruber og bedste praksis. Tidligere dækkede jeg determinisme og underspørgsmål. Denne gang fokuserer jeg på joins. Nogle af de fejl og bedste praksis, som jeg dækker her, er et resultat af en undersøgelse, jeg lavede blandt andre MVP'er. Tak Erland Sommarskog, Aaron Bertrand, Alejandro Mesa, Umachandar Jayachandran (UC), Fabiano Neves Amorim, Milos Radivojevic, Simon Sabin, Adam Machanic, Thomas Grohser, Chan Ming Man og Paul White for at tilbyde din indsigt!
I mine eksempler vil jeg bruge en prøvedatabase kaldet TSQLV5. Du kan finde scriptet, der opretter og udfylder denne database her, og dets ER-diagram her.
I denne artikel fokuserer jeg på fire klassiske almindelige fejl:COUNT(*) i ydre joinforbindelser, double-dipping aggregater, ON-WHERE modsigelse og OUTER-INNER joinmodsigelse. Alle disse fejl er relateret til grundlæggende T-SQL-forespørgsler og er nemme at undgå, hvis du følger enkle bedste fremgangsmåder.
COUNT(*) i ydre sammenføjninger
Vores første fejl har at gøre med forkerte tællinger rapporteret for tomme grupper som følge af brug af en ydre joinforbindelse og COUNT(*)-aggregatet. Overvej følgende forespørgsel, der beregner antallet af ordrer og samlet fragt pr. kunde:
BRUG TSQLV5; -- http://tsql.solidq.com/SampleDatabases/TSQLV5.zip SELECT custid, COUNT(*) AS antal, SUM(fragt) AS totalfragt FRA Salg.Ordrer GRUPPER EFTER custid ORDER BY custid;
Denne forespørgsel genererer følgende output (forkortet):
depotnumre totalfragt ------- ---------- ------------ 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)Der er i øjeblikket 91 kunder til stede i kundetabellen, hvoraf 89 afgav ordrer; derfor viser outputtet af denne forespørgsel 89 kundegrupper og deres korrekte ordreantal og samlede fragtaggregater. Kunder med ID 22 og 57 er til stede i Kunder-tabellen, men har ikke afgivet nogen ordrer, og de vises derfor ikke i resultatet.
Antag, at du bliver bedt om at inkludere kunder, der ikke har nogen relaterede ordrer, i forespørgselsresultatet. Den naturlige ting at gøre i et sådant tilfælde er at udføre en venstre ydre sammenføjning mellem kunder og ordrer for at bevare kunder uden ordrer. En typisk fejl, når man konverterer den eksisterende løsning til en, der anvender joinforbindelsen, er at lade beregningen af ordreantallet være COUNT(*), som vist i følgende forespørgsel (kald det Query 1):
VÆLG C.custid, COUNT(*) AS antal, SUM(O.fragt) SOM totalfragt FRA Salg.Kunder SOM C VENSTRE YDRE JOIN Sales.Ordre SOM O P C.custid =O.custid GRUPPE AF C.custid BESTIL AF C.custid;Denne forespørgsel genererer følgende output:
depotnumre totalfragt ------- ---------- ------------ 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 s. 71, 48 88 9 19 19. før>Bemærk, at kunder 22 og 57 denne gang vises i resultatet, men deres ordreantal viser 1 i stedet for 0, fordi COUNT(*) tæller rækker og ikke ordrer. Den samlede fragt rapporteres korrekt, fordi SUM(fragt) ignorerer NULL-input.
Planen for denne forespørgsel er vist i figur 1.
Figur 1:Plan for forespørgsel 1
I denne plan repræsenterer Expr1002 antallet af rækker pr. gruppe, som som et resultat af den ydre sammenføjning oprindeligt er sat til NULL for kunder uden matchende ordrer. Compute Scalar-operatoren lige under rod SELECT-noden konverterer derefter NULL til 1. Det er resultatet af at tælle rækker i modsætning til at tælle ordrer.
For at rette denne fejl, vil du anvende COUNT-aggregatet til et element fra den ikke-bevarede side af den ydre joinforbindelse, og du vil sikre dig, at du bruger en kolonne, der ikke kan nulstilles som input. Den primære nøglekolonne ville være et godt valg. Her er løsningsforespørgslen (kald det Query 2) med fejlen rettet:
VÆLG C.custid, COUNT(O.orderid) AS nummorders, SUM(O.freight) AS totalfragt FRA Salg.Kunder SOM C VENSTRE YDRE JOIN Sales.Ordrer SOM O PÅ C.custid =O.custid GRUPPE AF C .custid BESTILLING AF C.custid;Her er outputtet af denne forespørgsel:
depotnumre totalfragt ------- ---------- ------------ 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 19 s. 71, 48 88 9 19 19. før>Bemærk, at denne gang viser kunder 22 og 57 det korrekte antal nul.
Planen for denne forespørgsel er vist i figur 2.
Figur 2:Plan for forespørgsel 2
Du kan også se ændringen i planen, hvor en NULL, der repræsenterer antallet for en kunde uden matchende ordrer, konverteres til 0 og ikke 1 denne gang.
Når du bruger joinforbindelser, skal du være forsigtig med at anvende COUNT(*)-aggregatet. Når du bruger ydre sammenføjninger, er det normalt en fejl. Den bedste praksis er at anvende COUNT-aggregatet på en kolonne, der ikke kan NULL, fra mange-siden af en-til-mange-sammenføjningen. Den primære nøglekolonne er et godt valg til dette formål, da den ikke tillader NULL'er. Dette kan være en god praksis, selv når du bruger indre sammenføjninger, da du aldrig ved, om du på et senere tidspunkt bliver nødt til at ændre en indre sammenføjning til en ydre på grund af ændringer i kravene.
Dobbeltdyppede aggregater
Vores anden fejl involverer også at blande joins og aggregater, denne gang tager kildeværdier i betragtning flere gange. Overvej følgende forespørgsel som et eksempel:
VÆLG C.custid, COUNT(O.orderid) AS nummorders, SUM(O.fragt) AS totalfragt, CAST(SUM(OD.kv. * OD.enhedspris * (1 - OD.rabat)) SOM NUMERISK(12) , 2)) SOM samlet FRA Salg.Kunder SOM C VENSTRE YDRE JOIN Sales.Ordrer SOM O PÅ C.custid =O.custid VENSTRE YDRE JOIN Sales.Ordredetaljer SOM OD PÅ O.orderid =OD.orderid GRUPPE VED C.custid ORDRE AF C.custid;Denne forespørgsel forbinder kunder, ordrer og ordredetaljer, grupperer rækkerne efter custid og formodes at beregne aggregater som ordreantal, samlet fragt og samlet værdi pr. kunde. Denne forespørgsel genererer følgende output:
depotnumre totalfragt i alt ------- ---------- -------------------- ---------- 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 47.32 27363.61 90Kan du se fejlen her?
Ordreoverskrifter gemmes i ordretabellen, og deres respektive ordrelinjer gemmes i tabellen Ordredetaljer. Når du forbinder ordreoverskrifter med deres respektive ordrelinjer, gentages overskriften i resultatet af sammenkædningen pr. linje. Som et resultat heraf afspejler COUNT(O.orderid)-aggregatet forkert antallet af ordrelinjer og ikke antallet af ordrer. På samme måde tager SUM(O.fragt) fejlagtigt hensyn til fragten flere gange pr. ordre – lige så mange som antallet af ordrelinjer i ordren. Den eneste korrekte aggregerede beregning i denne forespørgsel er den, der bruges til at beregne den samlede værdi, da den anvendes på attributter for ordrelinjerne:SUM(OD.qty * OD.unitprice * (1 – OD.discount).
For at få det korrekte ordreantal er det nok at bruge et særskilt antal aggregater:COUNT(DISTINCT O.orderid). Du tror måske, at den samme rettelse kan anvendes på beregningen af den samlede fragt, men dette ville kun introducere en ny fejl. Her er vores forespørgsel med distinkte aggregater anvendt på ordreheaderens mål:
SELECT C.custid, COUNT(DISTINCT O.orderid) AS nummorders, SUM(DISTINCT O.fraight) AS totalfragt, CAST(SUM(OD.kv. * OD.enhedspris * (1 - OD.rabat)) SOM NUMERISK (12, 2)) SOM samlet FRA Salg.Kunder SOM C VENSTRE YDRE JOIN Sales.Ordre SOM O PÅ C.custid =O.custid VENSTRE YDRE JOIN Sales.Ordredetaljer SOM OD PÅ O.orderid =OD.orderid GRUPPE AF C. custid BESTILLING AF C.custid;Denne forespørgsel genererer følgende output:
depotnumre totalfragt i alt ------- ---------- -------------------- ---------- 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 /pre>Ordreantallet er nu korrekt, men de samlede fragtværdier er det ikke. Kan du se den nye fejl?
Den nye fejl er mere uhåndgribelig, fordi den kun viser sig, når den samme kunde har mindst ét tilfælde, hvor flere ordrer tilfældigvis har nøjagtig samme fragtværdi. I et sådant tilfælde tager du nu kun fragten i betragtning én gang pr. kunde, og ikke én gang pr. ordre, som du burde.
Brug følgende forespørgsel (kræver SQL Server 2017 eller nyere) til at identificere utydelige fragtværdier for den samme kunde:
WITH C AS ( SELECT custid, fragt, STRING_AGG(CAST(orderid AS VARCHAR(MAX)), ', ') INDEN FOR GRUPPE(ORDER BY orderid) AS ordrer FRA Salg. Ordrer GRUPPER EFTER custid, fragt HAR COUNT(* )> 1 ) SELECT custid, STRING_AGG(CONCAT('(fragt:', fragt, ', ordrer:', ordrer, ')'), ', ') som dubletter FRA C GROUP BY custid;Denne forespørgsel genererer følgende output:
custid dubletter ------- -------------------------------------------- - 4 (fragt:23,72, ordrer:10743, 10953) 90 (fragt:0,75, ordrer:10615, 11005)Med disse resultater indser du, at forespørgslen med fejlen rapporterede forkerte samlede fragtværdier for kunder 4 og 90. Forespørgslen rapporterede korrekte samlede fragtværdier for resten af kunderne, da deres fragtværdier tilfældigvis var unikke.
For at rette fejlen skal du adskille beregningen af aggregater af ordrer og ordrelinjer til forskellige trin ved hjælp af tabeludtryk, som f.eks.:
WITH O AS ( SELECT custid, COUNT(orderid) AS nummorders, SUM(fragt) AS totalfragt FRA Salg.Ordre GRUPPER EFTER custid ), OD AS ( SELECT O.custid, CAST(SUM(OD.qty * OD. enhedspris * (1 - OD.rabat)) SOM NUMERISK(12, 2)) SOM samlet FRA Salg.Ordrer SOM O INNER JOIN Sales.Ordredetaljer SOM OD PÅ O.orderid =OD.orderid GRUPPE AF O.custid ) VÆLG C. custid, O.numorders, O.totalfreight, OD.totalval FRA Salg.Kunder SOM C VENSTRE YDRE JOIN O PÅ C.custid =O.custid VENSTRE YDRE JOIN OD PÅ C.custid =OD.custid BESTILLING AF C.custid;Denne forespørgsel genererer følgende output:
depotnumre totalfragt i alt ------- ---------- -------------------- ---------- 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.4 /pre>Bemærk, at de samlede fragtværdier for kunder 4 og 90 nu er højere. Dette er de rigtige tal.
Den bedste praksis her er at være opmærksom, når du forbinder og samler data. Du vil være opmærksom på sådanne tilfælde, når du forbinder flere tabeller og anvender aggregater til mål fra en tabel, der ikke er en kant- eller bladtabel i sammenkædningerne. I et sådant tilfælde skal du normalt anvende de samlede beregninger i tabeludtryk og derefter forbinde tabeludtrykkene.
Så fejlen med dobbelt-dypning af aggregater er rettet. Der er dog potentielt en anden fejl i denne forespørgsel. Kan du få øje på det? Jeg vil give detaljerne om sådan en potentiel fejl som det fjerde tilfælde, jeg vil dække senere under "YDRE-INDRE sammenføjningsmodsigelse."
ON-WHERE-modsigelse
Vores tredje fejl er et resultat af at forveksle de roller, som ON- og WHERE-sætningerne skal spille. Antag som et eksempel, at du fik en opgave med at matche kunder og ordrer, som de har afgivet siden 12. februar 2019, men også inkludere kunder, som ikke har afgivet ordrer siden da, i outputtet. Du forsøger at løse opgaven ved at bruge følgende forespørgsel (kald det forespørgsel 3):
VÆLG C.custid, C.companyname, O.orderid, O.orderdate FRA Salg.Kunder SOM C VENSTRE YDRE JOIN Sales.Orders AS O ON O.custid =C.custid HVOR O.orderdate>='20190212';Når du bruger en indre joinforbindelse, spiller både ON og WHERE de samme filtreringsroller, og derfor er det lige meget, hvordan du organiserer prædikaterne mellem disse klausuler. Men når du bruger en ydre sammenføjning som i vores tilfælde, har disse klausuler forskellige betydninger.
ON-klausulen spiller en matchende rolle, hvilket betyder, at alle rækker fra den bevarede side af joinforbindelsen (kunder i vores tilfælde) vil blive returneret. De, der har kampe baseret på ON-prædikatet, er forbundet med deres kampe, og som følge heraf gentages pr. kamp. De, der ikke har nogen matches, returneres med NULLs som pladsholdere i den ikke-bevarede sides attributter.
Omvendt spiller WHERE-klausulen en enklere filtrerende rolle - altid. Det betyder, at rækker, for hvilke filtreringsprædikatet evalueres til sand, returneres, og resten kasseres. Som et resultat kan nogle af rækkerne fra den bevarede side af sammenføjningen fjernes helt.
Husk at attributter fra den ikke-bevarede side af den ydre join (ordrer i vores tilfælde) er markeret som NULLs for ydre rækker (ikke-matches). Når du anvender et filter, der involverer et element fra den ikke-bevarede side af joinforbindelsen, evalueres filterprædikatet til ukendt for alle ydre rækker, hvilket resulterer i, at de fjernes. Dette er i overensstemmelse med den tre-værdiede prædikatlogik, som SQL følger. Som et resultat bliver sammenføjningen en indre sammenføjning. Den eneste undtagelse fra denne regel er, når du specifikt leder efter en NULL i et element fra den ikke-bevarede side for at identificere ikke-matches (element ER NULL).
Vores buggy-forespørgsel genererer følgende output:
custid virksomhedsnavn ordre-id ordredato ------- --------------- -------- ---------- 1 kunde NRZBB 11011 2019-04-09 1 Kunde NRZBB 10952 2019-03-16 2 Kunde MLTDN 10926 2019-03-04 4 Kunde HFBZG 11016 2019-04-10 5 Kunde 3-10 0HF 9 HF 10 3 HF 10 9 HF 9 HF 10 10 10 10 03 5 Kunde HGVLZ 10924 2019-03-04 6 Kunde XHXJV 11058 2019-04-29 6 Kunde XHXJV 10956 2019-03-17 8 Kunde QUHWH 10197 10197 10197 020 9 020 101970 1019 ... 02 02 9 02 02 0 02 0 2 0 0 2 0 Kunde THHDP 10968 2019-03-23 20 Kunde THHDP 10895 2019-02-18 24 Kunde CYZTN 11050 2019-04-27 24 Kunde CYZTN 11001 2019-0204 9-204 Kunde 9-204 ... påvirket)Det ønskede output formodes at have 213 rækker, herunder 195 rækker, der repræsenterer ordrer, der blev afgivet siden 12. februar 2019, og 18 yderligere rækker, der repræsenterer kunder, der ikke har afgivet ordrer siden da. Som du kan se, inkluderer det faktiske output ikke de kunder, der ikke har afgivet ordrer siden den angivne dato.
Planen for denne forespørgsel er vist i figur 3.
Figur 3:Plan for forespørgsel 3
Bemærk, at optimeringsværktøjet opdagede modsigelsen og internt konverterede den ydre sammenføjning til en indre forbindelse. Det er godt at se, men det er samtidig en klar indikation af, at der er en fejl i forespørgslen.
Jeg har set tilfælde, hvor folk forsøgte at rette fejlen ved at tilføje prædikatet OR O.orderid IS NULL til WHERE-sætningen, som sådan:
VÆLG C.custid, C.companyname, O.orderid, O.orderdate FRA Salg.Kunder SOM C VENSTRE YDRE JOIN Sales.Orders AS O ON O.custid =C.custid HVOR O.orderdate>='20190212' ELLER O.orderid ER NULL;Det eneste matchende prædikat er det, der sammenligner kunde-id'erne fra de to sider. Så selve joinforbindelsen returnerer kunder, der har afgivet ordrer generelt, sammen med deres matchende ordrer, såvel som kunder, der slet ikke har afgivet ordrer, med NULL i deres ordreattributter. Derefter prædikerer filtreringen filterkunder, der har afgivet ordrer siden den angivne dato, samt kunder, der slet ikke har afgivet ordrer (kunde 22 og 57). Forespørgslen mangler kunder, der har afgivet nogle ordrer, men ikke siden den angivne dato!
Denne forespørgsel genererer følgende output:
custid virksomhedsnavn ordre-id ordredato ------- --------------- -------- ---------- 1 kunde NRZBB 11011 2019-04-09 1 Kunde NRZBB 10952 2019-03-16 2 Kunde MLTDN 10926 2019-03-04 4 Kunde HFBZG 11016 2019-04-10 5 Kunde 3-10 0HF 9 HF 10 3 HF 10 9 HF 9 HF 10 10 10 10 03 5 Kunde HGVLZ 10924 2019-03-04 6 Kunde XHXJV 11058 2019-04-29 6 Kunde XHXJV 10956 2019-03-17 8 Kunde QUHWH 10197 10197 10197 020 9 020 101970 1019 ... 02 02 9 02 02 0 02 0 2 0 0 2 0 Kunde THHDP 10968 2019-03-23 20 Kunde THHDP 10895 2019-02-18 22 Kunde DTDMN NULL NULL 24 Kunde CYZTN 11050 2019-04-27 24 Kunde 1100Z 1100Z 1100Z 1100Z 1100Z 1100Z 1100Z 11050 .. (197 rækker påvirket)For at rette fejlen korrekt skal du bruge både prædikatet, der sammenligner kunde-id'erne fra de to sider, og det mod ordredatoen for at blive betragtet som matchende prædikater. For at opnå dette skal begge angives i ON-klausulen, som sådan (kald denne forespørgsel 4):
VÆLG C.custid, C.companyname, O.orderid, O.orderdate FRA Salg.Kunder SOM C VENSTRE YDRE JOIN Sales.Orders AS O ON O.custid =C.custid OG O.orderdate>='20190212';Denne forespørgsel genererer følgende output:
custid virksomhedsnavn ordre-id ordredato ------- --------------- -------- ---------- 1 kunde NRZBB 11011 2019-04-09 1 Kunde NRZBB 10952 2019-03-16 2 Kunde MLTDN 10926 2019-03-04 3 Kunde KBUDE NULL NULL 4 Kunde HFBZG 110916 4 Kunde 4-20ZG 110916 4 Kunde 4-20ZG 110916 4 Kunde 4-20ZG 10920 2019-03-03 5 Kunde HGVLZ 10924 2019-03-04 6 Kunde XHXJV 11058 2019-04-29 6 Kunde XHXJV 10956 2019-03-04 6 Kunde XHXJV 11058 2019-04-29. 20 Kunde THHDP 10979 2019-03-26 20 Kunde THHDP 10968 2019-03-23 20 Kunde THHDP 10895 2019-02-18 21 Kunde KIDPX NULL NULL 23 27 24 Kunde CYZTN 11001 2019-04-06 24 Kunde CYZTN 10993 2019-04-01 ... (213 rækker berørt)Planen for denne forespørgsel er vist i figur 4.
Figur 4:Plan for forespørgsel 4
Som du kan se, håndterede optimeringsværktøjet joinforbindelsen som en ydre joinforbindelse denne gang.
Dette er en meget enkel forespørgsel, som jeg brugte til illustrationsformål. Med meget mere omfattende og komplekse forespørgsler kan selv erfarne udviklere have svært ved at finde ud af, om et prædikat hører hjemme i ON-klausulen eller i WHERE-klausulen. Det, der gør det nemt for mig, er simpelthen at spørge mig selv, om prædikatet er et matchende prædikat eller et filtrerende prædikat. Hvis førstnævnte hører hjemme i ON-klausulen; hvis sidstnævnte hører hjemme i WHERE-sætningen.
YDRE-INDRE sammenføjningsmodsigelse
Vores fjerde og sidste fejl er på en måde en variation af den tredje fejl. Det sker typisk i multi-join-forespørgsler, hvor man blander join-typer. Antag som et eksempel, at du skal tilslutte dig tabellerne Kunder, Ordrer, Ordredetaljer, Produkter og Leverandører for at identificere kunde-leverandør-par, der havde fælles aktivitet. Du skriver følgende forespørgsel (kald det forespørgsel 5):
VÆLG DISTINCT C.custid, C.companyname AS kunde, S.supplierid, S.companyname AS leverandør FRA Sales.Kunder AS C INNER JOIN Sales.Ordre SOM O PÅ 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;Denne forespørgsel genererer følgende output med 1.236 rækker:
custid kunde leverandørid leverandør ------- -------- ----------- ---------- ----- 1 Kunde NRZBB 1 Leverandør SWRXU 1 Kunde NRZBB 3 Leverandør STUAZ 1 Kunde NRZBB 7 Leverandør GQRCV ... 21 Kunde KIDPX 24 Leverandør JNNES 21 Kunde KIDPX 25 Leverandør ERVYZ 25 Leverandør ERVYZ 21 Kunde Leverandør 2 WTUAZVFQVli Kunde 2 23 Kunde WVFAF 7 Leverandør GQRCV 23 Kunde WVFAF 8 Leverandør BWGYE ... 56 Kunde QNIVZ 26 Leverandør ZWZDM 56 Kunde QNIVZ 28 Leverandør OAVQT 56 Kunde QNIVZ 29 Leverandør OGLliAHX 58 Kundeleverandør 5HTLUAH 58 Kundeleverandør 1 QWUSF ... (1236 rækker påvirket)Planen for denne forespørgsel er vist i figur 5.
Figur 5:Plan for forespørgsel 5
Alle joinforbindelser i planen behandles som indre joinforbindelser, som du ville forvente.
Du kan også observere i planen, at optimizeren anvendte join-order-optimering. Med indre joinforbindelser ved optimeringsværktøjet, at det kan omarrangere den fysiske rækkefølge af joinforbindelserne på enhver måde, som det kan lide, samtidig med at den oprindelige forespørgsels betydning bevares, så det har en masse fleksibilitet. Her resulterede dens omkostningsbaserede optimering i ordren:join(Customers, join(Orders, join(join(Suppliers, Products), OrderDetails))).
Antag, at du får et krav om at ændre forespørgslen, så den inkluderer kunder, der ikke har afgivet ordrer. Husk på, at vi i øjeblikket har to sådanne kunder (med ID'er 22 og 57), så det ønskede resultat formodes at have 1.238 rækker. En almindelig fejl i et sådant tilfælde er at ændre den indre sammenføjning mellem kunder og ordrer til en venstre ydre sammenføjning, men at lade alle de øvrige sammenføjninger være indre, som sådan:
VÆLG DISTINCT C.custid, C.companyname AS kunde, S.supplierid, S.companyname AS leverandør FRA Salg.Kunder SOM C VENSTRE YDRE JOIN Sales.Ordre SOM O P O.custid =C.custid INNER JOIN Salg. 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 venstre ydre joinforbindelse efterfølgende efterfølges af indre eller højre ydre joinforbindelser, og join-prædikatet sammenligner noget fra den ikke-bevarede side af den venstre ydre joinforbindelse med et andet element, er resultatet af prædikatet den logiske værdi ukendt, og den oprindelige ydre joinforbindelse rækker kasseres. Den venstre ydre sammenføjning bliver i praksis en indre sammenføjning.
Som et resultat genererer denne forespørgsel det samme output som for forespørgsel 5, og returnerer kun 1.236 rækker. Også her registrerer optimeringsværktøjet modsigelsen og konverterer den ydre sammenføjning til en indre sammenføjning, hvilket genererer den samme plan vist tidligere i figur 5.
Et almindeligt forsøg på at rette fejlen er at få alle joins til at blive venstre ydre join, som sådan:
VÆLG DISTINCT C.custid, C.companyname AS kunde, S.supplierid, S.companyname AS leverandør FRA Salg.Kunder SOM C VENSTRE YDRE JOIN Sales.Ordre SOM O P O.custid =C.custid VENSTRE YDRE JOIN Salg .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;Denne forespørgsel genererer følgende output, som inkluderer kunder 22 og 57:
custid kunde leverandørid leverandør ------- -------- ----------- ---------- ----- 1 Kunde NRZBB 1 Leverandør SWRXU 1 Kunde NRZBB 3 Leverandør STUAZ 1 Kunde NRZBB 7 Leverandør GQRCV ... 21 Kunde KIDPX 24 Leverandør JNNES 21 Kunde KIDPX 25 Leverandør ERVYZ 2 DMKIX1 Kunde 2 NULLTN Kunde 2 NULLTN Kunde Kunde WVFAF 3 Leverandør STUAZ 23 Kunde WVFAF 7 Leverandør GQRCV 23 Kunde WVFAF 8 Leverandør BWGYE ... 56 Kunde QNIVZ 26 Leverandør ZWZDM 56 Kunde QNIVZ 28 Leverandør OAVQT 56 Kunde QNAXIVZ 8 Kunde 5 Kunde QNAXIVZ 5 WWRLHX Leverandør 5 Kunde AHXHT 5 Leverandør EQPNC 58 Kunde AHXHT 6 Leverandør QWUSF ... (1238 rows affe) cted)Der er dog to problemer med denne løsning. Antag, at du foruden Kunder kan have rækker i en anden tabel i forespørgslen uden matchende rækker i en efterfølgende tabel, og at du i et sådant tilfælde ikke ønsker at beholde de ydre rækker. Hvad nu hvis det i dit miljø var tilladt at oprette en header til en ordre, og på et senere tidspunkt udfylde den med ordrelinjer. Antag, at forespørgslen i et sådant tilfælde ikke skal returnere sådanne tomme ordreoverskrifter. Alligevel formodes forespørgslen at returnere kunder uden ordrer. Da sammenføjningen mellem ordrer og ordredetaljer er en venstre ydre sammenføjning, returnerer denne forespørgsel sådanne tomme ordrer, selvom den ikke burde.
Et andet problem er, at når du bruger outer joins, pålægger du flere begrænsninger for optimizeren i forhold til de omarrangeringer, som den har lov til at udforske som en del af dens join-order optimering. Optimizeren kan omarrangere join A LEFT OUTER JOIN B til B RIGHT OUTER JOIN A, men det er stort set den eneste omarrangering, det er tilladt at udforske. Med indre joinforbindelser kan optimeringsværktøjet også omarrangere tabeller ud over blot at vende sider, for eksempel kan det omorganisere join(join(join(join(A, B), C), D), E)))) for at join(A, join(B, join(join(E,D), C))) som vist tidligere i figur 5.
Hvis du tænker over det, er det, du virkelig leder efter, at venstre-join-kunder med resultatet af de indre sammenføjninger mellem resten af bordene. Det kan du naturligvis opnå med tabeludtryk. T-SQL understøtter dog et andet trick. Det, der virkelig bestemmer logisk join-rækkefølge, er ikke nøjagtig rækkefølgen af tabellerne i FROM-sætningen, snarere rækkefølgen af ON-sætningerne. Men for at forespørgslen skal være gyldig, skal hver ON-klausul vises lige under de to enheder, den forbinder. Så for at betragte sammenføjningen mellem kunder og resten som sidst, er det eneste, du skal gøre, at flytte ON-klausulen, der forbinder kunder og resten, så den vises sidst, sådan:
VÆLG DISTINCT C.custid, C.companyname AS kunde, S.supplierid, S.companyname AS leverandør FRA Salg.Kunder SOM C VENSTRE YDRE JOIN Sales.Orders AS O -- flyt herfra ------- ---------------- 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; -- <-- hertil --Nu er den logiske joinbestilling:leftjoin(kunder, join(join(join(ordrer, ordredetaljer), produkter), leverandører)). Denne gang vil du beholde kunder, der ikke har afgivet ordrer, men du vil ikke beholde ordreoverskrifter, der ikke har matchende ordrelinjer. Du tillader også optimeringsværktøjet fuld samordningsfleksibilitet i de indre forbindelser mellem ordrer, ordredetaljer, produkter og leverandører.
Den eneste ulempe ved denne syntaks er læsbarhed. Den gode nyhed er, at dette nemt kan rettes ved at bruge parenteser, som sådan (kald denne forespørgsel 6):
VÆLG DISTINCT C.custid, C.companyname AS kunde, S.supplierid, S.companyname AS leverandør FRA Sales.Customers AS C LEFT YDRE 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;Du må ikke forveksle brugen af parenteser her med en afledt tabel. Dette er ikke en afledt tabel, snarere en måde at adskille nogle af tabeloperatørerne til deres egen enhed for klarhedens skyld. Sproget har egentlig ikke brug for disse parenteser, men de anbefales kraftigt af hensyn til læsbarheden.
Planen for denne forespørgsel er vist i figur 6.
Figur 6:Plan for forespørgsel 6
Bemærk, at sammenføjningen mellem kunder og resten denne gang behandles som en ydre sammenføjning, og at optimeringsværktøjet anvendte sammenføjningsbestillingsoptimering.
Konklusion
I denne artikel dækkede jeg fire klassiske fejl relateret til joinforbindelser. Når du bruger outer joins, resulterer beregning af COUNT(*)-aggregatet typisk i en fejl. The best practice is to apply the aggregate to a non-NULLable column from the nonpreserved side of the join.
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.