Page 1 sur 1

[MYSQL] Double LEFT JOIN

Posté : 20 sept. 2007, 15:55
par fab
Je suis en présence d'un cas complexe de requete SQL dans le cas ou j'aurais besoin de faire 2 LEFT JOIN, à partir de deux tables différentes sur deux autres tables.
Seulement je n'arrive pas à trouver comment effectuer cette opération.

Voici a quoi ressemble en gros la requete dans mon idée :

Code : Tout sélectionner

SELECT contrats. * , comments_has_correspondants.*,users_has_contrats. * , users.*, gammes.*, contrats_temp.* FROM users_has_contrats, contrats, users, contrats_temp, gammes LEFT JOIN comments_has_correspondants ON comments_has_correspondants.id_user = users.id AND comments_has_correspondants.id_gamme = gammes.id LEFT JOIN contrats_temp ON contrats_temp.id_contrat = contrats.id WHERE contrats.id = users_has_contrats.id_contrat AND users.id=users_has_contrats.id_user AND users.droits_id=7 AND contrats.gammes_id=gammes.id ORDER BY users.name ASC, gammes.name ASC , contrats.name ASC

Posté : 20 sept. 2007, 16:08
par Sékiltoyai
Tu peux poster les parties de structure intéressantes dans tes tables, une description de chaque table, et ce que la requète doit récupérer exactement ?

Posté : 20 sept. 2007, 16:28
par fab
En fait je veux récuperer une liste de contrats ( contrats ) avec l'user lié ( la table users_has_contrats fait cette jointure ) dont le droits_id est 7.
En plus de ça j'ai besoin de récuprer des informations temporaires sur le contrat ( table contrats_temp ) mais il n'existe pas forcément d'enregistrement correspondant dans cette table.

plus un commentaire (comments_has_corres.name )qui n'existe pas forcément ( table comments_has_corres ) chaque commentaire est lié par l'id de l'utilisateur (users.id et comments_has_corres.id_user ) et l'id de la gamme ( gammes.id et comments_has_corres.id_gamme )a laquelle appartient le contrat ( contrats.gammes_id )

et je crois que j'en fini la avec les informations de la gamme à laquelle chaque contrat est ratachée par le contrats.id_gamme qui correspond a gammes.id

Le double left join vient du fait que les enregistrements dans les tables comments_has_corres et contrats_temp n'existent pas forcément.

voici les principales structures :

Code : Tout sélectionner

CREATE TABLE `users_has_contrats` ( `id` int(10) unsigned NOT NULL auto_increment, `id_user` int(10) unsigned NOT NULL, `id_contrat` int(10) unsigned NOT NULL, PRIMARY KEY `id` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=62 ; CREATE TABLE `contrats` ( `id` int(10) unsigned NOT NULL auto_increment, `gammes_id` int(10) unsigned NOT NULL, `name` varchar(255) NOT NULL PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1571 ; CREATE TABLE `gammes` ( `id` int(10) unsigned NOT NULL auto_increment, `name` varchar(255) NOT NULL PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=20 ; CREATE TABLE `users` ( `id` int(10) unsigned NOT NULL auto_increment, `droits_id` int(10) unsigned NOT NULL, `name` varchar(255) NOT NULL PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=58 ; CREATE TABLE `contrats_temp` ( `id` int(10) unsigned NOT NULL auto_increment, `id_contrat` int(10) unsigned NOT NULL, `statut` int(10) unsigned NOT NULL, UNIQUE KEY `id` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=44 ; CREATE TABLE `comments_has_correspondants` ( `id` int(11) NOT NULL auto_increment, `id_user` int(11) unsigned default NULL, `id_gamme` int(11) unsigned default NULL, `name` text NOT NULL PRIMARY KEY (`id`) ) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;

Posté : 20 sept. 2007, 17:17
par sadeq
Je peux expliquer ce problème par un exemple:
La relation entre contrat et contrat_temp est de type 0,n/1,1 ce qui permet de dire que certains contrats n'ont pas forcémment d'infos temporaires mais une info temporaire est relative à un et un seul contrat quand elle existe.
Dans ce cas là si l'on veut afficher à la fois les contrats qui n'ont pas d'infos temporaires et ceux qui en ont, il faudra utiliser une jointure à gauche entre contrat et contrat_temp et celà s'écrira:

Code : Tout sélectionner

SELECT * FROM (contrat c LEFT JOIN contrat_temp ct ON c.id = ct.id_contrat)

Posté : 20 sept. 2007, 17:29
par fab
Oui mais le problème et qu'il y a les autres critères de jointures avec d'autres tables. Expliqué dans mon précédent post

Posté : 20 sept. 2007, 17:58
par fab
Bon après quelques heures actives de recherche et de retournage de doc
voila que miracle se produit !!!!!

pour ceux que ça interesse une requete inbouffable :)

Code : Tout sélectionner

SELECT contrats. * , comments_has_correspondants. * , users. * , gammes. * , contrats_temp. * , users_has_contrats. * FROM users_has_contrats, gammes, ( contrats LEFT JOIN contrats_temp ON ( contrats.id = contrats_temp.id ) ), ( users LEFT JOIN comments_has_correspondants ON ( users.id = comments_has_correspondants.id_user ) ) WHERE contrats.id = users_has_contrats.id_contrat AND users.id = users_has_contrats.id_user AND users.droits_id =7 AND contrats.gammes_id = gammes.id AND gammes.id = comments_has_correspondants.id_gamme

Posté : 21 sept. 2007, 00:03
par Hubert Roksor
Salut et désolé d'arriver après la bataille ;)

Avant d'essayer de comprendre la structure, j'ai voulu reformatter la requête d'après les conventions que j'utilise --des JOIN plutôt que des virgules, et les conditions de jointures dans le JOIN ... ON plutôt que WHERE-- et il y a un truc que j'ai mal recopié ou qui doit être incorrect dans la requête originale. [Note: en fait non, en recopiant l'exemple j'ai compris où j'avais oublié un truc, voir plus bas ;)] Je précise que je fais ça avant de chercher à comprendre la structure pour démarrer avec une point de vue neutre, comme le parser de MySQL quand il commence à lire la requête. Mon but ici n'étant pas de "trouver la solution", puisque fab semble l'avoir déjà trouvée, mais plutôt de la vérifier.

J'ai pris la requête originale, remplacé les colonnes du SELECT, changé les virgules en JOIN et donné des aliases aux tables

Code : Tout sélectionner

SELECT * FROM users_has_contrats uhc JOIN gammes g JOIN ( contrats c LEFT JOIN contrats_temp ct ON c.id = ct.id ) JOIN ( users u LEFT JOIN comments_has_correspondants chc ON u.id = chc.id_user ) WHERE c.id = uhc.id_contrat AND u.id = uhc.id_user AND u.droits_id = 7 AND c.gammes_id = g.id AND g.id = chc.id_gamme
Puis j'ai mis les conditions de jointures dans les JOIN

Code : Tout sélectionner

SELECT * FROM users_has_contrats uhc JOIN gammes g ON g.id = chc.id_gamme JOIN ( contrats c LEFT JOIN contrats_temp ct ON c.id = ct.id ) ON c.id = uhc.id_contrat AND c.gammes_id = g.id JOIN ( users u LEFT JOIN comments_has_correspondants chc ON u.id = chc.id_user ) ON u.id = uhc.id_user WHERE u.droits_id =7
À partir de là, puisque je vois que ta seule condition de WHERE est une constante, je devine que c'est à partir de cette table qu'on doit chercher les données donc je décide de mettre la table users en tête de liste dans le FROM et mettre à plat le reste. De plus, même si ça n'a pas d'incidence sur la requête, je remets certaines comparaisons dans l'ordre. Par exemple, au lieu de "a JOIN b ON a.col = b.col" j'écris "a JOIN b ON b.col = a.col" parce que je le trouve plus naturel si on le lit à voix haute : "table a jointe à la table b là où b est égal à a". Mais je digresse :

Code : Tout sélectionner

SELECT * FROM users u JOIN users_has_contrats uhc ON uhc.id_user = u.id JOIN gammes g ON g.id = chc.id_gamme /* ?? */ JOIN contrats c ON c.id = uhc.id_contrat AND c.gammes_id = g.id LEFT JOIN contrats_temp ct ON ct.id = c.id LEFT JOIN comments_has_correspondants chc ON chc.id_user = u.id WHERE u.droits_id = 7
Là mon problème c'est que la condition de jointure de gammes est une colonne dans une table jointe en LEFT JOIN donc ça disqualifie les lignes où il n'y a pas de correspondance dans comments_has_correspondants, le transformant en INNER JOIN. C'est là que je comprends que je n'ai pas mis les tables dans le bon ordre et les conditions de jointure dans le bon sens. Je vous passe les permutations et j'en arrive à

Code : Tout sélectionner

SELECT * FROM users u JOIN users_has_contrats uhc ON uhc.id_user = u.id JOIN contrats c ON c.id = uhc.id_contrat JOIN gammes g ON g.id = c.gammes_id LEFT JOIN contrats_temp ct ON c.id = ct.id LEFT JOIN comments_has_correspondants chc ON chc.id_user = u.id AND chc.id_gamme = g.id WHERE u.droits_id = 7
Si je ne me suis pas planté, cette dernière requête devrait être équivalente à la tienne, fab. J'ai retiré les parenthèses qui forçaient l'ordre de jointure mais je ne crois pas que cela change le résultat tout en donnant à MySQL un petit peu plus de libertés pour traverser les tables dans l'ordre le plus efficace.

Ce serait cool si tu pouvais vérifier que ça donne bien les mêmes résultats et me copier/coller la sortie d'un EXPLAIN EXTENDED + SHOW WARNINGS (ou EXPLAIN tout court si ton serveur ne le supporte pas) des deux requêtes, merci ;)

Un point intéressant, qui souligne l'utilité de certaines conventions de formattage et notamment de se placer du point de vue du serveur, c'est que j'ai pu me faire une bonne idée de la structure de la base rien qu'en lisant la requête SQL à voix haute :
Pour chaque users dont les droits sont 7,
on cherche qui possède un contrat dans users_has_contrats par id_user,
on met la main sur ce contrat dans contrats par id_contrat,
on récupère les informations de la gamme du contrat dans gammes par gammes_id,
puis on récupère des informations facultatives sur les contrats dans contrats_temp
et des commentaires sur les users dans la gamme du contrat dans comments_has_correspondants par id_user,id_gamme
Bien sûr, ça ne fonctionne que si la base a été correctement structurée, ce qui semble être le cas ici :)

[Note : je maintiendrai cette opinion même dans l'éventualité où je me serais planté au recopiage ;)]

Posté : 21 sept. 2007, 10:05
par fab

Code : Tout sélectionner

mysql> EXPLAIN EXTENDED SELECT * -> FROM users u -> JOIN users_has_contrats uhc ON uhc.id_user = u.id -> JOIN contrats c ON c.id = uhc.id_contrat -> JOIN gammes g ON g.id = c.gammes_id -> LEFT JOIN contrats_temp ct ON c.id = ct.id -> LEFT JOIN comments_has_correspondants chc ON chc.id_user = u.id -> AND chc.id_gamme = g.id -> WHERE u.droits_id =7 ; +----+-------------+-------+--------+---------------------------------------------------------------------+-----------------------------------+---------+-------------------------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+--------+---------------------------------------------------------------------+-----------------------------------+---------+-------------------------+------+-------------+ | 1 | SIMPLE | u | ref | PRIMARY,users_FKIndex2 | users_FKIndex2 | 4 | const | 2 | | | 1 | SIMPLE | uhc | ref | id_user,users_has_contrats_ibfk_2 | id_user | 4 | observer.u.id | 1 | Using index | | 1 | SIMPLE | c | eq_ref | PRIMARY,contrats_FKIndex1 | PRIMARY | 4 | observer.uhc.id_contrat | 1 | | | 1 | SIMPLE | g | eq_ref | PRIMARY,gammes_index2441 | PRIMARY | 4 | observer.c.gammes_id | 1 | | | 1 | SIMPLE | ct | eq_ref | id | id | 4 | observer.uhc.id_contrat | 1 | | | 1 | SIMPLE | chc | ref | comments_has_correspondants_fkid1,comments_has_correspondants_fkid2 | comments_has_correspondants_fkid1 | 5 | observer.uhc.id_user | 1 | | +----+-------------+-------+--------+---------------------------------------------------------------------+-----------------------------------+---------+-------------------------+------+-------------+ 6 rows in set, 1 warning (0.00 sec)
Finalement tu n'arrives pas tant après la bataille, je me suis suis apçeru que ma requète ne marchait pas vraiment correctement car elle ne me selectionne qu'un seul utilisateur.
En gros avec la requète suivante :

Code : Tout sélectionner

SELECT contrats.id AS C_ID, contrats.name AS C_NAME, contrats.gammes_id AS C_GAMME , users.id AS U_ID, users.name AS U_NAME, users.firstname AS U_FIRSTNAME, users.login AS U_LOGIN , gammes.id AS G_ID, gammes.name AS G_NAME , contrats_temp.statut AS CTEMP_STATUT , societes.id AS S_ID, societes.name AS S_NAME, comments_has_correspondants.name AS CHC_NAME FROM users_has_contrats, gammes, ( contrats LEFT JOIN contrats_temp ON ( contrats.id = contrats_temp.id_contrat )), ( users LEFT JOIN comments_has_correspondants ON ( users.id = comments_has_correspondants.id_user )), ( contrat_has_distributeur LEFT JOIN societes ON ( contrat_has_distributeur.societes_id = societes.id )) WHERE contrats.id = users_has_contrats.id_contrat AND users.id = users_has_contrats.id_user AND users.droits_id =7 AND contrats.gammes_id = gammes.id AND contrat_has_distributeur.contrats_id = contrats.id AND gammes.id = comments_has_correspondants.id_gamme
j'obtient 3 enregistrements valides mais en tout il y en a 6 !
Tandis qu'avec ta requete modifiée ( pour les alias :D et pour introduire la problématique des societes et aussi car l'identifiant du contrat dans la table contrats_temp c'est id_contrat) :

Code : Tout sélectionner

SELECT u.id AS U_ID, u.name AS U_NAME, u.firstname AS U_FIRSTNAME, u.login AS U_LOGIN, c.id AS C_ID, c.name AS C_NAME, g.id AS G_ID, g.name AS G_NAME, ct.statut AS CT_STATUT, chc.name AS CHC_NAME, s.id AS S_ID, s.name AS S_NAME FROM users u JOIN users_has_contrats uhc ON uhc.id_user = u.id JOIN contrats c ON c.id = uhc.id_contrat JOIN gammes g ON g.id = c.gammes_id JOIN contrat_has_distributeur chd ON chd.contrats_id = c.id LEFT JOIN contrats_temp ct ON c.id = ct.id_contrat LEFT JOIN comments_has_correspondants chc ON chc.id_user = u.id AND chc.id_gamme = g.id LEFT JOIN societes s ON s.id = chd.societes_id WHERE u.droits_id = 7
j'obtient les 6 bons enregistrements. :)

Si tu as des petits conseils d'optimisations je veux bien :)


Après le point négatif de l'histoire c'est d'intégrer ça dans symfony :s, la solution pour laquelle j'ai optée c'est de passer outre criteria en demandant simplement a propel d'executer une requete sql.

PS: je ne peux poster les données ça aurait été plus simple pour la compréhension mais je ne dispose que de données réelles trop importantes pour êtres affichées en public désolé :s

Posté : 21 sept. 2007, 12:13
par Hubert Roksor
Content que ça marche (à part pour la typo c <=> ct ;)).

Niveau optimisation, je pense que la structure est presque optimale. Je pense que tu aurais des performances légèrement meilleures en supprimant la colonne (surrogate key) chc.id et en utilisant (id_user,id_gamme) comme clé primaire. Au pire, s'assurer que (id_user,id_gamme) est au moins UNIQUE. Selon la distribution des données, (id_gamme,id_user) est envisageable mais tu auras probablement besoin d'un index supplémentaire sur (id_user). Pour le reste, tout a l'air bon et la requête n'est finalement pas très compliquée : 3 ref, 3 eq_ref et presque toutes les jointures sur les clé primaires.

Un mot sur EXPLAIN EXTENDED, après l'avoir exécuté il faut exécuter

Code : Tout sélectionner

SHOW WARNINGS
pour obtenir la requête normalisée, ou encore activer l'affichage automatique en tapant \W en début de session dans le client MySQL.