Perché gli RDBMS non restituiscono le tabelle unite in un formato nidificato?


14

Ad esempio, supponiamo di voler recuperare un utente e tutti i suoi numeri di telefono e indirizzi e-mail. I numeri di telefono e le e-mail sono memorizzati in tabelle separate, da un utente a molti telefoni / e-mail. Posso farlo abbastanza facilmente:

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

Il problema * con questo è che sta restituendo il nome dell'utente, il DOB, il colore preferito e tutte le altre informazioni archiviate nella tabella degli utenti ripetutamente per ogni record (gli utenti inviano messaggi di posta elettronica ai telefoni), presumibilmente consumando larghezza di banda e rallentando giù i risultati.

Non sarebbe più bello se restituisse una singola riga per ciascun utente e all'interno di quel record ci fosse un elenco di e-mail e un elenco di telefoni? Renderebbe molto più facile lavorare anche con i dati.

So che puoi ottenere risultati come questo usando LINQ o forse altri framework, ma sembra essere un punto debole nella progettazione sottostante dei database relazionali.

Potremmo aggirare questo problema usando NoSQL, ma non dovrebbe esserci una via di mezzo?

Mi sto perdendo qualcosa? Perché questo non esiste?

* Sì, è progettato in questo modo. Capisco. Mi chiedo perché non ci sia un'alternativa più facile da lavorare. SQL potrebbe continuare a fare quello che sta facendo ma poi potrebbe aggiungere una o due parole chiave per eseguire un po 'di post-elaborazione che restituisce i dati in un formato nidificato anziché in un prodotto cartesiano.

So che questo può essere fatto in un linguaggio di script a tua scelta, ma richiede che il server SQL invii dati ridondanti (esempio di seguito) o che invii più query come SELECT email FROM emails WHERE user_id IN (/* result of first query */).


Invece di fare in modo che MySQL restituisca qualcosa di simile a questo:

[
    {
        "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",
    }
]

E poi dover raggruppare un identificatore univoco (il che significa che devo recuperarlo anche io) sul lato client per riformattare il set di risultati come lo si desidera, basta restituire questo:

[
    {
        "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"],
    }
]

In alternativa, posso inviare 3 query: 1 per gli utenti, 1 per le e-mail e 1 per i numeri di telefono, ma quindi i set di risultati di e-mail e numeri di telefono devono contenere user_id in modo da poterli ricollegare con gli utenti Ho precedentemente recuperato. Ancora una volta, dati ridondanti e post-elaborazione inutile.


6
Pensa a SQL come a un foglio di calcolo, come in Microsoft Excel, quindi prova a capire come creare un valore di cella che contiene celle interne. Non funziona più bene come foglio di calcolo. Quello che stai cercando è una struttura ad albero, ma poi non hai più i vantaggi di un foglio di calcolo (cioè non puoi sommare una colonna in un albero). Le strutture ad albero non creano report leggibili dall'uomo.
Reactgular,

54
SQL non è male nel restituire i dati, non sei bravo a interrogare ciò che desideri. Come regola generale, se pensi che uno strumento ampiamente utilizzato sia difettoso o rotto per un caso d'uso comune, il problema sei tu.
Sean McSomething,

12
@SeanMcSomething Così vero che fa male, non avrei potuto dirlo meglio.
WernerCD,

5
Questa è un'ottima domanda. Le risposte che dicono "questo è il modo in cui è" mancano il punto. Perché non è possibile restituire righe con raccolte di righe incorporate?
Chris Pitman,

8
@SeanMcSomething: a meno che quello strumento ampiamente utilizzato sia C ++ o PHP, nel qual caso probabilmente hai ragione. ;)
Mason Wheeler,

Risposte:


11

In fondo, nelle viscere di un database relazionale, sono tutte le righe e le colonne. Questa è la struttura con cui un database relazionale è ottimizzato per funzionare. I cursori lavorano su singole file alla volta. Alcune operazioni creano tabelle temporanee (di nuovo, devono essere righe e colonne).

Lavorando solo con le righe e restituendo solo le righe, il sistema è in grado di gestire meglio il traffico di memoria e di rete.

Come accennato, ciò consente di eseguire alcune ottimizzazioni (indici, join, sindacati, ecc ...)

Se si voleva una struttura ad albero nidificata, è necessario che tutti i dati vengano estratti contemporaneamente. Sono finite le ottimizzazioni per i cursori sul lato database. Allo stesso modo, il traffico sulla rete diventa un grande scoppio che può richiedere molto più tempo del lento gocciolio di fila per fila (questo è qualcosa che a volte si perde nel mondo web di oggi).

Ogni lingua ha matrici al suo interno. Queste sono cose facili con cui lavorare e con cui interfacciarsi. Utilizzando una struttura molto primitiva, il driver tra il database e il programma, indipendentemente dalla lingua, può funzionare in modo comune. Una volta che si inizia ad aggiungere alberi, le strutture nella lingua diventano più complesse e più difficili da attraversare.

Non è difficile per un linguaggio di programmazione convertire le righe restituite in qualche altra struttura. Trasformalo in un albero o in un set di hash o lascialo come un elenco di righe su cui puoi scorrere.

C'è anche storia al lavoro qui. Il trasferimento di dati strutturati era qualcosa di brutto ai vecchi tempi. Guarda il formato EDI per avere un'idea di cosa potresti chiedere. Gli alberi implicano anche la ricorsione - che alcune lingue non supportavano (le due lingue più importanti dei vecchi tempi non supportavano la ricorsione - la ricorsione non è entrata in Fortran fino a F90 e nemmeno nell'epoca COBOL).

E mentre le lingue di oggi supportano la ricorsione e tipi di dati più avanzati, non c'è davvero un buon motivo per cambiare le cose. Funzionano e funzionano bene. Quelli che stanno cambiando le cose sono i database nosql. È possibile archiviare alberi nei documenti in uno basato sul documento. LDAP (in realtà è vecchio) è anche un sistema basato su alberi (anche se probabilmente non è quello che stai cercando). Chissà, forse la prossima cosa nei database nosql sarà quella che restituirà la query come oggetto json.

Tuttavia, i "vecchi" database relazionali ... stanno lavorando con le righe perché questo è ciò che sono bravi e tutto può parlare con loro senza problemi o traduzione.

  1. Nella progettazione del protocollo, la perfezione è stata raggiunta non quando non è rimasto nulla da aggiungere, ma quando non è rimasto nulla da togliere.

Da RFC 1925 - Le dodici verità sulla rete


"Se si voleva una struttura ad albero nidificata, ciò richiede che si estraggano tutti i dati contemporaneamente. Sono finite le ottimizzazioni per i cursori sul lato del database." - Non sembra vero. Dovrebbe solo mantenere un paio di cursori: uno per la tabella principale e poi uno per ogni tabella unita. A seconda dell'interfaccia, potrebbe restituire una riga e tutte le tabelle unite in un unico blocco (parzialmente trasmesso in streaming), oppure può eseguire lo streaming dei sottotitoli (e forse nemmeno interrogarli) fino a quando non si inizia a iterarli. Ma sì, questo complica molto le cose.
Aprire il

3
Ogni linguaggio moderno dovrebbe avere una sorta di classe di alberi, no? E non sarebbe compito del guidatore occuparsene? Suppongo che i ragazzi di SQL debbano ancora progettare un formato comune (non ne so molto). La cosa che mi ha fatto è che o devo inviare 1 query con join, e tornare indietro e filtrare i dati ridondanti che ogni riga (le informazioni utente, che cambia solo ogni N riga), o emettere 1 query (utenti) e passa in rassegna i risultati, quindi invia altre due query (e-mail, telefoni) per ogni record per recuperare le informazioni di cui ho bisogno. Entrambi i metodi sembrano dispendiosi.
Aprire il

51

Restituisce esattamente quello che hai chiesto: un singolo set di record contenente il prodotto cartesiano definito dai join. Ci sono molti scenari validi in cui è esattamente quello che vorresti, quindi dire che SQL sta dando un cattivo risultato (e quindi implica che sarebbe meglio se lo cambiassi) in realtà rovinerebbe molte query.

Ciò che stai vivendo è noto come " Mancata corrispondenza di impedenza oggetto / relazione " , le difficoltà tecniche che derivano dal fatto che il modello di dati orientato agli oggetti e il modello di dati relazionali sono fondamentalmente diversi in diversi modi. LINQ e altri framework (noti come ORM, Object / Relational Mapper, non a caso) non "aggirano magicamente questo"; fanno solo domande diverse. Può essere fatto anche in SQL. Ecco come lo farei:

SELECT * FROM users user where [criteria here]

Scorrere l'elenco degli utenti e creare un elenco di ID.

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

E poi fai il join lato client. Ecco come lo fanno LINQ e altri framework. Non c'è vera magia coinvolta; solo uno strato di astrazione.


14
+1 per "esattamente quello che hai chiesto". Troppo spesso saltiamo alla conclusione che c'è qualcosa che non va nella tecnologia piuttosto che la conclusione che dobbiamo imparare a usare la tecnologia in modo efficace.
Matt,

1
Hibernate recupererà l'entità radice e determinate raccolte in un'unica query quando viene utilizzata la modalità di recupero desideroso per tali raccolte; in quel caso fa la riduzione delle proprietà dell'entità radice in memoria. Altri ORM possono probabilmente fare lo stesso.
Mike Partridge,

3
In realtà questo non è da incolpare del modello relazionale. Grazie molto bene alle relazioni nidificate. Questo è puramente un bug di implementazione nelle prime versioni di SQL. Penso che versioni più recenti lo abbiano aggiunto però.
John Nilsson,

8
Sei sicuro che questo sia un esempio di impedenza relazionale oggetto? Mi sembra che il modello relazionale corrisponda perfettamente al modello di dati concettuali dell'OP: ogni utente è associato a un elenco di zero, uno o più indirizzi e-mail. Tale modello è anche perfettamente utilizzabile in un paradigma OO (aggregazione: l'oggetto utente ha una raccolta di e-mail). Il limite è nella tecnica utilizzata per eseguire query sul database, che è un dettaglio di implementazione. Esistono tecniche di query attorno alle quali restituiscono dati gerarchici
MarkJ

@MarkJ dovresti scriverlo come una risposta.
Mr.Mindor,

12

È possibile utilizzare una funzione integrata per concatenare i record insieme. In MySQL è possibile utilizzare la GROUP_CONCAT()funzione e in Oracle è possibile utilizzare la LISTAGG()funzione.

Ecco un esempio di come potrebbe apparire una query in 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 

Ciò restituirebbe qualcosa del genere

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

Questa sembra essere la soluzione più vicina (in SQL) a ciò che l'OP sta tentando di fare. Potenzialmente dovrà ancora eseguire l'elaborazione lato client per suddividere i risultati EmailAddresses e PhoneNumbers in elenchi.
Mr.Mindor,

2
Che cosa succede se il numero di telefono ha un "tipo", come "Cella", "Casa" o "Lavoro"? Inoltre, le virgole sono tecnicamente consentite negli indirizzi e-mail (se citati): come lo dividerei allora?
mpen

10

Il problema è che sta restituendo il nome dell'utente, DOB, il colore preferito e tutte le altre informazioni memorizzate

Il problema è che non sei abbastanza selettivo. Hai chiesto tutto quando hai detto

Select * from...

... e ce l'hai (compresi DOB e colori preferiti).

Probabilmente dovresti essere un po 'più (ahem) ... selettivo, e dire qualcosa del tipo:

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

È anche possibile che tu possa vedere record che sembrano duplicati perché userpotrebbero unirsi a più emailrecord, ma il campo che distingue questi due non è nella tua Selectdichiarazione, quindi potresti voler dire qualcosa come

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

... ancora e ancora per ogni disco ...

Inoltre, noto che stai facendo un LEFT JOIN. Questo unirà tutti i record a sinistra del join (cioè users) a tutti i record a destra, o in altre parole:

Un join esterno sinistro restituisce tutti i valori da un join interno più tutti i valori nella tabella di sinistra che non corrispondono alla tabella di destra.

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

Quindi un'altra domanda è: hai davvero bisogno di un join sinistro o sarebbe INNER JOINstato sufficiente? Sono tipi di join molto diversi.

Non sarebbe più bello se restituisse una singola riga per ogni utente e all'interno di quel record ci fosse un elenco di e-mail

Se in realtà si desidera che una singola colonna nel set di risultati contenga un elenco che viene generato al volo, ciò può essere fatto ma varia in base al database che si sta utilizzando. Oracle ha la listaggfunzione .


In definitiva, penso che il tuo problema potrebbe essere risolto se riscrivi la tua query vicino a qualcosa del genere:

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'uso di * è scoraggiato ma non il nocciolo del suo problema. Anche se seleziona 0 colonne utente, potrebbe comunque riscontrare un effetto di duplicazione poiché sia ​​i telefoni che le e-mail hanno una relazione 1-molti con gli utenti. Distinct non impedirebbe la visualizzazione di un numero di telefono due volte su phone1/name@hotmail.com, phone1/name@google.com.
mike30,

6
-1: "il problema potrebbe essere risolto", dice che non si sa che effetto avrebbe fatto il passaggio da left joina inner join. In questo caso, ciò non ridurrà le "ripetizioni" di cui l'utente si lamenta; ometterebbe semplicemente quegli utenti a cui manca un telefono o e-mail. quasi nessun miglioramento. inoltre, quando si interpretano "tutti i record a sinistra per tutti i record a destra" salta i ONcriteri, che elimina tutte le relazioni "sbagliate" inerenti al prodotto cartesiano ma mantiene tutti i campi ripetuti.
Javier,

@Javier: Sì, è per questo che ho anche detto che hai davvero bisogno di un join di sinistra o un INNER JOIN sarebbe stato sufficiente? * La descrizione di OP del problema fa sembrare * che si aspettassero il risultato di un join interno. Naturalmente, senza dati di esempio o una descrizione di ciò che volevano veramente , è difficile dirlo. Ho dato il suggerimento perché in realtà ho visto persone (quelle con cui lavoro) fare questo: scegliere il join sbagliato e poi lamentarsi quando non capiscono i risultati che ottengono. Avendolo visto , ho pensato che potesse essere successo qui.
FrustratedWithFormsDesigner

3
Ti manca il punto della domanda. In questo esempio ipotetico, io voglio tutti i dati utente (nome, data di nascita, ecc) e voglio che tutti i suoi / suoi numeri di telefono. Un join interno esclude gli utenti senza e-mail o senza telefoni: in che modo aiuta?
mpen

4

Le query producono sempre una serie di dati tabulari rettangolari (non frastagliati). Non ci sono sottoinsiemi nidificati all'interno di un set. Nel mondo degli insiemi ogni cosa è un puro rettangolo non annidato.

Puoi pensare a un join come mettere 2 set fianco a fianco. La condizione "on" è la corrispondenza dei record di ciascun set. Se un utente ha 3 numeri di telefono, vedrai una duplicazione di 3 volte nelle informazioni sull'utente. Un query rettangolare non frastagliata deve essere prodotta dalla query. È semplicemente la natura di unire i set con una relazione 1 a molti.

Per ottenere ciò che desideri, devi utilizzare una query separata come quella descritta da Mason Wheeler.

select * from Phones where user_id=344;

Il risultato di questa query è ancora un set non frastagliato rettangolo. Come tutto nel mondo dei set.


2

Devi decidere dove esistono i colli di bottiglia. La larghezza di banda tra il database e l'applicazione è in genere piuttosto rapida. Non c'è motivo per cui la maggior parte dei database non sia in grado di restituire 3 set di dati separati in una chiamata e nessun join. Quindi puoi unirti a tutti insieme nella tua app se vuoi.

Altrimenti, si desidera che il database unisca questo set di dati e quindi rimuova tutti i valori ripetuti in ogni riga che sono il risultato dei join e non necessariamente le righe stesse che hanno dati duplicati come due persone con lo stesso nome o numero di telefono. Sembra un sacco di spese generali per risparmiare sulla larghezza di banda. Sarebbe meglio concentrarsi sulla restituzione di meno dati con un migliore filtraggio e la rimozione delle colonne non necessarie. Perché Select * non viene mai utilizzato in produzione, dipende da ciò.


"Non c'è motivo per cui la maggior parte dei database non sia in grado di restituire 3 set di dati separati all'interno di una chiamata e nessun join" - Come si ottiene per restituire 3 set di dati separati con una chiamata? Pensavo che dovessi inviare 3 query diverse, il che introduce la latenza tra ognuna?
Aprire il

Una procedura memorizzata può essere chiamata in 1 transazione e quindi restituire tutti i set di dati desiderati. Forse è necessario uno sproc "SelectUserWithEmailsPhones".
Graham,

1
@Mark: è possibile inviare (almeno nel server sql) più di un comando come parte dello stesso batch. cmdText = "seleziona * da b; seleziona * da a; seleziona * da c", quindi usalo come testo del comando per il comando sql.
jmoreno,

2

Molto semplicemente, non unire i tuoi dati se desideri risultati distinti per una query dell'utente e una query del numero di telefono, altrimenti come altri hanno sottolineato "Imposta" o i dati conterranno campi aggiuntivi per ogni riga.

Emettere 2 query distinte anziché una con un join.

Nella procedura memorizzata o nelle query sql craft 2 in linea con parametri e restituire i risultati di entrambi. La maggior parte dei database e delle lingue supporta più set di risultati.

Ad esempio, SQL Server e C # eseguono questa funzionalità utilizzando IDataReader.NextResult().


1

Ti stai perdendo qualcosa. Se vuoi denormalizzare i tuoi dati, devi farlo da solo.

;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

Il concetto di chiusura relazionale significa sostanzialmente che il risultato di qualsiasi query è una relazione che può essere utilizzata in altre query come se fosse una tabella di base. Questo è un concetto potente perché rende le query componibili.

Se SQL ti consentisse di scrivere query che generassero strutture di dati nidificate, questo principio sarebbe stato infranto. Una struttura di dati nidificati non è una relazione, pertanto è necessario un nuovo linguaggio di query o estensioni complesse a SQL, per interrogarlo ulteriormente o unire ad esso quali altre relazioni.

Fondamentalmente si dovrebbe costruire un DBMS gerarchico sopra un DBMS relazionale. Sarà molto più complesso per un beneficio discutibile e perderai i vantaggi di un sistema relazionale coerente.

Capisco perché a volte sarebbe conveniente essere in grado di generare dati strutturati gerarchicamente da SQL, ma il costo nella complessità aggiunta in tutto il DBMS per supportare questo non vale sicuramente la pena.


-4

Si prega di fare riferimento all'uso della funzione STUFF che raggruppa più righe (numeri di telefono) di una colonna (contatto) che può essere estratta come una singola cella di valori delimitati di una riga (utente).

Oggi lo utilizziamo ampiamente, ma affrontiamo alcuni problemi di CPU e prestazioni. Il tipo di dati XML è un'altra opzione, ma è una modifica di progettazione non a livello di query.


5
Ti preghiamo di espandere come questo risolve la domanda. Piuttosto che dire "Si prega di fare riferimento all'uso di", fornire un esempio di come ciò raggiungerebbe la domanda posta. Può anche essere utile citare fonti di terze parti in cui rende le cose più chiare.
bitsoflogic,

1
Sembra che STUFFsia simile a unire. Non sono sicuro di come questo si applica alla mia domanda.
mpen
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.