Pourquoi les SGBDR ne renvoient-ils pas les tables jointes dans un format imbriqué?


14

Par exemple, disons que je veux récupérer un utilisateur et tous ses numéros de téléphone et adresses e-mail. Les numéros de téléphone et les e-mails sont stockés dans des tableaux séparés, un utilisateur pour plusieurs téléphones / e-mails. Je peux le faire assez facilement:

SELECT * FROM users user 
    LEFT JOIN emails email ON email.user_id=user.id
    LEFT JOIN phones phone ON phone.user_id=user.id

Le problème * avec cela est qu'il renvoie le nom de l'utilisateur, la date de naissance, la couleur préférée et toutes les autres informations stockées dans la table utilisateur encore et encore pour chaque enregistrement (les utilisateurs envoient des enregistrements par téléphone), ce qui suppose vraisemblablement une bande passante et un ralentissement. les résultats.

Ne serait-il pas plus agréable de renvoyer une seule ligne pour chaque utilisateur, et dans cet enregistrement, il y avait une liste de courriels et une liste de téléphones? Cela rendrait également les données beaucoup plus faciles à utiliser.

Je sais que vous pouvez obtenir des résultats comme celui-ci en utilisant LINQ ou peut-être d'autres cadres, mais cela semble être une faiblesse dans la conception sous-jacente des bases de données relationnelles.

Nous pourrions contourner cela en utilisant NoSQL, mais ne devrait-il pas y avoir un juste milieu?

Suis-je en train de manquer quelque chose? Pourquoi cela n'existe-t-il pas?

* Oui, c'est conçu de cette façon. J'ai compris. Je me demande pourquoi il n'y a pas d'alternative plus facile à utiliser. SQL pourrait continuer à faire ce qu'il fait, mais ensuite ils pourraient ajouter un ou deux mots clés pour faire un peu de post-traitement qui renvoie les données dans un format imbriqué au lieu d'un produit cartésien.

Je sais que cela peut être fait dans un langage de script de votre choix, mais cela nécessite que le serveur SQL envoie des données redondantes (exemple ci-dessous) ou que vous émettiez plusieurs requêtes comme SELECT email FROM emails WHERE user_id IN (/* result of first query */).


Au lieu d'avoir MySQL retourner quelque chose de semblable à ceci:

[
    {
        "name": "John Smith",
        "dob": "1945-05-13",
        "fav_color": "red",
        "email": "johnsmith45@gmail.com",
    },
    {
        "name": "John Smith",
        "dob": "1945-05-13",
        "fav_color": "red",
        "email": "john@smithsunite.com",
    },
    {
        "name": "Jane Doe",
        "dob": "1953-02-19",
        "fav_color": "green",
        "email": "originaljane@deerclan.com",
    }
]

Et puis avoir à regrouper sur un identifiant unique (ce qui signifie que je dois aussi le récupérer!) Côté client pour reformater le jeu de résultats comme vous le souhaitez, il suffit de retourner ceci:

[
    {
        "name": "John Smith",
        "dob": "1945-05-13",
        "fav_color": "red",
        "emails": ["johnsmith45@gmail.com", "john@smithsunite.com"]
    },
    {
        "name": "Jane Doe",
        "dob": "1953-02-19",
        "fav_color": "green",
        "emails": ["originaljane@deerclan.com"],
    }
]

Alternativement, je peux émettre 3 requêtes: 1 pour les utilisateurs, 1 pour les e-mails et 1 pour les numéros de téléphone, mais les ensembles de résultats de courrier électronique et de numéro de téléphone doivent contenir le user_id afin que je puisse les faire correspondre avec les utilisateurs J'ai déjà récupéré. Encore une fois, des données redondantes et un post-traitement inutile.


6
Considérez SQL comme une feuille de calcul, comme dans Microsoft Excel, puis essayez de comprendre comment créer une valeur de cellule contenant des cellules internes. Cela ne fonctionne plus bien comme feuille de calcul. Ce que vous recherchez est une structure arborescente, mais vous n'avez plus les avantages d'une feuille de calcul (c'est-à-dire que vous ne pouvez pas totaliser une colonne dans un arbre). Les structures arborescentes ne rendent pas les rapports très lisibles par l'homme.
Reactgular

54
SQL n'est pas mauvais pour renvoyer des données, vous êtes mauvais pour demander ce que vous voulez. En règle générale, si vous pensez qu'un outil largement utilisé est bogué ou cassé pour un cas d'utilisation courant, le problème est que vous.
Sean McSomething

12
@SeanMcSomething Si vrai que ça fait mal, je n'aurais pas pu mieux le dire moi-même.
WernerCD

5
Ce sont de grandes questions. Les réponses qui disent "c'est comme ça" manquent. Pourquoi n'est-il pas possible de renvoyer des lignes avec des collections de lignes intégrées?
Chris Pitman

8
@SeanMcSomething: À moins que cet outil largement utilisé soit C ++ ou PHP, auquel cas vous avez probablement raison. ;)
Mason Wheeler

Réponses:


11

Au fond, dans les entrailles d'une base de données relationnelle, toutes ses lignes et colonnes. C'est la structure avec laquelle une base de données relationnelle est optimisée pour fonctionner. Les curseurs travaillent sur des lignes individuelles à la fois. Certaines opérations créent des tables temporaires (encore une fois, il doit s'agir de lignes et de colonnes).

En ne travaillant qu'avec des lignes et en ne renvoyant que des lignes, le système peut mieux gérer la mémoire et le trafic réseau.

Comme mentionné, cela permet d'effectuer certaines optimisations (index, jointures, unions, etc ...)

Si l'on voulait une structure arborescente imbriquée, cela nécessite que l'on tire toutes les données à la fois. Finies les optimisations pour les curseurs côté base de données. De même, le trafic sur le réseau devient une grande rafale qui peut prendre beaucoup plus de temps que le lent filet de ligne par ligne (c'est quelque chose qui est parfois perdu dans le monde Web d'aujourd'hui).

Chaque langue contient des tableaux. Ce sont des choses faciles à travailler et à interfacer. En utilisant une structure très primitive, le pilote entre la base de données et le programme - quelle que soit la langue - peut fonctionner de manière courante. Une fois que l'on commence à ajouter des arbres, les structures du langage deviennent plus complexes et plus difficiles à parcourir.

Ce n'est pas si difficile pour un langage de programmation de convertir les lignes retournées dans une autre structure. Faites-en un arbre ou un ensemble de hachage ou laissez-le comme une liste de lignes que vous pouvez parcourir.

Il y a aussi de l'histoire à l'œuvre ici. Transférer des données structurées était quelque chose de laid à l'époque. Regardez le format EDI pour avoir une idée de ce que vous pourriez demander. Les arbres impliquent également la récursivité - que certaines langues ne prenaient pas en charge (les deux langues les plus importantes de l'ancien temps ne prenaient pas en charge la récursivité - la récursivité n'est entrée dans Fortran qu'en F90 et à l'époque COBOL non plus).

Et bien que les langues d'aujourd'hui prennent en charge la récursivité et les types de données plus avancés, il n'y a pas vraiment de bonne raison de changer les choses. Ils fonctionnent et ils fonctionnent bien. Ceux qui sont en train de changer les choses sont les bases de données NoSQL. Vous pouvez stocker des arborescences dans des documents dans un document basé sur un. LDAP (c'est en fait ancien) est également un système basé sur un arbre (bien que ce ne soit probablement pas ce que vous recherchez). Qui sait, peut-être que la prochaine chose dans les bases de données nosql sera de retourner la requête en tant qu'objet json.

Cependant, les «anciennes» bases de données relationnelles ... elles fonctionnent avec des lignes parce que c'est leur domaine et tout peut leur parler sans problème ni traduction.

  1. Dans la conception de protocoles, la perfection a été atteinte non pas quand il n'y a plus rien à ajouter, mais quand il n'y a plus rien à retirer.

De RFC 1925 - Les douze vérités de mise en réseau


"Si l'on voulait une structure arborescente imbriquée, cela nécessite que l'on tire toutes les données à la fois. Finies les optimisations pour les curseurs côté base de données." - Cela ne semble pas vrai. Il suffirait de conserver quelques curseurs: un pour la table principale, puis un pour chaque table jointe. Selon l'interface, il peut renvoyer une ligne et toutes les tables jointes en un seul morceau (partiellement diffusé), ou il peut diffuser les sous-arborescences (et peut-être même ne pas les interroger) jusqu'à ce que vous commenciez à les itérer. Mais oui, cela complique beaucoup les choses.
mpen

3
Chaque langue moderne devrait avoir une sorte de classe d'arbre, non? Et ne serait-ce pas au chauffeur de s'en occuper? Je suppose que les gars de SQL doivent encore concevoir un format commun (je ne sais pas grand-chose à ce sujet). Ce qui m'obtient cependant, c'est que je dois envoyer 1 requête avec des jointures, récupérer et filtrer les données redondantes que chaque ligne (les informations utilisateur, qui ne changent que toutes les N lignes), ou émettre 1 requête (utilisateurs) et parcourez les résultats, puis envoyez deux autres requêtes (e-mails, téléphones) pour chaque enregistrement afin de récupérer les informations dont j'ai besoin. L'une ou l'autre méthode semble inutile.
mpen

51

Il renvoie exactement ce que vous avez demandé: un seul jeu d'enregistrements contenant le produit cartésien défini par les jointures. Il existe de nombreux scénarios valides où c'est exactement ce que vous voudriez, donc dire que SQL donne un mauvais résultat (et donc impliquer qu'il serait préférable que vous le changiez) reviendrait en fait à bousiller beaucoup de requêtes.

Ce que vous rencontrez est connu sous le nom de «non -concordance d'impédance objet / relationnelle », les difficultés techniques qui découlent du fait que le modèle de données orienté objet et le modèle de données relationnel sont fondamentalement différents à plusieurs égards. LINQ et les autres frameworks (connus sous le nom d'ORM, Object / Relational Mappers, pas par coïncidence) ne sont pas comme par magie "contourner cela;" ils émettent simplement des requêtes différentes. Cela peut aussi être fait en SQL. Voici comment je le ferais:

SELECT * FROM users user where [criteria here]

Itérer la liste des utilisateurs et faire une liste d'ID.

SELECT * from EMAILS where user_id in (list of IDs here)
SELECT * from PHONES where user_id in (list of IDs here)

Et ensuite, vous vous joignez au côté client. C'est ainsi que LINQ et d'autres frameworks le font. Il n'y a pas de vraie magie impliquée; juste une couche d'abstraction.


14
+1 pour "exactement ce que vous avez demandé". Trop souvent, nous sautons à la conclusion qu'il y a quelque chose qui ne va pas avec la technologie plutôt qu'à la conclusion que nous devons apprendre à utiliser la technologie efficacement.
Matt

1
Mise en veille prolongée récupérera l'entité racine et certaines collections en une seule requête lorsque l' envie mode de récupération est utilisé pour les collections; dans ce cas, il réduit les propriétés de l'entité racine en mémoire. D'autres ORM peuvent probablement faire de même.
Mike Partridge

3
En fait, ce n'est pas à blâmer sur le modèle relationnel. Il fait très bien face aux relations imbriquées merci. Il s'agit uniquement d'un bogue d'implémentation dans les premières versions de SQL. Je pense que des versions plus récentes l'ont ajouté.
John Nilsson

8
Êtes-vous sûr qu'il s'agit d'un exemple d'impédance relationnelle-objet? Il me semble que le modèle relationnel correspond parfaitement au modèle de données conceptuel de l'OP: chaque utilisateur est associé à une liste de zéro, une ou plusieurs adresses e-mail. Ce modèle est également parfaitement utilisable dans un paradigme OO (agrégation: l'objet utilisateur possède une collection d'e-mails). La limitation réside dans la technique utilisée pour interroger la base de données, qui est un détail d'implémentation. Il existe des techniques de requête autour desquelles les données héritières sont retournées, par exemple les ensembles de données héréditaires dans .Net
MarkJ

@MarkJ, vous devriez écrire cela comme une réponse.
Mr.Mindor

12

Vous pouvez utiliser une fonction intégrée pour concaténer les enregistrements ensemble. Dans MySQL, vous pouvez utiliser la GROUP_CONCAT()fonction et dans Oracle, vous pouvez utiliser la LISTAGG()fonction.

Voici un exemple de ce à quoi pourrait ressembler une requête dans MySQL:

SELECT user.*, 
    (SELECT GROUP_CONCAT(DISTINCT emailAddy) FROM emails email WHERE email.user_id = user.id
    ) AS EmailAddresses,
    (SELECT GROUP_CONCAT(DISTINCT phoneNumber) FROM phones phone WHERE phone.user_id = user.id
    ) AS PhoneNumbers
FROM users user 

Cela retournerait quelque chose comme

username    department       EmailAddresses                        PhoneNumbers
Tim_Burton  Human Resources  hr@m.com, tb@me.com, nunya@what.com   231-123-1234, 231-123-1235

Cela semble être la solution (en SQL) la plus proche de ce que l'OP tente de faire. Il devra potentiellement encore effectuer un traitement côté client pour diviser les résultats EmailAddresses et PhoneNumbers en listes.
Mr.Mindor

2
Que faire si le numéro de téléphone a un "type", comme "Cellule", "Domicile" ou "Travail"? De plus, les virgules sont techniquement autorisées dans les adresses e-mail (si elles sont citées) - comment pourrais-je alors les diviser?
mpen

10

Le problème est qu'il renvoie le nom de l'utilisateur, la date de naissance, la couleur préférée et toutes les autres informations stockées

Le problème est que vous n'êtes pas assez sélectif. Vous avez demandé tout quand vous avez dit

Select * from...

... et vous l'avez compris (y compris la date de naissance et les couleurs préférées).

Vous devriez probablement être un peu plus (hum) ... sélectif, et dire quelque chose comme:

select users.name, emails.email_address, phones.home_phone, phones.bus_phone
from...

Il est également possible que vous voyiez des enregistrements qui ressemblent à des doublons car un userpeut se joindre à plusieurs emailenregistrements, mais le champ qui les distingue n'est pas dans votre Selectdéclaration, vous pouvez donc vouloir dire quelque chose comme

select distinct users.name, emails.email_address, phones.home_phone, phones.bus_phone
from...

... encore et encore pour chaque enregistrement ...

De plus, je remarque que vous faites un LEFT JOIN. Cela joindra tous les enregistrements à gauche de la jointure (c'est users-à- dire ) à tous les enregistrements à droite, ou en d'autres termes:

Une jointure externe gauche renvoie toutes les valeurs d'une jointure interne plus toutes les valeurs de la table de gauche qui ne correspondent pas à la table de droite.

( http://en.wikipedia.org/wiki/Join_(SQL)#Left_outer_join )

Une autre question est donc la suivante: avez-vous réellement besoin d' une jointure gauche ou aurait INNER JOIN-elle suffi? Ce sont des types de jointures très différents.

Ce ne serait pas mieux s'il renvoyait une seule ligne pour chaque utilisateur, et dans cet enregistrement, il y avait une liste de courriels

Si vous voulez réellement qu'une seule colonne dans le jeu de résultats contienne une liste qui est générée à la volée, cela peut être fait mais cela varie en fonction de la base de données que vous utilisez. Oracle a la listaggfonction .


En fin de compte, je pense que votre problème pourrait être résolu si vous réécrivez votre requête près de quelque chose comme ceci:

select distinct users.name, users.id, emails.email_address, phones.phone_number
from users
  inner join emails on users.user_id = emails.user_id
  inner join phones on users.user_id = phones.user_id

1
l'utilisation de * est déconseillée mais pas au cœur de son problème. Même s'il sélectionne 0 colonne utilisateur, il peut toujours subir un effet de duplication, car les téléphones et les e-mails ont une relation à plusieurs avec les utilisateurs. Distinct n'empêcherait pas qu'un numéro de téléphone apparaisse deux fois ala phone1/name@hotmail.com, phone1/name@google.com.
mike30

6
-1: "votre problème pourrait être résolu" indique que vous ne savez pas quel serait l'effet du changement de left joinà inner join. Dans ce cas, cela ne réduira pas les «répétitions» dont se plaint l'utilisateur; cela supprimerait simplement les utilisateurs qui n'ont ni téléphone ni e-mail. pratiquement aucune amélioration. en outre, lors de l'interprétation de «tous les enregistrements de gauche à tous les enregistrements de droite», le ONcritère est ignoré , qui élague toutes les «mauvaises» relations inhérentes au produit cartésien mais conserve tous les champs répétés.
Javier

@Javier: Oui, c'est pourquoi j'ai également dit que vous avez réellement besoin d'une jointure gauche, ou une INNER JOIN aurait-elle été suffisante? * La description du problème par OP fait * sonner comme s'ils attendaient le résultat d'une jointure interne. Bien sûr, sans aucun échantillon de données ou une description de ce qu'ils voulaient vraiment , c'est difficile à dire. J'ai fait la suggestion parce que j'ai vu des gens (ceux avec qui je travaille) faire ceci: choisir la mauvaise jointure, puis se plaindre lorsqu'ils ne comprennent pas les résultats qu'ils obtiennent. Après l'avoir vu , j'ai pensé que cela aurait pu arriver ici.
FrustratedWithFormsDesigner

3
Vous manquez le point de la question. Dans cet exemple hypothétique, je veux toutes les données utilisateur (nom, date de naissance, etc.) et je veux tous ses numéros de téléphone. Une jointure interne exclut les utilisateurs sans e-mails ni téléphone - comment cela peut-il aider?
mpen

4

Les requêtes produisent toujours un ensemble tabulaire rectangulaire (non dentelé) de données. Il n'y a pas de sous-ensembles imbriqués dans un ensemble. Dans le monde des décors, tout est un pur rectangle non imbriqué.

Vous pouvez considérer une jointure comme mettant 2 ensembles côte à côte. La condition "on" est la façon dont les enregistrements de chaque ensemble sont mis en correspondance. Si un utilisateur possède 3 numéros de téléphone, vous verrez une duplication 3 fois dans les informations utilisateur. Un ensemble rectangulaire non dentelé doit être produit par la requête. C'est simplement la nature de joindre des ensembles avec une relation 1 à plusieurs.

Pour obtenir ce que vous voulez, vous devez utiliser une requête distincte comme Mason Wheeler décrit.

select * from Phones where user_id=344;

Le résultat de cette requête est toujours un ensemble rectangulaire non dentelé. Comme tout dans le monde des décors.


2

Vous devez décider où les goulots d'étranglement existent. La bande passante entre votre base de données et votre application est généralement assez rapide. Il n'y a aucune raison pour laquelle la plupart des bases de données ne peuvent pas renvoyer 3 jeux de données distincts au cours d'un même appel et aucune jointure. Ensuite, vous pouvez tout regrouper dans votre application si vous le souhaitez.

Sinon, vous voulez que la base de données rassemble cet ensemble de données, puis supprime toutes les valeurs répétées dans chaque ligne qui sont le résultat des jointures et pas nécessairement les lignes elles-mêmes ayant des données en double comme deux personnes avec le même nom ou numéro de téléphone. On dirait beaucoup de frais généraux pour économiser sur la bande passante. Vous feriez mieux de vous concentrer sur le retour de moins de données avec un meilleur filtrage et la suppression des colonnes dont vous n'avez pas besoin. Parce que Select * n'est jamais utilisé en production, cela dépend.


"Il n'y a aucune raison pour que la plupart des bases de données ne puissent pas renvoyer 3 jeux de données distincts en un seul appel et aucune jointure" - Comment faire pour renvoyer 3 jeux de données distincts en un seul appel? Je pensais que vous deviez envoyer 3 requêtes différentes, ce qui introduit une latence entre chacune?
mpen

Une procédure stockée peut être appelée en 1 transaction, puis renvoyer autant d'ensembles de données que vous le souhaitez. Peut-être qu'un sproc "SelectUserWithEmailsPhones" est nécessaire.
Graham

1
@Mark: vous pouvez envoyer (au moins sur un serveur SQL) plusieurs commandes dans le même lot. cmdText = "select * from b; select * from a; select * from c", puis utilisez-le comme texte de commande pour la commande sql.
jmoreno

2

Très simplement, ne joignez pas vos données si vous voulez des résultats distincts pour une requête utilisateur et une requête de numéro de téléphone, sinon, comme d'autres l'ont souligné, le "Set" ou les données contiendront des champs supplémentaires pour chaque ligne.

Émettez 2 requêtes distinctes au lieu d'une avec une jointure.

Dans la procédure stockée ou SQL paramétré en ligne, 2 requêtes et renvoyez les résultats des deux. La plupart des bases de données et des langues prennent en charge plusieurs jeux de résultats.

Par exemple, SQL Server et C # accomplissent cette fonctionnalité en utilisant IDataReader.NextResult().


1

Vous manquez quelque chose. Si vous souhaitez dénormaliser vos données, vous devez le faire vous-même.

;with toList as (
    select  *, Stuff(( select ',' + (phone.phoneType + ':' + phone.PhoneNumber) 
                    from phones phone
                    where phone.user_id = user.user_id
                    for xml path('')
                  ), 1,1,'') as phoneNumbers
from users user
)
select *
from toList

1

Le concept de fermeture relationnelle signifie essentiellement que le résultat de toute requête est une relation qui peut être utilisée dans d'autres requêtes comme s'il s'agissait d'une table de base. Il s'agit d'un concept puissant car il rend les requêtes composables.

Si SQL vous permettait d'écrire des requêtes qui produisent des structures de données imbriquées, vous briseriez ce principe. Une structure de données imbriquée n'est pas une relation, vous auriez donc besoin d'un nouveau langage de requête ou d'extensions complexes pour SQL, afin de l'interroger davantage ou de la joindre à d'autres relations.

Fondamentalement, vous construisez un SGBD hiérarchique au-dessus d'un SGBD relationnel. Ce sera beaucoup plus complexe pour un bénéfice douteux et vous perdrez les avantages d'un système relationnel cohérent.

Je comprends pourquoi il serait parfois pratique de pouvoir sortir des données structurées hiérarchiquement à partir de SQL, mais le coût de la complexité supplémentaire dans le SGBD pour prendre en charge cela n'en vaut certainement pas la peine.


-4

Les pls font référence à l'utilisation de la fonction STUFF qui regroupe plusieurs lignes (numéros de téléphone) d'une colonne (contact) qui peuvent être extraites comme une seule cellule de valeurs délimitées d'une ligne (utilisateur).

Aujourd'hui, nous l'utilisons largement, mais nous sommes confrontés à des problèmes de processeur et de performances élevés. Le type de données XML est une autre option, mais il s'agit d'un changement de conception et non d'un niveau de requête.


5
Veuillez expliquer comment cela résout la question. Plutôt que de dire "Pls se réfère à l'utilisation de", donnez un exemple de la manière dont cela permettrait de répondre à la question posée. Il peut également être utile de citer des sources tierces lorsque cela rend les choses plus claires.
bitsoflogic

1
On dirait que ça STUFFressemble à une épissure. Je ne sais pas comment cela s'applique à ma question.
mpen
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.