Tento článek je třetím dílem série o chybách, úskalích a osvědčených postupech T-SQL. Dříve jsem se zabýval determinismem a poddotazy. Tentokrát se zaměřím na spoje. Některé z chyb a osvědčených postupů, které zde uvádím, jsou výsledkem průzkumu, který jsem provedl mezi ostatními MVP. Děkujeme Erlandu Sommarskogovi, Aaronovi Bertrandovi, Alejandro Mesovi, Umachandar Jayachandran (UC), Fabiano Neves Amorim, Miloš Radivojevič, Simon Sabin, Adam Machanic, Thomas Grohser, Chan Ming Man a Paul White za poskytnutí vašich postřehů!
Ve svých příkladech použiji ukázkovou databázi nazvanou TSQLV5. Skript, který vytváří a naplňuje tuto databázi, najdete zde a její ER diagram zde.
V tomto článku se zaměřím na čtyři klasické běžné chyby:COUNT(*) ve vnějších spojeních, dvojité ponoření agregátů, rozpor ON-WHERE a rozpor spojení OUTER-INNER. Všechny tyto chyby souvisejí se základy dotazování T-SQL a je snadné se jim vyhnout, pokud budete postupovat podle jednoduchých osvědčených postupů.
COUNT(*) ve vnějších spojeních
Naše první chyba souvisí s nesprávnými počty hlášenými pro prázdné skupiny v důsledku použití vnějšího spojení a agregace COUNT(*). Zvažte následující dotaz pro výpočet počtu objednávek a celkového přepravného na zákazníka:
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;
Tento dotaz generuje následující výstup (zkráceně):
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 14 1953,8 9,8 9 194,71 89 14 14 1953,8V tabulce Zákazníci je aktuálně přítomno 91 zákazníků, z toho 89 zadaných objednávek; výstup tohoto dotazu tedy ukazuje 89 skupin zákazníků a jejich správný počet objednávek a celkové souhrny nákladů. Zákazníci s ID 22 a 57 jsou přítomni v tabulce Zákazníci, ale nezadali žádné objednávky, a proto se ve výsledku nezobrazují.
Předpokládejme, že jste požádáni, abyste do výsledku dotazu zahrnuli zákazníky, kteří nemají žádné související objednávky. Přirozenou věcí v takovém případě je provést levé vnější spojení mezi zákazníky a objednávkami, aby se zachovali zákazníci bez objednávek. Typickou chybou při převodu stávajícího řešení na řešení, které používá spojení je však ponechání výpočtu počtu objednávek jako COUNT(*), jak je znázorněno v následujícím dotazu (nazývejte ho Dotaz 1):
SELECT C.custid, COUNT(*) AS cisla, 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 OBJEDNEJTE U C.custid;Tento dotaz generuje následující výstup:
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 ... 19 87 15 822,48 848 71 81907 07.07 848 71 819 před>Všimněte si, že zákazníci 22 a 57 se tentokrát objeví ve výsledku, ale jejich počet objednávek ukazuje 1 místo 0, protože COUNT(*) počítá řádky a ne objednávky. Celkové přepravné je hlášeno správně, protože SUM(přepravné) ignoruje NULL vstupy.
Plán pro tento dotaz je znázorněn na obrázku 1.
Obrázek 1:Plán pro dotaz 1
V tomto plánu Expr1002 představuje počet řádků na skupinu, který je v důsledku vnějšího spojení zpočátku nastaven na NULL pro zákazníky bez odpovídajících objednávek. Operátor Compute Scalar přímo pod kořenovým uzlem SELECT pak převede NULL na 1. To je výsledek počítání řádků na rozdíl od počítání objednávek.
Chcete-li tuto chybu opravit, chcete použít agregaci COUNT na prvek z nezachované strany vnějšího spojení a chcete se ujistit, že jako vstup použijete sloupec bez možnosti NULL. Sloupec primárního klíče by byl dobrou volbou. Zde je dotaz na řešení (nazývejte ho Dotaz 2) s opravenou chybou:
VYBERTE C.custid, COUNT(O.orderid) JAKO čísla, SUM(O.freight) JAKO celkové přepravné Z Sales.Customers AS C VLEVO VNĚJŠÍ PŘIPOJIT SE k Sales.Orders AS O ON C.custid =O.custid GROUP BY C .custid OBJEDNÁVKA U C.custid;Zde je výstup tohoto dotazu:
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 848 71 81907, 9 9, 707 837 9 819 před>Všimněte si, že tentokrát zákazníci 22 a 57 ukazují správný počet nuly.
Plán pro tento dotaz je znázorněn na obrázku 2.
Obrázek 2:Plán pro dotaz 2
Můžete také vidět změnu v plánu, kde NULL představující počet pro zákazníka bez odpovídajících objednávek je tentokrát převeden na 0 a nikoli na 1.
Při používání spojení buďte opatrní při použití agregace COUNT(*). Při použití vnějších spojení se obvykle jedná o chybu. Nejlepším postupem je použít agregaci COUNT na sloupec bez možnosti NULL z mnoha stran spojení typu one-to-many. Sloupec primárního klíče je pro tento účel dobrou volbou, protože nepovoluje hodnoty NULL. To by mohl být dobrý postup i při používání vnitřních spojení, protože nikdy nevíte, zda později nebudete muset změnit vnitřní spojení na vnější kvůli změně požadavků.
Dvojité máčení kameniva
Naše druhá chyba také zahrnuje míchání spojení a agregátů, tentokrát s vícenásobným zohledněním zdrojových hodnot. Jako příklad zvažte následující dotaz:
VYBERTE C.custid, COUNT(O.orderid) JAKO čísla, SUM(O.dopravné) JAKO celkové přepravné, CAST(SUM(OD.množství * VN.jednotková cena * (1 - VN.sleva)) JAKO ČÍSLO(12 , 2)) JAKO součet Z Prodeje.Zákazníci JAKO C VLEVO VNĚJŠÍ PŘIPOJIT Prodej.Objednávky JAKO O NA C.custid =O.custid VLEVO VNĚJŠÍ PŘIPOJIT Prodej.Podrobnosti objednávky JAKO OD NA O.orderid =OD.orderid SKUPINA PODLE C.custid OBJEDNÁVKA BY C.custid;Tento dotaz spojuje Customers, Orders and OrderDetails, seskupuje řádky podle zákazníka a má vypočítat agregáty, jako je počet objednávek, celkové přepravné a celková hodnota na zákazníka. Tento dotaz generuje následující výstup:
custid numorders celková hodnota přepravného ------- ---------- ------------- --------- 1 12 419,60 4273,00 2 10 306,59 1402,95 3 17 667,29 7023,98 4 30 1447,14 13390.65 52 4835,18 24927,58 ... 87 37 2611.93 15648,70 88 883333331616161.30 2 262616161.30, 262, 266161.30, 262, 266161,30, 262661.70, 2626161,30, 26661.70 88.Najdete tu chybu?
Záhlaví objednávek jsou uložena v tabulce Objednávky a jejich příslušné řádky objednávek jsou uloženy v tabulce Podrobnosti objednávky. Když spojíte záhlaví objednávky s příslušnými řádky objednávky, záhlaví se opakuje ve výsledku spojení na řádek. Výsledkem je, že agregace COUNT(O.orderid) nesprávně odráží počet řádků objednávky a nikoli počet objednávek. Podobně SUM(O.freight) nesprávně zohledňuje přepravné vícekrát na objednávku – tolik, kolik je počet řádků objednávky v objednávce. Jediný správný agregovaný výpočet v tomto dotazu je ten, který se používá k výpočtu celkové hodnoty, protože je aplikován na atributy řádků objednávky:SUM(OD.qty * OD.jednotková cena * (1 – OD.sleva).
Chcete-li získat správný počet objednávek, stačí použít samostatný souhrnný počet:COUNT(DISTINCT O.orderid). Možná si myslíte, že stejnou opravu lze použít na výpočet celkového nákladu, ale to by znamenalo pouze novou chybu. Zde je náš dotaz s odlišnými agregacemi použitými na míry záhlaví objednávky:
VYBERTE C.custid, COUNT(DISTINCT O.orderid) AS numorders, SUM(DISTINCT O.freight) AS total transport, CAST(SUMA(OD.qty *OD.jednotprice * (1 - OD.sleva)) AS NUMERIC (12, 2)) JAKO součet OD Prodeje.Zákazníci JAKO C VLEVO VNĚJŠÍ PŘIPOJIT Prodej.Objednávky JAKO O Z C.custid =O.custid VLEVO VNĚJŠÍ PŘIPOJIT Prodej.Podrobnosti objednávky JAKO OD Z O.orderid =OD.orderid SKUPINA BY C. custid OBJEDNÁVKA U C.custid;Tento dotaz generuje následující výstup:
custid numorders celková hodnota přepravného ------- ---------- ------------- --------- 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 1859,52 24927,58 ... 87 15 822,48 15648,70 88 9 194,71 60611111,3111.374 3161 90 7 87,66111.351.3611961.370. /před>Počty objednávek jsou nyní správné, ale celkové hodnoty přepravného nikoli. Dokážete najít novou chybu?
Nová chyba je obtížnější, protože se projevuje pouze v případě, že stejný zákazník má alespoň jeden případ, kdy více objednávek má přesně stejné hodnoty přepravného. V takovém případě nyní započítáváte dopravné pouze jednou za zákazníka a ne jednou za objednávku, jak byste měli.
Pomocí následujícího dotazu (vyžaduje SQL Server 2017 nebo vyšší) identifikujte nevýrazné hodnoty přepravného pro stejného zákazníka:
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('(doprava:', přepravné, ', objednávky:', objednávky, ')'), ', ') jako duplikáty FROM C GROUP BY custid;Tento dotaz generuje následující výstup:
duplikáty zákazníka ------- -------------------------------------- - 4 (náklad:23,72, objednávky:10743, 10953) 90 (náklad:0,75, objednávky:10615, 11005)S těmito zjištěními si uvědomíte, že dotaz s chybou hlásil nesprávné celkové hodnoty přepravného pro zákazníky 4 a 90. Dotaz hlásil správné hodnoty celkového přepravného pro ostatní zákazníky, protože jejich hodnoty přepravného byly náhodou jedinečné.
Chcete-li chybu opravit, musíte oddělit výpočet souhrnů objednávek a řádků objednávek do různých kroků pomocí tabulkových výrazů, například takto:
S O AS ( SELECT custid, COUNT(objednavka) AS cisla, SUM(doprava) JAKO celkove dopravne Z Sales.Orders GROUP BY custid ), OD AS ( SELECT O.custid, CAST(SUM(OD.qty * OD. jednotková cena * (1 - OD.sleva)) JAKO ČÍSELNÉ (12, 2)) JAKO součet Z Prodeje. Objednávky JAKO O VNITŘNÍ PŘIPOJENÍ Prodej.Podrobnosti objednávky JAKO OD NA O.orderid =OD.orderid SKUPINA PODLE O.custid ) VYBERTE C. custid, O.čísla, O.celkové přepravné, OD.celkem OD Prodeje.Zákazníci JAKO C VLEVO VNĚJŠÍ PŘIPOJENÍ O NA C.custid =O.custid VLEVO VNĚJŠÍ PŘIPOJENÍ OD NA C.custid =OD.custid OBJEDNÁVKA PODLE C.custid;Tento dotaz generuje následující výstup:
custid numorders celková hodnota přepravného ------- ---------- ------------- --------- 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 1859,52 24927,58 ... 87 15 822,48 15648,70 88 9 194,71 60611111.30 7 88,41 3161.374 31611.374 31611.374 3161. /před>Sledujte, že celkové hodnoty přepravného pro zákazníky 4 a 90 jsou nyní vyšší. Toto jsou správná čísla.
Osvědčeným postupem je být při spojování a agregaci dat opatrní. Chcete-li si dát pozor na takové případy při spojování více tabulek a aplikování agregací na míry z tabulky, která není okrajovou nebo listovou tabulkou ve spojeních. V takovém případě obvykle potřebujete použít agregované výpočty v rámci tabulkových výrazů a poté spojit tabulkové výrazy.
Takže chyba dvojitého namáčení agregátů je opravena. V tomto dotazu je však potenciálně další chyba. Dokážete to zjistit? Podrobnosti o takové potenciální chybě poskytnu jako čtvrtý případ, kterému se budu věnovat později v části „VNĚJŠÍ-VNITŘNÍ rozpor spojení.“
Rozpor ON-WHERE
Naše třetí chyba je výsledkem záměny rolí, které mají hrát klauzule ON a WHERE. Předpokládejme například, že jste dostali za úkol spárovat zákazníky a objednávky, které zadali od 12. února 2019, ale také zahrnout do výstupu zákazníky, kteří od té doby objednávky nezadali. Pokusíte se vyřešit úlohu pomocí následujícího dotazu (nazývejte jej Dotaz 3):
VYBERTE 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';Při použití vnitřního spojení hrají ON i WHERE stejné role filtrování, a proto nezáleží na tom, jak uspořádáte predikáty mezi těmito klauzulemi. Při použití vnějšího spojení jako v našem případě však mají tyto klauzule různé významy.
Klauzule ON hraje odpovídající roli, což znamená, že všechny řádky ze zachované strany spojení (v našem případě zákazníci) budou vráceny. Ty, které mají shody založené na predikátu ON, jsou spojeny se svými shodami a v důsledku toho se opakují na zápas. Ty, které nemají žádné shody, jsou vráceny s hodnotami NULL jako zástupnými symboly v atributech nezachované strany.
Naopak klauzule WHERE hraje jednodušší filtrovací roli – vždy. To znamená, že jsou vráceny řádky, pro které je predikát filtrování vyhodnocen jako pravdivý, a všechny ostatní jsou zahozeny. V důsledku toho mohou být některé řádky ze zachované strany spojení zcela odstraněny.
Pamatujte, že atributy z nezachované strany vnějšího spojení (v našem případě objednávky) jsou označeny jako NULL pro vnější řádky (neshodné). Kdykoli použijete filtr zahrnující prvek z nezachované strany spojení, predikát filtru se vyhodnotí jako neznámý pro všechny vnější řádky, což povede k jejich odstranění. To je v souladu s trojhodnotovou predikátovou logikou, kterou SQL následuje. V důsledku toho se spojení stane vnitřním spojením. Jedinou výjimkou z tohoto pravidla je situace, kdy konkrétně hledáte hodnotu NULL v prvku z nezachované strany, abyste identifikovali neshody (prvek JE NULL).
Náš chybný dotaz generuje následující výstup:
custid název společnosti orderid datum objednávky ------- --------------- -------- ---------- 1 zákazník NRZBB 11011 2019-04-09 1 Zákazník NRZBB 10952 2019-03-16 2 Zákazník MLTDN 10926 2019-03-04 4 Zákazník HFBZG 11016 2019-04-10 03-03 Zákazník HFZG69-401 2019BZG104 03 5 Zákazník HGVLZ 10924 2019-03-04 6 Zákazník XHXJV 11058 2019-04-29 6 Zákazník XHXJV 10956 2019-03-17 8 Zákazník QUHWH 10970 2P-2019 2019 Zákazník Zákazník THHDP 10968 2019-03-23 20 Zákazník THHDP 10895 2019-02-18 24 Zákazník CYZTN 11050 2019-04-27 24 Zákazník CYZTN 11001 11001 2019-04-016024CY Zákazník 2019-04-0106 postižené)Požadovaný výstup má mít 213 řádků včetně 195 řádků představujících objednávky, které byly zadány od 12. února 2019, a 18 dalších řádků představujících zákazníky, kteří od té doby žádné objednávky nezadali. Jak vidíte, skutečný výstup nezahrnuje zákazníky, kteří od zadaného data nezadali objednávku.
Plán pro tento dotaz je znázorněn na obrázku 3.
Obrázek 3:Plán pro dotaz 3
Všimněte si, že optimalizátor zjistil rozpor a interně převedl vnější spojení na vnitřní spojení. To je dobré vidět, ale zároveň je to jasným znamením, že v dotazu je chyba.
Viděl jsem případy, kdy se lidé pokusili opravit chybu přidáním predikátu OR O.orderid IS NULL do klauzule WHERE, například takto:
VYBERTE 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' NEBO O.orderid JE NULL;Jediný odpovídající predikát je ten, který porovnává ID zákazníků ze dvou stran. Samotné spojení tedy vrátí zákazníky, kteří zadali objednávky obecně, spolu s jejich odpovídajícími objednávkami, a také zákazníky, kteří objednávky nezadali vůbec, s hodnotami NULL v atributech objednávky. Potom predikáty filtrování filtrují zákazníky, kteří zadali objednávky od zadaného data, a také zákazníky, kteří žádné objednávky nezadali (zákazníci 22 a 57). V dotazu chybí zákazníci, kteří zadali nějaké objednávky, ale ne od zadaného data!
Tento dotaz generuje následující výstup:
custid název společnosti orderid datum objednávky ------- --------------- -------- ---------- 1 zákazník NRZBB 11011 2019-04-09 1 Zákazník NRZBB 10952 2019-03-16 2 Zákazník MLTDN 10926 2019-03-04 4 Zákazník HFBZG 11016 2019-04-10 03-03 Zákazník HFZG69-401 2019BZG104 03 5 Zákazník HGVLZ 10924 2019-03-04 6 Zákazník XHXJV 11058 2019-04-29 6 Zákazník XHXJV 10956 2019-03-17 8 Zákazník QUHWH 10970 2P-2019 2019 Zákazník Zákazník THHDP 10968 2019-03-23 20 Zákazník THHDP 10895 2019-02-18 22 Zákazník DTDMN NULL NULL 24 Zákazník CYZTN 11050 2019-04-27 24 Zákazník CY09-2401 Zákazník 11-0CY09-2401 11-04 Zákazník CY09-240101 .. (ovlivněno 197 řádků)Chcete-li chybu správně opravit, potřebujete jak predikát, který porovnává ID zákazníků ze dvou stran, tak i predikát s datem objednávky, aby byl považován za odpovídající predikáty. Abychom toho dosáhli, oba musí být specifikovány v klauzuli ON, jako je to (nazývejte tento dotaz 4):
VYBERTE 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';Tento dotaz generuje následující výstup:
custid název společnosti orderid datum objednávky ------- --------------- -------- ---------- 1 zákazník NRZBB 11011 2019-04-09 1 Zákazník NRZBB 10952 2019-03-16 2 Zákazník MLTDN 10926 2019-03-04 3 Zákazník KBUDE NULL NULL 4 Zákazník HFBZG 11016-1093-04 Zákazník 2019-04 2019-04GHF 10920 2019-03-03 5 Zákazník HGVLZ 10924 2019-03-04 6 Zákazník XHXJV 11058 2019-04-29 6 Zákazník XHXJV 10956 2019-03-17 2019-03-17 2019-03-17 2 Zákazník 19-03-17 7 Zákazník 19-03-17 2019 LL1X09-17 Zákazník 20 Zákazník THHDP 10979 2019-03-26 20 Zákazník THHDP 10968 2019-03-23 20 Zákazník THHDP 10895 2019-02-18 21 Zákazník KIDPX NULL NULL-26 WLLNUDMN 22 Zákazník DTNUDMN 22 Zákazník DTNUDMN 22 Zákazník DTNUDMN01- 27 24 Zákazník CYZTN 11001 2019-04-06 24 Zákazník CYZTN 10993 2019-04-01 ... (ovlivněno 213 řádků)Plán pro tento dotaz je znázorněn na obrázku 4.
Obrázek 4:Plán pro dotaz 4
Jak můžete vidět, optimalizátor tentokrát zpracoval spojení jako vnější spojení.
Toto je velmi jednoduchý dotaz, který jsem použil pro ilustrační účely. S mnohem propracovanějšími a složitějšími dotazy mohou mít i zkušení vývojáři problém zjistit, zda predikát patří do klauzule ON nebo do klauzule WHERE. To, co mi usnadňuje práci, je jednoduše si položit otázku, zda je predikát shodný predikát nebo filtrující. Pokud první, patří do klauzule ON; pokud je to druhé, patří do klauzule WHERE.
VNĚJŠÍ-VNITŘNÍ rozpor spojení
Naše čtvrtá a poslední chyba je svým způsobem variací na třetí chybu. Obvykle k tomu dochází u dotazů s více připojeními, kde mícháte typy spojení. Předpokládejme například, že potřebujete spojit tabulky Zákazníci, Objednávky, Podrobnosti objednávky, Produkty a Dodavatelé, abyste identifikovali dvojice zákazník-dodavatel, které měly společnou aktivitu. Napíšete následující dotaz (nazývejte ho Dotaz 5):
SELECT DISTINCT C.custid, C.companyname JAKO zákazník, S.supplierid, S.companyname AS dodavatel Z Prodeje.Zákazníci JAKO C VNITŘNÍ PŘIPOJIT Prodej.Objednávky JAKO O ON O.custid =C.custid VNITŘNÍ PŘIPOJIT Prodej.Podrobnosti objednávky AS OD ON OD.orderid =O.orderid INNER JOIN Production.Products AS P ON P.productid =OD.productid VNITŘNÍ JOIN Production.Dodavatelé AS S ON S.supplierid =P.supplierid;Tento dotaz generuje následující výstup s 1 236 řádky:
zákazník dodavatelid dodavatel ------- -------------- ----------- ---------- ----- 1 Zákazník NRZBB 1 Dodavatel SWRXU 1 Zákazník NRZBB 3 Dodavatel STUAZ 1 Zákazník NRZBB 7 Dodavatel GQRCV ... 21 Zákazník KIDPX 24 Dodavatel JNNES 21 Zákazník KIDPX 25 Dodavatel ERVYZ 21 Zákazník KIDPX 28 Dodavatel OAVQT 23 Zákazník 23 Odběratel WVFAF 7 Dodavatel GQRCV 23 Odběratel WVFAF 8 Dodavatel BWGYE ... 56 Odběratel QNIVZ 26 Dodavatel ZWZDM 56 Odběratel QNIVZ 28 Dodavatel OAVQT 56 Odběratel QNIVZ 29 Dodavatel OGLRK 58 Odběratel AHXHTP 1 Dodavatel SWRXU 58 Odběratel EHK Dodavatel SWRXU 58 Odběratel Dodavatel SWRXU 58 Odběratel QWUSF ... (ovlivněno 1236 řádků)Plán pro tento dotaz je znázorněn na obrázku 5.
Obrázek 5:Plán pro dotaz 5
Všechna spojení v plánu jsou zpracována jako vnitřní spojení, jak byste očekávali.
V plánu můžete také pozorovat, že optimalizátor použil optimalizaci seřazení. S vnitřními spojeními optimalizátor ví, že může změnit uspořádání fyzického pořadí spojení libovolným způsobem, přičemž zachová význam původního dotazu, takže má velkou flexibilitu. Zde jeho nákladová optimalizace vyústila v objednávku:join(Customers, join(Orders, join(join(Suppliers, Products), OrderDetails))).
Předpokládejme, že máte požadavek změnit dotaz tak, aby zahrnoval zákazníky, kteří nezadali objednávky. Připomeňme, že v současné době máme dva takové zákazníky (s ID 22 a 57), takže požadovaný výsledek má mít 1 238 řádků. Běžnou chybou v takovém případě je změna vnitřního spojení mezi zákazníky a objednávkami na levé vnější spojení, ale ponechání všech zbývajících spojení jako vnitřních, takto:
SELECT DISTINCT C.custid, C.companyname AS customer, S.supplierid, S.companyname AS dodavatel FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON O.custid =C.custid INNER JOIN Sales. OrderDetails JAKO OD NA OD.orderid =O.orderid INNER JOIN Production.Products AS P ON P.productid =OD.productid VNITŘNÍ JOIN Production.Dodavatelé AS S ON S.supplierid =P.supplierid;Když po levém vnějším spojení následně následují vnitřní nebo pravé vnější spojení a predikát spojení porovná něco z nezachované strany levého vnějšího spojení s nějakým jiným prvkem, výsledkem predikátu je logická hodnota neznámá a původní vnější spojení řádky jsou vyřazeny. Levé vnější spojení se efektivně stane vnitřním spojením.
Výsledkem je, že tento dotaz generuje stejný výstup jako pro dotaz 5 a vrací pouze 1 236 řádků. Také zde optimalizátor detekuje rozpor a převede vnější spojení na vnitřní spojení, čímž vytvoří stejný plán, jak je znázorněno dříve na obrázku 5.
Běžným pokusem o opravu chyby je provést všechna spojení levým vnějším spojením, například takto:
SELECT DISTINCT C.custid, C.companyname JAKO zákazník, S.supplierid, S.companyname JAKO dodavatel Z Prodeje.Zákazníci JAKO C LEFT OUTER JOIN Sales.Orders AS O ON O.custid =C.custid LEFT OUTER JOIN Sales .OrderDetails JAKO OD NA OD.orderid =O.orderid LEFT OUTTER JOIN Production.Products AS P ON P.productid =OD.productid LEFT OUTER JOIN Production.Suppliers AS ON S.supplierid =P.supplierid;Tento dotaz generuje následující výstup, který nezahrnuje zákazníky 22 a 57:
zákazník dodavatelid dodavatel ------- -------------- ----------- ---------- ----- 1 Zákazník NRZBB 1 Dodavatel SWRXU 1 Zákazník NRZBB 3 Dodavatel STUAZ 1 Zákazník NRZBB 7 Dodavatel GQRCV ... 21 Zákazník KIDPX 24 Dodavatel JNNES 21 Zákazník KIDPX 25 Dodavatel ERVYZ 21 Zákazník KIDPX 28 Dodavatel 2DTLLNUT 22 Zákazník Zákazník WVFAF 3 Dodavatel STUAZ 23 Zákazník WVFAF 7 Dodavatel GQRCV 23 Zákazník WVFAF 8 Dodavatel BWGYE ... 56 Zákazník QNIVZ 26 Dodavatel ZWZDM 56 Zákazník QNIVZ 28 Dodavatel OAVQT 56 Zákazník QNIVZ 29 Dodavatel OGLRK 57 Zákazník SWVAXS Dodavatel NU Zákazník AHXHT 5 Dodavatel EQPNC 58 Zákazník AHXHT 6 Dodavatel QWUSF ... (1238 řádků affe cted)Toto řešení má však dva problémy. Předpokládejme, že kromě Customers můžete mít v dotazu řádky v jiné tabulce bez odpovídajících řádků v následující tabulce a že v takovém případě nechcete tyto vnější řádky ponechat. Co kdyby bylo například ve vašem prostředí povoleno vytvořit záhlaví pro objednávku a později ji vyplnit řádky objednávky. Předpokládejme, že v takovém případě by dotaz neměl vracet taková prázdná záhlaví objednávky. Přesto má dotaz vracet zákazníky bez objednávek. Protože spojení mezi Orders a OrderDetails je levé vnější spojení, tento dotaz vrátí takové prázdné objednávky, i když by neměl.
Dalším problémem je, že při použití vnějších spojení uvalujete na optimalizátor více omezení, pokud jde o přeuspořádání, která může prozkoumat v rámci optimalizace řazení spojení. Optimalizátor může změnit uspořádání spojení A LEFT OUTER JOIN B na B RIGHT OUTER JOIN A, ale to je v podstatě jediné přeuspořádání, které může prozkoumat. Díky vnitřním spojením může optimalizátor také změnit pořadí tabulek, než je pouhé převrácení stran, například může změnit pořadí join(join(join(join(A, B), C), D), E)))) a join(A, join(B, join(join(E, D), C))), jak je znázorněno dříve na obrázku 5.
Pokud se nad tím zamyslíte, ve skutečnosti vám jde o to, abyste spojili zákazníky vlevo s výsledkem vnitřních spojení mezi zbytkem tabulek. Je zřejmé, že toho můžete dosáhnout pomocí tabulkových výrazů. T-SQL však podporuje další trik. To, co skutečně určuje pořadí logického spojení, není přesně pořadí tabulek v klauzuli FROM, ale spíše pořadí klauzulí ON. Aby však byl dotaz platný, musí se každá klauzule ON objevit přímo pod dvěma jednotkami, které spojuje. Chcete-li tedy považovat spojení mezi zákazníky a ostatními za poslední, vše, co musíte udělat, je přesunout klauzuli ON, která spojuje zákazníky a ostatní, aby se zobrazovala jako poslední, například takto:
VYBRAT DISTINCT C.custid, C.companyname JAKO zákazník, S.supplierid, S.companyname JAKO dodavatel OD Sales.Customers JAKO C LEFT OUTER JOIN Sales.Orders AS O -- přesun odsud ------- ---------------- 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; -- <-- sem --Nyní je logické řazení spojení:leftjoin(Zákazníci, join(join(join(Objednávky, Podrobnosti objednávky), Produkty), Dodavatelé)). Tentokrát si ponecháte zákazníky, kteří nezadali objednávky, ale neuchováte hlavičky objednávek, které nemají odpovídající řádky objednávky. Optimalizátoru také umožníte plnou flexibilitu objednávání spojení ve vnitřních spojeních mezi objednávkami, podrobnostmi objednávek, produkty a dodavateli.
Jedinou nevýhodou této syntaxe je čitelnost. Dobrou zprávou je, že to lze snadno opravit pomocí závorek, jako je to (nazývejte tento dotaz 6):
SELECT DISTINCT C.custid, C.companyname AS customer, S.supplierid, S.companyname AS dodavatel 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.Dodavatelé AS S ON S.supplierid =P.supplierid ) ON O.custid =C.custid;Nepleťte si zde použití závorek s odvozenou tabulkou. Toto není odvozená tabulka, spíše jen způsob, jak pro přehlednost oddělit některé operátory tabulky do jejich vlastní jednotky. Jazyk tyto závorky ve skutečnosti nepotřebuje, ale důrazně je doporučujeme kvůli čitelnosti.
Plán pro tento dotaz je znázorněn na obrázku 6.
Obrázek 6:Plán pro dotaz 6
Všimněte si, že tentokrát je spojení mezi zákazníky a ostatními zpracováno jako vnější spojení a že optimalizátor použil optimalizaci řazení spojení.
Závěr
V tomto článku jsem se zabýval čtyřmi klasickými chybami souvisejícími s připojeními. Při použití vnějších spojení má výpočet agregace COUNT(*) obvykle za následek chybu. 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.