Database
 sql >> Base de données >  >> RDS >> Database

Bogues, pièges et meilleures pratiques T-SQL - jointures

Cet article est le troisième volet d'une série sur les bogues, les pièges et les meilleures pratiques de T-SQL. Auparavant, j'ai couvert le déterminisme et les sous-requêtes. Cette fois, je me concentre sur les jointures. Certains des bogues et des meilleures pratiques que je couvre ici sont le résultat d'une enquête que j'ai menée auprès de collègues MVP. Merci à Erland Sommarskog, Aaron Bertrand, Alejandro Mesa, Umachandar Jayachandran (UC), Fabiano Neves Amorim, Milos Radivojevic, Simon Sabin, Adam Machanic, Thomas Grohser, Chan Ming Man et Paul White pour leurs commentaires !

Dans mes exemples, j'utiliserai un exemple de base de données appelé TSQLV5. Vous pouvez trouver le script qui crée et remplit cette base de données ici, et son diagramme ER ici.

Dans cet article, je me concentre sur quatre bogues courants classiques :COUNT(*) dans les jointures externes, les agrégats à double dipping, la contradiction ON-WHERE et la contradiction de jointure OUTER-INNER. Tous ces bogues sont liés aux principes de base des requêtes T-SQL et sont faciles à éviter si vous suivez les meilleures pratiques simples.

COUNT(*) dans les jointures externes

Notre premier bogue concerne les décomptes incorrects signalés pour les groupes vides suite à l'utilisation d'une jointure externe et de l'agrégat COUNT(*). Considérez la requête suivante calculant le nombre de commandes et le fret total par client :

 UTILISER 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;

Cette requête génère la sortie suivante (abrégé) :

 nb de commandesfret total ------- ---------- ------------- 1 6 225,58 2 4 97,42 3 7 268,52 4 13 471,95 5 18 1559,52 ... 21 7 232,75 23 5 637,94 ... 56 10 862,74 58 6 277,96 ... 87 15 822,48 88 9 194,71 89 14 1353,06 90 7 88,41 91 7 175,74
 Il y a 91 clients actuellement présents dans le tableau Clients, dont 89 ont passé des commandes ; par conséquent, la sortie de cette requête montre 89 groupes de clients et leur nombre correct de commandes et les agrégats de fret total. Les clients avec les ID 22 et 57 sont présents dans la table Clients mais n'ont passé aucune commande et par conséquent ils n'apparaissent pas dans le résultat. 

Supposons qu'il vous soit demandé d'inclure les clients qui n'ont pas de commandes associées dans le résultat de la requête. La chose naturelle à faire dans un tel cas est d'effectuer une jointure externe gauche entre Customers et Orders pour conserver les clients sans commandes. Cependant, un bogue typique lors de la conversion de la solution existante en une solution qui applique la jointure consiste à laisser le calcul du nombre de commandes à COUNT(*), comme indiqué dans la requête suivante (appelez-la Requête 1) :

 SELECT C.custid, COUNT(*) AS numorders, SUM(O.freight) AS totalfreight FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON C.custid =O.custid GROUP BY C.custid COMMANDER PAR C.custid ;

Cette requête génère la sortie suivante :

 nb de commandesfret total ------- ---------- ------------- 1 6 225,58 2 4 97,42 3 7 268,52 4 13 471,95 5 18 1559.52 ... 21 7 232.75 22 1 NULL 23 5 637.94 ... 56 10 862.74 57 1 NULL 58 6 277.96 ... 87 15 822.48 88 9 194.71 89 14 1353.06 90 7 88.71  

Observez que les clients 22 et 57 apparaissent cette fois dans le résultat, mais leur nombre de commandes affiche 1 au lieu de 0 car COUNT(*) compte les lignes et non les commandes. Le fret total est signalé correctement car SUM(freight) ignore les entrées NULL.

Le plan de cette requête est illustré à la figure 1.

Figure 1 :Plan pour la requête 1

Dans ce plan, Expr1002 représente le nombre de lignes par groupe qui, en raison de la jointure externe, est initialement défini sur NULL pour les clients sans commandes correspondantes. L'opérateur Compute Scalar juste en dessous du nœud racine SELECT convertit ensuite le NULL en 1. C'est le résultat du comptage des lignes par opposition au comptage des commandes.

Pour corriger ce bogue, vous souhaitez appliquer l'agrégat COUNT à un élément du côté non conservé de la jointure externe, et vous voulez vous assurer que vous utilisez une colonne non NULLable comme entrée. La colonne de clé primaire serait un bon choix. Voici la requête de solution (appelez-la Requête 2) avec le bogue corrigé :

 SELECT C.custid, COUNT(O.orderid) AS numorders, SUM(O.freight) AS totalfreight FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON C.custid =O.custid GROUP BY C .custid ORDER BY C.custid;

Voici le résultat de cette requête :

 nb de commandesfret total ------- ---------- ------------- 1 6 225,58 2 4 97,42 3 7 268,52 4 13 471,95 5 18 1559,52 ... 21 7 232,75 22 0 NULL 23 5 637,94 ... 56 10 862,74 57 0 NULL 58 6 277,96 ... 87 15 822,48 88 9 194,71 89 14 1353,06 90 7 88,7  

Observez que cette fois les clients 22 et 57 affichent le compte correct de zéro.

Le plan de cette requête est illustré à la figure 2.

Figure 2 :Plan pour la requête 2

Vous pouvez également voir le changement dans le plan, où un NULL représentant le nombre d'un client sans commandes correspondantes est converti en 0 et non en 1 cette fois.

Lorsque vous utilisez des jointures, veillez à ne pas appliquer l'agrégat COUNT(*). Lors de l'utilisation de jointures externes, il s'agit généralement d'un bogue. La meilleure pratique consiste à appliquer l'agrégat COUNT à une colonne non NULLable du côté plusieurs de la jointure un-à-plusieurs. La colonne de clé primaire est un bon choix à cet effet car elle n'autorise pas les valeurs NULL. Cela peut être une bonne pratique même lorsque vous utilisez des jointures internes, car vous ne savez jamais si, ultérieurement, vous devrez remplacer une jointure interne par une jointure externe en raison d'une modification des exigences.

Agrégats à double effet

Notre deuxième bogue implique également de mélanger les jointures et les agrégats, cette fois en prenant en compte plusieurs fois les valeurs source. Considérez la requête suivante comme exemple :

 SELECT C.custid, COUNT(O.orderid) AS numorders, SUM(O.freight) AS totalfreight, CAST(SUM(OD.qty * OD.unitprice * (1 - OD.discount)) AS NUMERIC(12 , 2)) AS totalval FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON C.custid =O.custid LEFT OUTER JOIN Sales.OrderDetails AS OD ON O.orderid =OD.orderid GROUP BY C.custid ORDER BY C.custid ;

Cette requête joint Customers, Orders et OrderDetails, regroupe les lignes par custid et est censée calculer des agrégats comme le nombre de commandes, le fret total et la valeur totale par client. Cette requête génère la sortie suivante :

 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 401017.32 27363.61 

Pouvez-vous repérer le bogue ici?

Les en-têtes de commande sont stockés dans la table Orders et leurs lignes de commande respectives sont stockées dans la table OrderDetails. Lorsque vous joignez des en-têtes de commande avec leurs lignes de commande respectives, l'en-tête est répété dans le résultat de la jointure par ligne. Par conséquent, l'agrégat COUNT(O.orderid) reflète de manière incorrecte le nombre de lignes de commande et non le nombre de commandes. De même, SUM(O.freight) prend en compte de manière incorrecte le fret plusieurs fois par commande, autant que le nombre de lignes de commande dans la commande. Le seul calcul agrégé correct dans cette requête est celui utilisé pour calculer la valeur totale puisqu'il est appliqué aux attributs des lignes de commande :SUM(OD.qty * OD.unitprice * (1 - OD.discount).

Pour obtenir le bon nombre de commandes, il suffit d'utiliser un agrégat de nombre distinct :COUNT(DISTINCT O.orderid). Vous pourriez penser que le même correctif peut être appliqué au calcul du fret total, mais cela ne ferait qu'introduire un nouveau bogue. Voici notre requête avec des agrégats distincts appliqués aux mesures de l'en-tête de commande :

 SELECT C.custid, COUNT(DISTINCT O.orderid) AS numorders, SUM(DISTINCT O.freight) AS totalfreight, CAST(SUM(OD.qty * OD.unitprice * (1 - OD.discount)) AS NUMERIC (12, 2)) AS totalval FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON C.custid =O.custid LEFT OUTER JOIN Sales.OrderDetails AS OD ON O.orderid =OD.orderid GROUP BY C. custid ORDER BY C.custid;

Cette requête génère la sortie suivante :

 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 891353.06 27363,61 90 7 87.66 3161.35 *****7553 /pré> 

Le nombre de commandes est maintenant correct, mais les valeurs totales de fret ne le sont pas. Pouvez-vous repérer le nouveau bogue ?

Le nouveau bogue est plus insaisissable car il ne se manifeste que lorsque le même client a au moins un cas où plusieurs commandes ont exactement les mêmes valeurs de fret. Dans un tel cas, vous ne prenez désormais en compte le fret qu'une seule fois par client, et non plus une fois par commande comme vous le devriez.

Utilisez la requête suivante (nécessite SQL Server 2017 ou supérieur) pour identifier des valeurs de fret non distinctes pour le même client :

 WITH C AS ( SELECT custid, fret, STRING_AGG(CAST(orderid AS VARCHAR(MAX)), ', ') WITHIN GROUP(ORDER BY orderid) AS orders FROM Sales.Orders GROUP BY custid, fret HAVING COUNT(* )> 1 ) SELECT custid, STRING_AGG(CONCAT('(fret :', fret, ', commandes :', commandes, ')'), ', ') en tant que doublons FROM C GROUP BY custid ;

Cette requête génère la sortie suivante :

 custid doublons ------- -------------------------------------- - 4 (fret :23,72, commandes :10743, 10953) 90 (fret :0,75, commandes :10615, 11005)

Avec ces résultats, vous vous rendez compte que la requête avec le bogue a signalé des valeurs de fret totales incorrectes pour les clients 4 et 90. La requête a signalé des valeurs de fret totales correctes pour le reste des clients puisque leurs valeurs de fret se sont avérées uniques.

Pour corriger le bogue, vous devez séparer le calcul des agrégats de commandes et des lignes de commande en différentes étapes à l'aide d'expressions de table, comme ceci :

 WITH O AS ( SELECT custid, COUNT(orderid) AS numorders, SUM(freight) AS totalfreight FROM Sales.Orders GROUP BY custid ), OD AS ( SELECT O.custid, CAST(SUM(OD.qty * OD. prix unitaire * (1 - OD.discount)) AS NUMERIC(12, 2)) AS totalval FROM Sales.Orders AS O INNER JOIN Sales.OrderDetails AS OD ON O.orderid =OD.orderid GROUP BY O.custid ) SELECT C. custid, O.numorders, O.totalfreight, OD.totalval FROM Sales.Customers AS C LEFT OUTER JOIN O ON C.custid =O.custid LEFT OUTER JOIN OD ON C.custid =OD.custid ORDER BY C.custid; 

Cette requête génère la sortie suivante :

 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 78.41 3161.35 *** 7.7353 /pré> 

Observez que les valeurs de fret totales pour les clients 4 et 90 sont maintenant plus élevées. Ce sont les bons chiffres.

La meilleure pratique ici consiste à être attentif lors de la réunion et de l'agrégation des données. Vous souhaitez être attentif à ces cas lors de la jointure de plusieurs tables et de l'application d'agrégats aux mesures d'une table qui n'est pas une table périphérique ou feuille dans les jointures. Dans un tel cas, vous devez généralement appliquer les calculs agrégés dans les expressions de table, puis joindre les expressions de table.

Ainsi, le bogue des agrégats de double dipping est corrigé. Cependant, il y a potentiellement un autre bogue dans cette requête. Peux tu le repérer? Je fournirai les détails d'un tel bogue potentiel dans le quatrième cas que je couvrirai plus tard sous "Contradiction de jointure OUTER-INNER".

Contradiction ON-WHERE

Notre troisième bogue résulte de la confusion des rôles que les clauses ON et WHERE sont censées jouer. Par exemple, supposons qu'on vous ait confié une tâche pour faire correspondre les clients et les commandes qu'ils ont passées depuis le 12 février 2019, mais également inclure dans la sortie les clients qui n'ont pas passé de commandes depuis lors. Vous tentez de résoudre la tâche à l'aide de la requête suivante (appelez-la Requête 3) :

 SELECT C.custid, C.companyname, O.orderid, O.orderdate FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON O.custid =C.custid WHERE O.orderdate>='20190212';

Lors de l'utilisation d'une jointure interne, ON et WHERE jouent les mêmes rôles de filtrage, et donc peu importe la façon dont vous organisez les prédicats entre ces clauses. Cependant, lorsque vous utilisez une jointure externe comme dans notre cas, ces clauses ont des significations différentes.

La clause ON joue un rôle de correspondance, ce qui signifie que toutes les lignes du côté préservé de la jointure (Customers dans notre cas) vont être renvoyées. Ceux qui ont des correspondances basées sur le prédicat ON sont connectés avec leurs correspondances et, par conséquent, répétés par correspondance. Ceux qui n'ont aucune correspondance sont renvoyés avec des valeurs NULL comme espaces réservés dans les attributs du côté non conservé.

À l'inverse, la clause WHERE joue un rôle de filtrage plus simple, toujours. Cela signifie que les lignes pour lesquelles le prédicat de filtrage prend la valeur true sont renvoyées et que toutes les autres sont supprimées. Par conséquent, certaines des lignes du côté préservé de la jointure peuvent être supprimées complètement.

N'oubliez pas que les attributs du côté non conservé de la jointure externe (Orders dans notre cas) sont marqués comme NULL pour les lignes externes (non correspondances). Chaque fois que vous appliquez un filtre impliquant un élément du côté non conservé de la jointure, le prédicat de filtre est évalué comme inconnu pour toutes les lignes externes, ce qui entraîne leur suppression. Ceci est en accord avec la logique de prédicat à trois valeurs suivie par SQL. En fait, la jointure devient alors une jointure interne. La seule exception à cette règle est lorsque vous recherchez spécifiquement un NULL dans un élément du côté non conservé pour identifier les non-correspondances (l'élément EST NULL).

Notre requête boguée génère la sortie suivante :

 custid companyname orderid orderdate ------- --------------- -------- ---------- 1 Client NRZBB 11011 09/04/2019 1 Client NRZBB 10952 16/03/2019 2 Client MLTDN 10926 04/03/2019 4 Client HFBZG 11016 10/04/2019 4 Client HFBZG 10953 16/03/2019 4 Client HFBZG 101920 3-20 03 5 Client HGVLZ 10924 04/03/2019 6 Client XHXJV 11058 29/04/2019 6 Client XHXJV 10956 17/03/2019 8 Client QUHWH 10970 24/03/2019 ... 20 Client THHDP 10979 26/03/2019 20 Client THHDP 10968 2019-03-23 ​​20 Client THHDP 10895 2019-02-18 24 Client CYZTN 11050 2019-04-27 24 Client CYZTN 11001 2019-04-06 24 Client CYZTN 10993 2019-04-01 ... (195 lignes affecté)

La sortie souhaitée est censée comporter 213 lignes, dont 195 lignes représentant les commandes passées depuis le 12 février 2019 et 18 lignes supplémentaires représentant les clients qui n'ont pas passé de commande depuis lors. Comme vous pouvez le voir, la sortie réelle n'inclut pas les clients qui n'ont pas passé de commande depuis la date spécifiée.

Le plan de cette requête est illustré à la figure 3.

Figure 3 :Plan pour la requête 3

Observez que l'optimiseur a détecté la contradiction et a converti en interne la jointure externe en jointure interne. C'est agréable à voir, mais en même temps, cela indique clairement qu'il y a un bogue dans la requête.

J'ai vu des cas où des personnes ont essayé de corriger le bogue en ajoutant le prédicat OR O.orderid IS NULL à la clause WHERE, comme ceci :

 SELECT C.custid, C.companyname, O.orderid, O.orderdate FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON O.custid =C.custid WHERE O.orderdate>='20190212' OR O.orderid IS NULL ;

Le seul prédicat correspondant est celui qui compare les ID client des deux côtés. Ainsi, la jointure elle-même renvoie les clients qui ont passé des commandes en général, ainsi que leurs commandes correspondantes, ainsi que les clients qui n'ont pas du tout passé de commandes, avec des valeurs NULL dans leurs attributs de commande. Ensuite, les prédicats de filtrage filtrent les clients qui ont passé des commandes depuis la date spécifiée, ainsi que les clients qui n'ont pas du tout passé de commandes (clients 22 et 57). La requête manque des clients qui ont passé des commandes, mais pas depuis la date spécifiée !

Cette requête génère la sortie suivante :

 custid companyname orderid orderdate ------- --------------- -------- ---------- 1 Client NRZBB 11011 09/04/2019 1 Client NRZBB 10952 16/03/2019 2 Client MLTDN 10926 04/03/2019 4 Client HFBZG 11016 10/04/2019 4 Client HFBZG 10953 16/03/2019 4 Client HFBZG 101920 3-20 03 5 Client HGVLZ 10924 04/03/2019 6 Client XHXJV 11058 29/04/2019 6 Client XHXJV 10956 17/03/2019 8 Client QUHWH 10970 24/03/2019 ... 20 Client THHDP 10979 26/03/2019 20 Client THHDP 10968 2019-03-23 ​​20 Client THHDP 10895 2019-02-18 22 Client DTDMN NULL NULL 24 Client CYZTN 11050 2019-04-27 24 Client CYZTN 11001 2019-04-06 24 Client CYZTN 10993 2019-04-06 . .. (197 lignes concernées)

Pour corriger correctement le bogue, vous avez besoin à la fois du prédicat qui compare les ID client des deux côtés et de celui par rapport à la date de commande pour être considérés comme des prédicats correspondants. Pour ce faire, les deux doivent être spécifiés dans la clause ON, comme ceci (appelez cette requête 4) :

 SELECT C.custid, C.companyname, O.orderid, O.orderdate FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON O.custid =C.custid AND O.orderdate>='20190212';

Cette requête génère la sortie suivante :

 custid companyname orderid orderdate ------- --------------- -------- ---------- 1 Client NRZBB 11011 2019-04-09 1 Client NRZBB 10952 2019-03-16 2 Client MLTDN 10926 2019-03-04 3 Client KBUDE NULL NULL 4 Client HFBZG 11016 2019-04-10 4 Client HFBZG 10953 2019-03-16 4 Client HFBZG 10920 2019-03-03 5 Client HGVLZ 10924 2019-03-04 6 Client XHXJV 11058 2019-04-29 6 Client XHXJV 10956 2019-03-17 7 Client QXVLA NULL NULL 8 Client QUHWH 10970 2019-03-24 ... 20 Client THHDP 10979 2019-03-26 20 Client THHDP 10968 2019-03-23 ​​20 Client THHDP 10895 2019-02-18 21 Client KIDPX NULL NULL 22 Client DTDMN NULL NULL 23 Client WVFAF NULL NULL 24 Client CYZTN 11050 2019-04- 27 24 Client CYZTN 11001 2019-04-06 24 Client CYZTN 10993 2019-04-01 ... (213 lignes concernées)

Le plan de cette requête est illustré à la figure 4.

Figure 4 :Plan pour la requête 4

Comme vous pouvez le constater, l'optimiseur a cette fois traité la jointure comme une jointure externe.

Il s'agit d'une requête très simple que j'ai utilisée à des fins d'illustration. Avec des requêtes beaucoup plus élaborées et complexes, même les développeurs expérimentés peuvent avoir du mal à déterminer si un prédicat appartient à la clause ON ou à la clause WHERE. Ce qui me facilite les choses, c'est simplement de me demander si le prédicat est un prédicat de correspondance ou un prédicat de filtrage. Si c'est le premier, il appartient à la clause ON ; si ce dernier, il appartient à la clause WHERE.

Contradiction de jointure OUTER-INNER

Notre quatrième et dernier bug est en quelque sorte une variante du troisième bug. Cela se produit généralement dans les requêtes multi-jointures où vous mélangez les types de jointure. Par exemple, supposons que vous deviez joindre les tables Customers, Orders, OrderDetails, Products et Suppliers pour identifier les paires client-fournisseur qui avaient une activité conjointe. Vous écrivez la requête suivante (appelez-la Requête 5) :

 SELECT DISTINCT C.custid, C.companyname AS client, S.supplierid, S.companyname AS fournisseur FROM Sales.Customers AS C INNER JOIN Sales.Orders AS O ON O.custid =C.custid INNER JOIN Sales.OrderDetails AS OD ON OD.orderid =O.orderid INNER JOIN Production.Products AS P ON P.productid =OD.productid INNER JOIN Production.Suppliers AS S ON S.supplierid =P.supplierid;

Cette requête génère le résultat suivant avec 1 236 lignes :

 custid client fournisseurid fournisseur ------- --------------- ----------- ---------- ----- 1 Client NRZBB 1 Fournisseur SWRXU 1 Client NRZBB 3 Fournisseur STUAZ 1 Client NRZBB 7 Fournisseur GQRCV ... 21 Client KIDPX 24 Fournisseur JNNES 21 Client KIDPX 25 Fournisseur ERVYZ 21 Client KIDPX 28 Fournisseur OAVQT 23 Client WVFAF 3 Fournisseur STUAZ 23 Client WVFAF 7 Fournisseur GQRCV 23 Client WVFAF 8 Fournisseur BWGYE ... 56 Client QNIVZ 26 Fournisseur ZWZDM 56 Client QNIVZ 28 Fournisseur OAVQT 56 Client QNIVZ 29 Fournisseur OGLRK 58 Client AHXHT 1 Fournisseur SWRXU 58 Client AHXHT 5 Fournisseur EQPNC 58 Client AHXHT 6 Fournisseur QWUSF ... (1236 lignes concernées)

Le plan de cette requête est illustré à la figure 5.

Figure 5 :Plan pour la requête 5

Toutes les jointures du plan sont traitées comme des jointures internes, comme prévu.

Vous pouvez également observer dans le plan que l'optimiseur a appliqué l'optimisation de l'ordre des jointures. Avec les jointures internes, l'optimiseur sait qu'il peut réorganiser l'ordre physique des jointures comme bon lui semble tout en préservant le sens de la requête d'origine, il dispose donc d'une grande flexibilité. Ici, son optimisation basée sur les coûts a abouti à la commande :join(Customers, join(Orders, join(join(Suppliers, Products), OrderDetails))).

Supposons que vous deviez modifier la requête de manière à inclure les clients qui n'ont pas passé de commande. Rappelez-vous que nous avons actuellement deux clients de ce type (avec les ID 22 et 57), donc le résultat souhaité est censé avoir 1 238 lignes. Un bogue courant dans un tel cas est de changer la jointure interne entre Customers et Orders en une jointure externe gauche, mais de laisser tout le reste des jointures comme des jointures internes, comme ceci :

 SELECT DISTINCT C.custid, C.companyname AS client, S.supplierid, S.companyname AS fournisseur FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON O.custid =C.custid INNER JOIN Sales. OrderDetails AS OD ON OD.orderid =O.orderid INNER JOIN Production.Products AS P ON P.productid =OD.productid INNER JOIN Production.Suppliers AS S ON S.supplierid =P.supplierid;

Lorsqu'une jointure externe gauche est ensuite suivie de jointures internes ou externes droites, et que le prédicat de jointure compare quelque chose du côté non conservé de la jointure externe gauche avec un autre élément, le résultat du prédicat est la valeur logique inconnue et la valeur externe d'origine les lignes sont supprimées. La jointure externe gauche devient effectivement une jointure interne.

Par conséquent, cette requête génère la même sortie que pour la requête 5, renvoyant uniquement 1 236 lignes. Ici également, l'optimiseur détecte la contradiction et convertit la jointure externe en jointure interne, générant le même plan que celui illustré précédemment à la figure 5.

Une tentative courante pour corriger le bogue consiste à faire en sorte que toutes les jointures soient des jointures externes à gauche, comme suit :

 SELECT DISTINCT C.custid, C.companyname AS client, S.supplierid, S.companyname AS fournisseur FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON O.custid =C.custid LEFT OUTER JOIN Sales .OrderDetails AS OD ON OD.orderid =O.orderid LEFT OUTER JOIN Production.Products AS P ON P.productid =OD.productid LEFT OUTER JOIN Production.Suppliers AS S ON S.supplierid =P.supplierid;

Cette requête génère le résultat suivant, qui inclut les clients 22 et 57 :

 custid client fournisseurid fournisseur ------- --------------- ----------- ---------- ----- 1 Client NRZBB 1 Fournisseur SWRXU 1 Client NRZBB 3 Fournisseur STUAZ 1 Client NRZBB 7 Fournisseur GQRCV ... 21 Client KIDPX 24 Fournisseur JNNES 21 Client KIDPX 25 Fournisseur ERVYZ 21 Client KIDPX 28 Fournisseur OAVQT 22 Client DTDMN NULL NULL 23 Client WVFAF 3 Fournisseur STUAZ 23 Client WVFAF 7 Fournisseur GQRCV 23 Client WVFAF 8 Fournisseur BWGYE ... 56 Client QNIVZ 26 Fournisseur ZWZDM 56 Client QNIVZ 28 Fournisseur OAVQT 56 Client QNIVZ 29 Fournisseur OGLRK 57 Client WVAXS NULL NULL 58 Client AHXHT 1 Fournisseur SWRXU 58 Client AHXHT 5 Fournisseur EQPNC 58 Client AHXHT 6 Fournisseur QWUSF ... (1238 lignes affe ct)

Cependant, il y a deux problèmes avec cette solution. Supposons qu'en plus de Customers, vous puissiez avoir des lignes dans une autre table de la requête sans lignes correspondantes dans une table suivante, et que dans ce cas, vous ne souhaitiez pas conserver ces lignes externes. Par exemple, que se passe-t-il si dans votre environnement, il était permis de créer un en-tête pour une commande et de le remplir ultérieurement avec des lignes de commande. Supposons que dans un tel cas, la requête ne soit pas censée renvoyer de tels en-têtes de commande vides. Pourtant, la requête est censée renvoyer les clients sans commandes. Étant donné que la jointure entre Orders et OrderDetails est une jointure externe gauche, cette requête renverra ces commandes vides, même si ce n'est pas le cas.

Un autre problème est que lorsque vous utilisez des jointures externes, vous imposez plus de restrictions à l'optimiseur en termes de réarrangements qu'il est autorisé à explorer dans le cadre de son optimisation de l'ordre des jointures. L'optimiseur peut réorganiser la jointure A LEFT OUTER JOIN B en B RIGHT OUTER JOIN A, mais c'est à peu près le seul réarrangement qu'il est autorisé à explorer. Avec les jointures internes, l'optimiseur peut également réorganiser les tables au-delà du simple retournement des côtés, par exemple, il peut réorganiser join(join(join(join(A, B), C), D), E)))) to join(A, join(B, join(join(E, D), C))) comme indiqué précédemment dans la figure 5.

Si vous y réfléchissez, ce que vous recherchez vraiment, c'est de joindre à gauche Customers avec le résultat des jointures internes entre le reste des tables. Évidemment, vous pouvez y parvenir avec des expressions de table. Cependant, T-SQL prend en charge une autre astuce. Ce qui détermine vraiment l'ordre des jointures logiques n'est pas exactement l'ordre des tables dans la clause FROM, mais plutôt l'ordre des clauses ON. Cependant, pour que la requête soit valide, chaque clause ON doit apparaître juste en dessous des deux unités qu'elle rejoint. Ainsi, afin de considérer la jointure entre Customers et le reste comme la dernière, tout ce que vous avez à faire est de déplacer la clause ON qui relie Customers et le reste pour qu'elle apparaisse en dernier, comme ceci :

 SELECT DISTINCT C.custid, C.companyname AS client, S.supplierid, S.companyname AS fournisseur FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O -- déplacer d'ici ------- ---------------- 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; -- <-- ici --

Maintenant, l'ordre de jointure logique est :leftjoin(Customers, join(join(join(Orders, OrderDetails), Products), Suppliers)). Cette fois, vous conserverez les clients qui n'ont pas passé de commande, mais vous ne conserverez pas les en-têtes de commande qui n'ont pas de lignes de commande correspondantes. De plus, vous accordez à l'optimiseur une flexibilité totale de commande de jointure dans les jointures internes entre les commandes, les détails de la commande, les produits et les fournisseurs.

Le seul inconvénient de cette syntaxe est la lisibilité. La bonne nouvelle est que cela peut être facilement corrigé en utilisant des parenthèses, comme ceci (appelez cette requête 6) :

 SELECT DISTINCT C.custid, C.companyname AS client, S.supplierid, S.companyname AS fournisseur 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;

Ne confondez pas l'utilisation de parenthèses ici avec une table dérivée. Ce n'est pas une table dérivée, mais juste un moyen de séparer certains des opérateurs de table dans leur propre unité, pour plus de clarté. La langue n'a pas vraiment besoin de ces parenthèses, mais elles sont fortement recommandées pour la lisibilité.

Le plan de cette requête est illustré à la figure 6.

Figure 6 :Plan pour la requête 6

Observez que cette fois, la jointure entre Customers et le reste est traitée comme une jointure externe, et que l'optimiseur a appliqué l'optimisation de l'ordre des jointures.

Conclusion

Dans cet article, j'ai couvert quatre bogues classiques liés aux jointures. 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.