web-dev-qa-db-fra.com

sélectionner les données de plusieurs tables dans une requête - comment contrôler JOIN?

Je développe ma question ici: Construire une requête avec JOIN, mais filtrer et trier cette table en premier Maintenant, l'objectif est devenu plus compliqué et je ne peux pas le résoudre.

Voici le problème:
Je construis un composant personnalisé dans Joomla 3.x. J'ai deux tables dans mon modèle:

table persons  
id  name 
1   Peter
2   Paul
3   Mary

table cars
id  personid  make     price     color
1   1         BMW      10,000    red
2   1         Audi     8,000     blue
3   1         BMW      6,000     white
4   2         BMW      21,000    silver
5   2         Renault  9,500     black
6   3         Seat     4,200     green

Maintenant, j'aimerais construire une requête sur le retour des personnes, de leur voiture, de son prix et de sa couleur.
Voici la partie la plus délicate: s’ils ont plusieurs voitures, la requête doit sélectionner la BMW dont le prix est le plus bas:

Peter   BMW    6,000    white
Paul    BMW    21,000   silver
Mary    Seat   4,200    green

J'ai essayé avec ...

SELECT p.name, c.make, MIN(c.price), c.color
FROM persons AS p 
LEFT JOIN cars AS c ON c.personid = p.id
GROUP BY p.id

mais les résultats sont faux:

Peter    BMW    6,000    red      (color change!)
Paul     BMW    9,500    silver   (price drop!)
Mary     Seat   4,200    green    (well...)

voir le violon: http://sqlfiddle.com/#!9/d93f41/2/

Comment puis-je contrôler quelle voiture est liée à la personne qui a plus d'une voiture?

1
michi

Joomla ne gère pas très bien les sous-requêtes car il n'y a pas de vraie méthode $ query-> subQuery. Dans ce cas, il est plus facile de formater la requête en tant que requête MySQL standard et de laisser Joomla traiter les résultats. La requête ci-dessous renverra le résultat approprié pour Peter et Paul ayant des BMW, avec le prix le plus bas et les couleurs correctes.

$db       = JFactory::getDbo();
$query    = $db->getQuery(true);
$sql = "SELECT p.name, c.make, c.price, c.color FROM #__persons AS p LEFT JOIN #__cars AS c ON c.personid = p.id WHERE c.make = 'BMW' AND c.price = (SELECT MIN(price) FROM #__cars WHERE make = 'BMW' AND personid = p.id)";
$db->setQuery($sql);
$rows = $db->loadObjectList();

Mise à jour SQLFiddle

2
Terry Carter

La solution de Terry Carter ne fournit pas la fonctionnalité/l'ensemble de résultats souhaité ET elle transmet la fausse notion selon laquelle Joomla "ne gère pas bien les sous-requêtes". S'il est vrai qu'il n'y a pas de méthode appelée subQuery(), Joomla fournit une syntaxe propre pour imbriquer des requêtes à l'intérieur d'autres requêtes.

La question la plus criante de la requête de Terry est qu’elle ignore toutes les personnes qui n’ont pas de BMW à vendre. Ce n'est pas la logique souhaitée décrite dans la question. L'ensemble de résultats attendus devrait inclure une ligne pour Mary qui contient l'élément non BMW le moins cher.

De plus, ma solution implémente INNER JOIN au lieu de LEFT JOIN, de sorte que tout nom existant dans persons et ne comportant pas de ligne dans cars sera exclu du jeu de résultats. (Si la logique opposée est souhaitée, utilisez bien sûr un LEFT JOIN.) En d'autres termes, un LEFT JOIN générera potentiellement des lignes avec NULL valeurs dans make, price et color.

SQL brut: ( démonstration de DB-Fiddle.com )

SELECT name, make, price, color
FROM persons p
INNER JOIN cars ON p.id = personid
WHERE price = IFNULL(
                  (SELECT MIN(price) FROM cars WHERE personid = p.id AND make = 'BMW'),
                  (SELECT MIN(price) FROM cars WHERE personid = p.id)
              )

Les choses à noter:

  • Je n'ai pas ajouté d'alias de table dans la clause SELECT car il n'y a pas d'ambiguïté à gérer. En d'autres termes, étant donné qu'aucune des colonnes désignées n'existe dans les deux tableaux, MySQL sait exactement ce à quoi il est destiné et aucune alarme ne sonne.
  • La même logique est appliquée à la première clause WHERE - j'aurais pu écrire c.price, Mais MySQL sait déjà dans quel tableau il doit être dessiné.
  • Comme cars n'est jamais utilisé pour identifier les colonnes de la requête, j'ai choisi d'omettre l'alias c.
  • Si vous décidez de déclarer l'alias de table c et d'ajouter c. À chacune des colonnes de la requête, veillez à ne PAS écrire c.make = 'BMW' Dans la première sous-requête, car modifiera la logique et endommagera les résultats - make dans ce contexte doit faire référence à la valeur make de la sous-requête.
  • IFNULL() est un choix judicieux pour la requête, car la deuxième sous-requête n'est exécutée que si la première sous-requête ne renvoie aucune ligne qualifiante. Ce bloc d’état garantit que vous obtenez la valeur BMW la plus basse, à moins qu’il n’y ait pas de BMW pour la personne concernée, auquel cas vous retombez au "prix le plus bas" pour cette personne.

Maintenant, pour utiliser les méthodes de construction de requêtes de Joomla pour créer la requête ... Comme pour la grande majorité de mes messages de cette communauté, je vais emballer mon extrait avec un vidage de requête et quelques astuces de diagnostic pour aider les chercheurs à comprendre et à déboguer leurs propres projets. . Veuillez noter que vous ne devriez jamais afficher les requêtes générées ni les erreurs mysql auprès du public, car cela peut conduire des personnes coquines à affiner les attaques malveillantes sur votre système.

try {
    $db = JFactory::getDbo();

    $bmw_subquery = $db->getQuery(true)
                       ->select("MIN(price)")
                       ->from("cars")
                       ->where("personid = p.id")
                       ->where("make = " . $db->q("BMW"));

    $all_subquery = $db->getQuery(true)
                       ->select("MIN(price)")
                       ->from("cars")
                       ->where("personid = p.id");

    $query = $db->getQuery(true)
                ->select("name, make, price, color")
                ->from("persons p")
                ->innerJoin("cars ON p.id = personid")
                ->where("price = IFNULL(($bmw_subquery), ($all_subquery))");

    $db->setQuery($query);
    JFactory::getApplication()->enqueueMessage($query->dump(), 'info');
    if (!$result = $db->loadAssocList()) {
        echo "<p>No Rows Found</p>";
    } else {
        echo "<pre>";
        var_export($result);
        echo "</pre>";
    }
} catch (Exception $e) {
    JFactory::getApplication()->enqueueMessage("Query Syntax Error: " . $e->getMessage(), 'error');  // don't show $e->getMessage() to public
}

Cela servira la requête rendue décrite ci-dessus et le jeu de résultats souhaité:

array (
  0 => 
  array (
    'name' => 'Peter',
    'make' => 'BMW',
    'price' => '6000',
    'color' => 'white',
  ),
  1 => 
  array (
    'name' => 'Paul',
    'make' => 'BMW',
    'price' => '21000',
    'color' => 'silver',
  ),
  2 => 
  array (
    'name' => 'Mary',
    'make' => 'Seat',
    'price' => '4200',
    'color' => 'green',
  ),
)

Quelques mots d'adieu:

  1. Je me serais attendu à ce que les tables de la question portent les noms des caractères de préfixe aléatoires de Joomla. Il est vivement recommandé d’utiliser ces préfixes et ajuster la syntaxe du générateur de requêtes Joomla signifie uniquement que les préfixes #__, Tels que #__cars Et #__persons Sont associés à des tables pourrait s'appeler abcde_cars et abcde_persons.

  2. Si quelqu'un est voué à écrire du code laconique, la sous-requête $bmw_subquery Peut être générée en clonant la sous-requête $all_subquery Puis en chaînant la condition de clause where supplémentaire.

    $all_subquery = $db->getQuery(true)
                       ->select("MIN(price)")
                       ->from("cars")
                       ->where("personid = p.id");
    
    $bmw_subquery = (clone($all_subquery))->where("make = " . $db->q("BMW"));
    //              ^--------------------^-- parentheses are essential
    
    $query = $db->getQuery(true)
                ->select("name, make, price, color")
                ->from("persons p")
                ->innerJoin("cars ON p.id = personid")
                ->where("price = IFNULL(($bmw_subquery), ($all_subquery))");
    

    * Les parenthèses sont obligatoires pour que vous "cloniez puis enchaînez" au lieu de "enchaîner à l'original puis cloner". Si vous supprimez les parenthèses, vous constaterez que les deux sous-requêtes seront identiques et qu'elles incluront AND make = 'BMW'.

0
mickmackusa