Utiliser les méthodes UNION dans les requêtes de base de données
From Joomla! Documentation
Bien que les méthodes UNION soient présentes dans Joomla! 2.5 et 3.x, elles ne fonctionnent pas pour toutes les versions antérieures à 3.3.
Utiliser UNION dans une requête de base de données est un moyen très utile pour combiner les résultats de deux ou plusieurs requêtes de bases de données SELECT qui ne sont pas nécessairement liés par une relation entre bases de données. Elle peut également être utile pour l'optimisation des performances. Utiliser UNION pour combiner les résultats des différentes requêtes peut parfois être beaucoup plus rapide qu'une simple requête avec une clause WHERE, en particulier lorsque la requête implique une jonction à d'autres importantes tables.
Pour ceux qui connaissent un peu la théorie des ensembles, une UNION est exactement ce que vous rechercher et permet de fusionner l'ensemble des résultats d'une requête avec l'ensemble des résultats d'une autre requête pour produire un ensemble de résultats qui sera l'ensemble des résultats individuels. Si vous souhaitez que les résultats soient triés, il vous faudra porter une attention particulière à la façon de procéder, comme cela sera expliqué un peu plus tard.
Les bases
Afin d'utiliser UNION vous devez être conscient des exigences de base requises par le serveur SQL que vous utilisez. Elles ne sont pas définies par Joomla! mais si vous ne vous y conformez pas, vous obtiendrez des erreurs de base de données. Ainsi, chaque requête SELECT doit renvoyer le même nombre de champs, dans le même ordre et avec des types de données compatibles pour que le UNION puisse fonctionner correctement.
Un exemple simple
Supposons que vous souhaitez envoyer un publipostage à un groupe de personnes, mais que la base de données est telle que les noms et adresses de courriels à utiliser ne sont pas tous dans la même table. Pour prendre un exemple, supposons que vous souhaitez envoyer le message à tous les clients et tous les fournisseurs et que les noms et adresses de courriels sont stockés dans des tables dénommées respectivement "customers" (clients) et "suppliers" (fournisseurs).
Cette requête permettra de récupérer l'ensemble des informations concernant les clients (customers) dont nous avons besoin pour le publipostage :
$query
->select('name, email')
->from('customers')
;
$mailshot = $db->setQuery($query)->loadObjectList();
Alors que cette requête va faire la même chose mais pour les fournisseurs (suppliers) :
$query
->select('name, email')
->from('suppliers')
;
$mailshot = $db->setQuery($query)->loadObjectList();
Vous pouvez alors combiner les résultats en une seule requête comme ceci :
$query
->select('name, email')
->from('customers')
->union($q2->select('name , email')->from('suppliers'))
;
$mailshot = $db->setQuery($query)->loadObjectList();
L'ensemble des résultats obtenus à partir de la requête Union sera en réalité un peu différent que celui obtenu lors de l'exécution de requêtes individuelles séparées, car la requête utilisant Union éliminera automatiquement les doublons. Si cela ne vous dérange pas que les résultats puissent contenir des doublons, alors vous devez utiliser unionAll à la place de union ce qui permettra d'améliorer les performances.
Différentes façons d'utiliser union
Les méthodes union (et unionAll) sont relativement souples en ce qui concerne les arguments qu'elles vont accepter. Vous pouvez passer une chaîne de requête "comme au bon vieux temps", un objet JDatabaseQuery ou un tableau array d'objets JDatabaseQuery. Par exemple, supposons que vous disposez de trois tables similaires à l'exemple ci-dessus :
$q1->select('name, email')->from('customers');
$q2->select('name, email')->from('suppliers');
$q3->select('name, email')->from('shareholders');
Ainsi, toutes ces requêtes produiront les mêmes résultats :
// The union method can be chained.
$q1->union($q2)->union($q3);
// The union method will accept string queries.
$q1->union($q2)->union('SELECT name, email FROM shareholders');
// The union method will accept an array of JDatabaseQuery objects.
$q1->union(array($q2, $q3));
// It doesn't matter which query object is at the "root" of the query. In this case the actual query that is produced will be different but the result set will be the same.
$q2->union(array($q1, $q3));
union, unionAll et unionDistinct
Il existe trois méthodes union disponibles.
- union produit un résultat constitué d'un ensemble de résultats individuels ; les doublons sont supprimés. Le processus d'élimination des doublons peuvent engendrer une dégradation des performances au regard des paramètres des données et de la structure utilisée pour la base de données.
- unionAll produit un résultat constitué d'un ensemble de résultats individuels mais pour lequel les doublons ne sont pas supprimés.
- unionDistinct est identique dans son comportement à union et est simplement un proxy pour la méthode union.
Utiliser UNION à la place de OR
Il existe certaines instances où l'utilisation de union peut engendrer un gain significatif de performances par rapport à l'utilisation courante et alternative d'un OR ou d'un IN dans une clause where.
Supposons par exemple que vous ayez une table de produits et que vous souhaiter en extraire uniquement les produits qui appartiennent à deux catégories bien définies. Habituellement, le code ressemblerait à quelque chose de ce type :
$query
->select('*')
->from('products')
->where('category = ' . $db->q('catA'), 'or')
->where('category = ' . $db->q('catB'))
;
$products = $db->setQuery($query)->loadObjectList();
Cependant, il est probable que vous serez contents d'une augmentation des performances par l'utilisation de union :
$query
->select('*')
->from('products')
->where('category = ' . $db->q('catA'))
;
$q2
->select('*')
->from('products')
->where('category = ' . $db->q('catB'))
;
$query->union($q2);
$products = $db->setQuery($query)->loadObjectList();
Bien sûr, si vous souhaitez sélectionner des produits présents dans plus de deux catégories, vous pouvez combiner plusieurs requêtes individuelles.
Vous devriez constater une augmentation semblable des performances lors du remplacement d'une clause where contenant une instruction IN.
Classer les résultats
Si vous souhaitez que vos résultats soient classés, alors vous devez connaître la façon dont la base de données utilise les clauses ORDER BY. Les remarques suivantes s'appliquent pour MySQL, mais doivent sans doute s'appliquer à d'autres types de bases de données.
Suivant l'exemple précédent, supposons que vous souhaitiez que les noms et adresses de courriels soient classés par ordre alphabétique pour le publipostage. Il vous suffit alors de faire comme ceci :
$q2
->select('name , email')
->from('suppliers')
;
$query
->select('name, email')
->from('customers')
->union($q2)
->order('name')
;
$mailshot = $db->setQuery($query)->loadObjectList();
Mais supposons que, pour une raison ou une autre, vous souhaitez que les noms et adresses de courriels soient classés par ordre alphabétique mais avec tous les clients d'abord, puis tous les fournisseurs. Cette requête ne donnera pas le résultat espéré :
$q2
->select('name , email')
->from('suppliers')
->order('name')
;
$query
->select('name, email')
->from('customers')
->order('name')
->union($q2)
;
$mailshot = $db->setQuery($query)->loadObjectList();
Ceci s'explique par le fait qu'une clause ORDER BY dans un SELECT individuel n'a aucune incidence quant à l'ordre dans lequel les lignes apparaissent dans le résultat final. Un UNION produit un ensemble de lignes non-ordonnées. La requête ci-dessus est correcte quant à sa syntaxe, mais l'optimiseur MySQL va simplement ignorer le ORDER BY pour l'instruction SELECT pour les fournisseurs, et l'instruction ORDER BY pour la clause SELECT pour les clients sera appliquée à l'ensemble des résultats plutôt que sur un résultat individuel.
Le moyen de contourner ce problème est d'ajouter une colonne supplémentaire pour le jeu de résultats et d'effectuer le tri sur celle-ci de telle sorte que les résultats seront classés de la façon désirée. Voici une façon de faire :
$q2
->select('name , email, 1 as sort_col')
->from('suppliers')
;
$query
->select('name, email, 2 as sort_col')
->from('customers')
->union($q2)
->order('sort_col, name')
;
$mailshot = $db->setQuery($query)->loadObjectList();
Classement avancé
Cependant, il peut exister des occasions où il est important d'avoir une clause ORDER BY pour des requêtes individuelles qui ne sera pas supprimée par l'optimiseur de requête. Supposons que vous souhaitiez envoyer une offre spéciale pour vos 10 premiers clients et vos 5 premiers fournisseurs. Par conséquent, vous voudrez appliquer une clause LIMIT en combinaison avec une clause ORDER BY et dans ce cas, l'optimiseur de requête ne pourra pas ignorer la commande. Voici la façon dont vous pouvez procéder :
$q2
->select('name , email, 1 as sort_col')
->from('suppliers')
->order('turnover DESC')
->setLimit(5)
;
$q1
->select('name, email, 2 as sort_col')
->from('customers')
->order('turnover DESC')
->setLimit(10)
;
$query
->select('name, email, 0 as sort_col')
->from('customers')
->where('1 = 0')
->union($q1)
->union($q2)
->order('sort_col, name')
;
$mailshot = $db->setQuery($query)->loadObjectList();
Il est nécessaire d'utiliser une requête particulière dans ce cas car sinon les méthodes order et setLimit seront appliquées au résultat final plutôt qu'au résultats individuels.
Si vous rencontrez des erreurs en reproduisant cet exemple, consultez l'anomalie et le patch associé : 4127