Database
 sql >> база данни >  >> RDS >> Database

T-SQL грешки, клопки и най-добри практики – присъединява се

Тази статия е третата част от поредицата за грешки в T-SQL, клопки и най-добри практики. По-рано разглеждах детерминизма и подзаявките. Този път се фокусирам върху съединенията. Някои от грешките и най-добрите практики, които разглеждам тук, са резултат от проучване, което направих сред колеги MVP. Благодаря на Ерланд Сомарско, Арън Бертран, Алехандро Меса, Умачандар Джаячандран (UC), Фабиано Невес Аморим, Милош Радивоевич, Саймън Сабин, Адам Мачаник, Томас Гросер, Чан Минг Ман и Пол Уайт за предлагането на вашите прозрения!

В моите примери ще използвам примерна база данни, наречена TSQLV5. Тук можете да намерите скрипта, който създава и попълва тази база данни, както и нейната диаграма за ER тук.

В тази статия се фокусирам върху четири класически често срещани грешки:COUNT(*) във външни съединения, двойно потапяне на агрегати, противоречие ON-WHERE и противоречие на OUTER-INNER присъединяване. Всички тези грешки са свързани с основите на T-SQL заявките и са лесни за избягване, ако следвате прости най-добри практики.

COUNT(*) във външни присъединявания

Първата ни грешка е свързана с неправилни отчети за празни групи в резултат на използване на външно присъединяване и COUNT(*) агрегат. Помислете за следната заявка за изчисляване на броя на поръчките и общия товар на клиент:

 ИЗПОЛЗВАЙТЕ 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 1390. 

В момента в таблицата Клиенти има 91 клиенти, от които 89 са направили поръчки; следователно изходът от тази заявка показва 89 групи клиенти и техния правилен брой поръчки и общи агрегати на товари. Клиенти с ID 22 и 57 присъстват в таблицата Клиенти, но не са направили никакви поръчки и следователно не се показват в резултата.

Да предположим, че от вас се иска да включите клиенти, които нямат свързани поръчки в резултата от заявката. Естественото нещо, което трябва да се направи в такъв случай, е да се извърши ляво външно свързване между клиенти и поръчки, за да се запазят клиентите без поръчки. Въпреки това, типична грешка при преобразуването на съществуващото решение в такова, което прилага присъединяването е да оставите изчисляването на броя на поръчките като COUNT(*), както е показано в следната заявка (наречете го Query 1):

 ИЗБЕРЕТЕ 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 ПОРЪЧАЙТЕ ОТ 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 84 719. предварително> 

Забележете, че клиентите 22 и 57 този път се появяват в резултата, но броят им на поръчките показва 1 вместо 0, тъй като COUNT(*) брои редове, а не поръчки. Общият товар се отчита правилно, тъй като SUM(това) игнорира NULL входове.

Планът за тази заявка е показан на фигура 1.

Фигура 1:План за заявка 1

В този план Expr1002 представлява броят на редовете на група, който в резултат на външното присъединяване първоначално е зададен на NULL за клиенти без съвпадащи поръчки. Операторът Compute Scalar точно под основния SELECT възел след това преобразува NULL в 1. Това е резултатът от преброяване на редове, за разлика от преброяването на поръчките.

За да коригирате тази грешка, искате да приложите агрегата COUNT към елемент от незапазената страна на външното съединение и искате да се уверите, че използвате колона без NULL като вход. Колоната с първичен ключ би била добър избор. Ето заявката за решение (наречете я Заявка 2) с отстранена грешка:

 ИЗБЕРЕТЕ C.custid, COUNT(O.orderid) КАТО номера, SUM(O.freight) КАТО общ товар ОТ Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON C.custid =O.custid GROUP BY C .custid ПОРЪЧАЙТЕ ОТ 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 84 719. предварително> 

Обърнете внимание, че този път клиенти 22 и 57 показват правилния брой нула.

Планът за тази заявка е показан на Фигура 2.

Фигура 2:План за заявка 2

Можете също да видите промяната в плана, където NULL, представляваща броя за клиент без съвпадащи поръчки, се преобразува в 0, а не в 1 този път.

Когато използвате обединявания, внимавайте с прилагането на агрегата COUNT(*). Когато използвате външни съединения, обикновено това е грешка. Най-добрата практика е да приложите агрегата COUNT към колона, която не може да бъде NULL, от много страни на съединението един към много. Колоната с първичен ключ е добър избор за тази цел, тъй като не позволява NULL. Това може да е добра практика дори при използване на вътрешни съединения, тъй като никога не знаете дали в по-късен момент ще трябва да промените вътрешно присъединяване с външно поради промяна в изискванията.

Агрегати с двойно потапяне

Втората ни грешка също включва смесване на обединения и агрегати, като този път се вземат предвид стойностите на източника многократно. Разгледайте следната заявка като пример:

 ИЗБЕРЕТЕ C.custid, COUNT(O.orderid) КАТО номера, SUM(O.freight) КАТО общ товар, CAST(SUM(OD.qty * OD.unitprice * (1 - OD.discount)) КАТО 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 КАТО OD ON O.orderid =OD.orderid ГРУПА BY C.custid ORDER ОТ C.custid;

Тази заявка обединява Customers, Orders и OrderDetails, групира редовете по 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 4017. 

Можете ли да забележите грешката тук?

Заглавките на поръчките се съхраняват в таблицата Orders, а съответните им редове за поръчки се съхраняват в таблицата OrderDetails. Когато обедините заглавията на поръчката със съответните им редове за поръчка, заглавката се повтаря в резултата от обединяването на ред. В резултат на това агрегатът COUNT(O.orderid) неправилно отразява броя на редовете за поръчки, а не броя на поръчките. По същия начин, SUM(O.freight) неправилно отчита навлото няколко пъти на поръчка – толкова, колкото е броят на редовете за поръчка в рамките на поръчката. Единственото правилно обобщено изчисление в тази заявка е това, което се използва за изчисляване на общата стойност, тъй като се прилага към атрибутите на редовете за поръчка:SUM(OD.qty * OD.unitprice * (1 – OD.discount).

За да получите правилния брой поръчки, достатъчно е да използвате отделен агрегат за броене:COUNT(DISTINCT O.orderid). Може да си помислите, че същата корекция може да се приложи към изчисляването на общия товар, но това само би въведе нова грешка. Ето нашата заявка с различни агрегати, приложени към мерките на заглавката на поръчката:

 ИЗБЕРЕТЕ C.custid, COUNT(DISTINCT O.orderid) КАТО номера, SUM(DISTINCT O.freight) КАТО общ товар, CAST(SUM(OD.кол * OD.единична цена * (1 - OD.отстъпка)) КАТО ЦИФРИ (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 КАТО OD ON O.orderid =OD.orderid GROUP BY C. custid ПОРЪЧКА ОТ 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 или по-нова версия), за да идентифицирате неразличими стойности на товари за същия клиент:

 С C AS ( ИЗБЕРЕТЕ custid, freight, STRING_AGG(CAST(orderid AS VARCHAR(MAX)), ', ') WITHIN GROUP(ORDER BY orderid) КАТО поръчки ОТ Продажби. Поръчки ГРУПА ПО custid, freight HAVING COUNT(* )> 1 ) ИЗБЕРЕТЕ custid, STRING_AGG(CONCAT('(freight:', freight, ', orders:', orders, ')'), ', ') като дубликати ОТ C GROUP BY custid;

Тази заявка генерира следния изход:

 custid дубликати ------- ------------------------------------- - 4 (товар:23,72, поръчки:10743, 10953) 90 (товар:0,75, поръчки:10615, 11005)

С тези констатации разбирате, че заявката с грешката отчита неправилни общи стойности на товари за клиенти 4 и 90. Заявката отчита правилни общи стойности на товари за останалите клиенти, тъй като техните стойности на товари са били уникални.

За да коригирате грешката, трябва да разделите изчисляването на агрегати от поръчки и редове за поръчки на различни стъпки, като използвате изрази на таблицата, както следва:

 С 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. единична цена * (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 ) ИЗБЕРЕТЕ C. custid, O.numorders, O.totalfreight, OD.totalval ОТ 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

Третата ни грешка е резултат от объркването на ролите, които клаузите ON и WHERE трябва да играят. Като пример да предположим, че ви е дадена задача да съпоставите клиенти и поръчки, които са направили от 12 февруари 2019 г., но също така да включите в изхода клиенти, които не са направили поръчки оттогава. Опитвате се да решите задачата, като използвате следната заявка (наречете я Заявка 3):

 ИЗБЕРЕТЕ C.custid, C.companyname, O.orderid, O.orderdate ОТ Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON O.custid =C.custid WHERE O.orderdate>='20190212';

Когато използвате вътрешно присъединяване, ON и WHERE играят едни и същи филтриращи роли и следователно няма значение как организирате предикатите между тези клаузи. Въпреки това, когато използвате външно присъединяване, както в нашия случай, тези клаузи имат различни значения.

Клаузата ON играе съвпадаща роля, което означава, че всички редове от запазената страна на съединението (Клиентите в нашия случай) ще бъдат върнати. Тези, които имат съвпадения въз основа на предиката ON, са свързани с техните съвпадения и в резултат на това се повтарят на съвпадение. Тези, които нямат съвпадения, се връщат с NULL като заместители в атрибутите на незапазената страна.

Обратно, клаузата WHERE играе по-проста филтрираща роля – винаги. Това означава, че редовете, за които предикатът за филтриране се оценява на true, се връщат, а всички останали се отхвърлят. В резултат на това някои от редовете от запазената страна на съединението могат да бъдат премахнати напълно.

Не забравяйте, че атрибутите от незапазената страна на външното съединение (поръчки в нашия случай) са маркирани като NULL за външните редове (несъответствия). Всеки път, когато приложите филтър, включващ елемент от незапазената страна на съединението, предикатът на филтъра се оценява на неизвестен за всички външни редове, което води до тяхното премахване. Това е в съответствие с тризначната предикатна логика, която SQL следва. В резултат на това съединението се превръща във вътрешно съединение. Единственото изключение от това правило е, когато специално търсите NULL в елемент от незапазената страна, за да идентифицирате несъвпадения (елементът Е NULL).

Нашата бъги заявка генерира следния изход:

 custid companyname orderid orderdate ------- --------------- -------- ---------- 1 Клиент NRZBB 11011 2019-04-09 1 Клиент NRZBB 10952 2019-03-16 2 Клиент MLTDN 10926 2019-03-04 4 Клиент HFBZG 11016 2019-04-10 4 2019-04-10 4 2019-04-10 4 Customer 2019-03-16 2 Клиент MLTDN 10926 2019-03-04 4 Клиент HFBZG 11016 2019-04-10 4 2019-04-10 4 2019-04-10 4 2019-04-10 4 Customer 2019-03-16 2019-03-16 Клиент 03 5 Клиент HGVLZ 10924 2019-03-04 6 Клиент XHXJV 11058 2019-04-29 6 Клиент XHXJV 10956 2019-03-17 8 Клиент QUHWH 101970 DP 10970 10970 20 1970 10970 Клиент THHDP 10968 2019-03-23 ​​20 Клиент THHDP 10895 2019-02-18 24 Клиент CYZTN 11050 2019-04-27 24 Клиент CYZTN 11001-11001 4-NZ 2019 19 19 1909 засегнати)

Желаният изход трябва да има 213 реда, включително 195 реда, представляващи поръчки, направени от 12 февруари 2019 г., и 18 допълнителни реда, представляващи клиенти, които не са направили поръчки оттогава. Както можете да видите, действителната продукция не включва клиентите, които не са направили поръчки след определената дата.

Планът за тази заявка е показан на Фигура 3.

Фигура 3:План за заявка 3

Забележете, че оптимизаторът открива противоречието и вътрешно преобразува външното съединение във вътрешно съединение. Това е добре да се види, но в същото време е ясна индикация, че има грешка в заявката.

Виждал съм случаи, в които хората се опитват да коригират грешката, като добавят предиката OR O.orderid IS NULL към клаузата WHERE, като така:

 ИЗБЕРЕТЕ C.custid, C.companyname, O.orderid, O.orderdate ОТ Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON O.custid =C.custid WHERE O.orderdate>='20190212' ИЛИ O.orderid Е NULL;

Единственият съвпадащ предикат е този, който сравнява идентификаторите на клиенти от двете страни. Така самото присъединяване връща клиенти, които са направили поръчки като цяло, заедно с техните съответстващи поръчки, както и клиенти, които изобщо не са направили поръчки, с NULL в атрибутите на поръчката си. След това предикати за филтриране филтрират клиенти, които са направили поръчки след определената дата, както и клиенти, които изобщо не са направили поръчки (клиенти 22 и 57). В заявката липсват клиенти, които са направили някои поръчки, но не от посочената дата!

Тази заявка генерира следния изход:

 custid companyname orderid orderdate ------- --------------- -------- ---------- 1 Клиент NRZBB 11011 2019-04-09 1 Клиент NRZBB 10952 2019-03-16 2 Клиент MLTDN 10926 2019-03-04 4 Клиент HFBZG 11016 2019-04-10 4 2019-04-10 4 2019-04-10 4 Customer 2019-03-16 2 Клиент MLTDN 10926 2019-03-04 4 Клиент HFBZG 11016 2019-04-10 4 2019-04-10 4 2019-04-10 4 2019-04-10 4 Customer 2019-03-16 2019-03-16 Клиент 03 5 Клиент HGVLZ 10924 2019-03-04 6 Клиент XHXJV 11058 2019-04-29 6 Клиент XHXJV 10956 2019-03-17 8 Клиент QUHWH 101970 DP 10970 10970 20 1970 10970 Клиент THHDP 10968 2019-03-23 ​​20 Клиент THHDP 10895 2019-02-18 22 Клиент DTDMN NULL NULL 24 Клиент CYZTN 11050 2019-04-27 2019-04-27 2019-04-27 2019-04-27 2019-04-27 2019-04-27 2019-04-27 2019-04-27 2019-04-27 2019-04-27 2019-04-27 2019-04-27 2019 THHDP 10895 2019-02-18. .. (засегнати 197 реда)

За да коригирате правилно грешката, трябва както предиката, който сравнява идентификаторите на клиенти от двете страни, така и този срещу датата на поръчката, за да се счита за съвпадащи предикати. За да постигнете това, и двете трябва да бъдат посочени в клаузата ON, като така (наречете тази заявка 4):

 ИЗБЕРЕТЕ C.custid, C.companyname, O.orderid, O.orderdate ОТ Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON O.custid =C.custid И O.orderdate>='20190212';

Тази заявка генерира следния изход:

 custid companyname orderid orderdate ------- --------------- -------- ---------- 1 Клиент NRZBB 11011 2019-04-09 1 Клиент NRZBB 10952 2019-03-16 2 Клиент MLTDN 10926 2019-03-04 3 Клиент KBUDE NULL NULL 4 Клиент HFBZG 11016-04GF0 11016-04GF0 11016-04GF0 11016-04GF00104GF01 10920 2019-03-03 5 Клиент HGVLZ 10924 2019-03-04 6 Клиент XHXJV 11058 2019-04-29 6 Клиент XHXJV 10956 2019-03-17 NULL 03-17 NULL0 NULL0 NULL0 NULL0 20 Клиент THHDP 10979 2019-03-26 20 Клиент THHDP 10968 2019-03-23 ​​20 Клиент THHDP 10895 2019-02-18 21 Клиент KIDPX NULL NULL NULL NULL 2 NULL0 NULL NULL NULL NULL NULL NULL NULL 2 NULL2 27 24 Клиент CYZTN 11001 2019-04-06 24 Клиент CYZTN 10993 01.04.2019 ... (засегнати 213 реда)

Планът за тази заявка е показан на Фигура 4.

Фигура 4:План за заявка 4

Както можете да видите, този път оптимизаторът обработва присъединяването като външно.

Това е много проста заявка, която използвах за илюстративни цели. С много по-сложни и сложни заявки, дори опитните разработчици могат да имат трудности да разберат дали даден предикат принадлежи към клаузата ON или в клаузата WHERE. Това, което прави нещата лесни за мен, е просто да се запитам дали предикатът е съвпадащ предикат или филтриращ. Ако първото, то принадлежи в клаузата ON; ако последното, то принадлежи на клаузата WHERE.

ВЪНШНО-ВЪТРЕШНО присъединително противоречие

Нашата четвърта и последна грешка е в известен смисъл вариант на третата грешка. Обикновено се случва в заявки за многообединение, където смесвате типове присъединяване. Като пример да предположим, че трябва да се присъедините към таблиците Клиенти, Поръчки, Подробности за поръчката, Продукти и Доставчици, за да идентифицирате двойки клиент-доставчик, които са имали съвместна дейност. Пишете следната заявка (наречете я Заявка 5):

 ИЗБЕРЕТЕ DISTINCT C.custid, C.companyname КАТО клиент, S.supplierid, S.companyname КАТО доставчик ОТ 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 Производство.Продукти AS P ON P.productid =OD.productid INNER JOIN Производство.Доставчици AS S ON S.supplierid =P.supplierid;

Тази заявка генерира следния изход с 1236 реда:

 custid client spplieid доставчик ------- --------------- ----------- ---------- ----- 1 Клиент NRZBB 1 Доставчик SWRXU 1 Клиент NRZBB 3 Доставчик STUAZ 1 Клиент NRZBB 7 Доставчик GQRCV ... 21 Клиент KIDPX 24 Доставчик JNNES 21 Клиент KIDPX 25 Доставчик ERVYZ 21 Клиент WFX 2 Доставчик STUAZ 21 Клиент NRZBB 7 Доставчик 23 Клиент WVFAF 7 Доставчик GQRCV 23 Клиент WVFAF 8 Доставчик BWGYE ... 56 Клиент QNIVZ 26 Доставчик ZWZDM 56 Клиент QNIVZ 28 Доставчик OAVQT 56 Клиент QNIVZ 29 Доставчик Клиент Доставчик EGLRKXAHXHT58P Доставчик EGLRKXAHXHT58Ppli QWUSF ... (засегнати 1236 реда)

Планът за тази заявка е показан на Фигура 5.

Фигура 5:План за заявка 5

Всички съединения в плана се обработват като вътрешни съединения, както бихте очаквали.

Можете също да забележите в плана, че оптимизаторът е приложил оптимизация за подреждане на присъединяване. С вътрешните присъединявания оптимизаторът знае, че може да пренареди физическия ред на присъединяванията по всякакъв начин, който му харесва, като същевременно запазва значението на оригиналната заявка, така че има голяма гъвкавост. Тук нейната оптимизация на базата на разходите доведе до поръчката:join(Customers, join(Orders, join(join(Доставчици, Продукти), OrderDetails))).

Да предположим, че получавате изискване да промените заявката, така че да включва клиенти, които не са направили поръчки. Припомнете си, че в момента имаме двама такива клиента (с ID 22 и 57), така че желаният резултат трябва да има 1238 реда. Често срещана грешка в такъв случай е да се промени вътрешното свързване между клиенти и поръчки на ляво външно присъединяване, но да се оставят всички останали съединения като вътрешни, както следва:

 ИЗБЕРЕТЕ DISTINCT C.custid, C.companyname КАТО клиент, S.supplierid, S.companyname КАТО доставчик ОТ Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON O.custid =C.custid INNER JOIN Sales. Подробности за поръчката AS OD ON OD.orderid =O.orderid INNER JOIN Производство.Продукти AS P ON P.productid =OD.productid INNER JOIN Производство.Доставчици AS S ON S.supplierid =P.supplierid;

Когато лявото външно съединение впоследствие е последвано от вътрешни или дясно външни съединения и предикатът за свързване сравнява нещо от незапазената страна на лявото външно съединение с някакъв друг елемент, резултатът от предиката е неизвестната логическа стойност, а оригиналният външен редовете се отхвърлят. Лявото външно съединение ефективно се превръща във вътрешно съединение.

В резултат на това тази заявка генерира същия изход като за заявка 5, връщайки само 1236 реда. Също така тук оптимизаторът открива противоречието и преобразува външното съединение във вътрешно съединение, генерирайки същия план, показан по-рано на Фигура 5.

Често срещан опит да се поправи грешката е да се направят всички присъединявания отляво външно съединение, като така:

 ИЗБЕРЕТЕ DISTINCT C.custid, C.companyname КАТО клиент, S.supplierid, S.companyname КАТО доставчик ОТ Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON O.custid =C.custid LEFT OUTER JOIN Продажби .OrderDetails AS OD ON OD.orderid =O.orderid LEFT OUTER JOIN Производство.Продукти AS P ON P.productid =OD.productid LEFT OUTER JOIN Производство.Доставчици AS S ON S.supplierid =P.supplierid;

Тази заявка генерира следния изход, който включва клиенти 22 и 57:

 custid client spplieid доставчик ------- --------------- ----------- ---------- ----- 1 Клиент NRZBB 1 Доставчик SWRXU 1 Клиент NRZBB 3 Доставчик STUAZ 1 Клиент NRZBB 7 Доставчик GQRCV ... 21 Клиент KIDPX 24 Доставчик JNNES 21 Клиент KIDPX 25 Доставчик ERVYZ 21 Клиент 21 Клиент NRZBB 7 Доставчик GQRCV... Клиент WVFAF 3 Доставчик STUAZ 23 Клиент WVFAF 7 Доставчик GQRCV 23 Клиент WVFAF 8 Доставчик BWGYE ... 56 Клиент QNIVZ 26 Доставчик ZWZDM 56 Клиент QNIVZ 28 Доставчик OAVQT 56 Доставчик Клиент OAVQT 56 56 Клиент WVFAF 8 Доставчик BWGYE ... 56 Клиент QNIVZ 26 Доставчик ZWZDM 56 Клиент QNIVZ 28 Доставчик OAVQT 56 Доставчик 56 Клиент WVFAF 8 Доставчик BWGYE ... 56 Клиент QNIVZ 26 Доставчик ZWZDM 56 Клиент QNIVZ 28 Доставчик OAVQT 56 Доставчик OAVQT 56 56 Клиент WVFAF 8 Доставчик BWGYE... Клиент AHXHT 5 Доставчик EQPNC 58 Клиент AHXHT 6 Доставчик QWUSF ... (1238 реда отпред цитирано)

Има обаче два проблема с това решение. Да предположим, че освен Клиенти бихте могли да имате редове в друга таблица в заявката без съвпадащи редове в следваща таблица и че в такъв случай не искате да запазите тези външни редове. Например, какво ще стане, ако във вашата среда е било разрешено да създадете заглавка за поръчка и в по-късен момент да я попълните с редове за поръчка. Да предположим, че в такъв случай заявката не трябва да връща такива празни заглавки на поръчка. Все пак заявката трябва да върне клиенти без поръчки. Тъй като свързването между Orders и OrderDetails е ляво външно свързване, тази заявка ще върне такива празни поръчки, въпреки че не трябва.

Друг проблем е, че когато използвате външни обединявания, вие налагате повече ограничения на оптимизатора по отношение на пренарежданията, които му е позволено да изследва като част от неговата оптимизация за подреждане на присъединяване. Оптимизаторът може да пренареди съединението A LEFT OUTER JOIN B към B DIGHT OUTER JOIN A, но това е почти единственото пренареждане, което му е разрешено да изследва. С вътрешните съединения, оптимизаторът може също да пренарежда таблици извън само обръщане, например, може да пренареди join(join(join(join(A, B), C), D), E)))) към join(A, join(B, join(join(E, D), C))), както е показано по-рано на фигура 5.

Ако се замислите, това, което наистина търсите, е да присъедините клиентите наляво с резултата от вътрешните свързвания между останалите таблици. Очевидно можете да постигнете това с таблични изрази. T-SQL обаче поддържа друг трик. Това, което наистина определя подреждането на логическото присъединяване, не е точно редът на таблиците в клаузата FROM, а по-скоро редът на клаузите ON. Въпреки това, за да е валидна заявката, всяка клауза ON трябва да се появи точно под двете единици, които се свързва. Така че, за да считате свързването между клиенти и останалите за последно, всичко, което трябва да направите, е да преместите клаузата ON, която свързва клиентите, и останалите да се показват последни, така:

 ИЗБЕРЕТЕ DISTINCT C.custid, C.companyname КАТО клиент, S.supplierid, S.companyname КАТО доставчик ОТ Sales.Customers AS C LEFT OUTER JOIN Sales.Поръчки КАТО 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; -- <-- до тук --

Сега логическата поръчка на присъединяване е:leftjoin(Клиенти, присъединете се (присъединете се(присъединете се (Поръчки, Подробности за поръчка), Продукти), Доставчици)). Този път ще задържите клиенти, които не са направили поръчки, но няма да запазите заглавки на поръчки, които нямат съвпадащи редове за поръчки. Освен това, вие позволявате на оптимизатора пълна гъвкавост при поръчване на присъединяване във вътрешните връзки между поръчки, подробности за поръчката, продукти и доставчици.

Единственият недостатък на този синтаксис е четливостта. Добрата новина е, че това може лесно да се поправи с помощта на скоби, например (наречете тази заявка 6):

 ИЗБЕРЕТЕ DISTINCT C.custid, C.companyname КАТО клиент, S.supplierid, S.companyname КАТО доставчик ОТ Sales.Customers AS C LEFT OUTER JOIN ( Sales.Orders AS O INNER JOIN Sales.OrderDetails КАТО 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

Обърнете внимание, че този път свързването между клиентите и останалите се обработва като външно обединение и че оптимизаторът е приложил оптимизация за подреждане на присъединяване.

Заключение

В тази статия разгледах четири класически грешки, свързани с присъединяването. When using outer joins, computing the COUNT(*) aggregate typically results in a bug. 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.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Префиксът sp_ все още ли е не-не?

  2. Как да изтриете ревизии на публикации с помощта на WP-CLI

  3. Повреждане на базата данни

  4. Изпращане на данни от SentryOne към калкулатора DTU на базата данни на Azure SQL

  5. Какво е DBMS? – Изчерпателно ръководство за системи за управление на бази данни