Dlaczego RDBMS nie zwracają połączonych tabel w formacie zagnieżdżonym?

14

Powiedzmy na przykład, że chcę pobrać użytkownika oraz wszystkie jego numery telefonów i adresy e-mail. Numery telefonów i e-maile są przechowywane w osobnych tabelach, jeden użytkownik do wielu telefonów / e-maili. Mogę to zrobić dość łatwo:

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

Problem * polega na tym, że zwraca nazwę użytkownika, DOB, ulubiony kolor i wszystkie inne informacje przechowywane w tabeli użytkowników w kółko dla każdego rekordu (użytkownicy wysyłają e-maile z rekordów przez telefon), prawdopodobnie pochłaniając przepustowość i spowalniając w dół wyników.

Czy nie byłoby lepiej, gdyby zwrócił jeden wiersz dla każdego użytkownika, a w tym rekordzie znajdowała się lista e-maili i lista telefonów? Ułatwi to również pracę z danymi.

Wiem, że możesz uzyskać takie wyniki za pomocą LINQ lub być może innych frameworków, ale wydaje się, że jest to słabość w projekcie baz relacyjnych baz danych.

Możemy to obejść za pomocą NoSQL, ale czy nie powinno być czegoś pośredniego?

Czy coś brakuje? Dlaczego to nie istnieje?

* Tak, jest zaprojektowany w ten sposób. Rozumiem. Zastanawiam się, dlaczego nie ma alternatywy, z którą łatwiej byłoby pracować. SQL może nadal robić to, co robi, ale może dodać słowo kluczowe lub dwa, aby wykonać trochę przetwarzania końcowego, które zwraca dane w formacie zagnieżdżonym zamiast produktu kartezjańskiego.

Wiem, że można to zrobić w wybranym przez Ciebie języku skryptowym, ale wymaga to, aby serwer SQL wysłał zbędne dane (przykład poniżej) lub abyś mógł zadawać wiele takich zapytań SELECT email FROM emails WHERE user_id IN (/* result of first query */).


Zamiast zwracania przez MySQL czegoś podobnego do tego:

[
    {
        "name": "John Smith",
        "dob": "1945-05-13",
        "fav_color": "red",
        "email": "[email protected]",
    },
    {
        "name": "John Smith",
        "dob": "1945-05-13",
        "fav_color": "red",
        "email": "[email protected]",
    },
    {
        "name": "Jane Doe",
        "dob": "1953-02-19",
        "fav_color": "green",
        "email": "[email protected]",
    }
]

A następnie trzeba zgrupować na jednym unikalnym identyfikatorze (co oznacza, że ​​ja też muszę go pobrać!) Po stronie klienta, aby sformatować zestaw wyników w odpowiedni sposób, po prostu zwróć to:

[
    {
        "name": "John Smith",
        "dob": "1945-05-13",
        "fav_color": "red",
        "emails": ["[email protected]", "[email protected]"]
    },
    {
        "name": "Jane Doe",
        "dob": "1953-02-19",
        "fav_color": "green",
        "emails": ["[email protected]"],
    }
]

Alternatywnie mogę zadać 3 zapytania: 1 dla użytkowników, 1 dla e-maili i 1 dla numerów telefonów, ale następnie zestawy wyników dla adresu e-mail i numeru telefonu muszą zawierać identyfikator_użytkownika, aby móc dopasować je z powrotem do użytkowników Wcześniej ściągałem. Znowu zbędne dane i niepotrzebne przetwarzanie końcowe.

mpen
źródło
6
Pomyśl o SQL jako arkuszu kalkulacyjnym, jak w Microsoft Excel, a następnie spróbuj dowiedzieć się, jak utworzyć wartość komórki zawierającą komórki wewnętrzne. Nie działa już dobrze jako arkusz kalkulacyjny. To, czego szukasz, to struktura drzewa, ale wtedy nie masz już zalet arkusza kalkulacyjnego (tzn. Nie możesz sumować kolumny w drzewie). Struktury drzewiaste nie tworzą raportów bardzo czytelnych dla człowieka.
Reactgular,
54
SQL nie jest zły w zwracaniu danych, jesteś kiepski w pytaniu o to, czego chcesz. Zasadniczo, jeśli uważasz, że powszechnie używane narzędzie jest wadliwe lub uszkodzone w typowym przypadku, problem dotyczy Ciebie.
Sean McSomething,
12
@SeanMcSomething Tak prawdziwe, że to boli, sam nie powiedziałbym tego lepiej.
WernerCD,
5
To świetne pytania. Odpowiedzi, które mówią „tak to jest”, nie mają sensu. Dlaczego nie można zwrócić wierszy z osadzonymi zbiorami wierszy?
Chris Pitman
8
@SeanMcSomething: Chyba że tym powszechnie używanym narzędziem jest C ++ lub PHP, w którym to przypadku prawdopodobnie masz rację. ;)
Mason Wheeler,

Odpowiedzi:

11

Głęboko, w trzewiach relacyjnej bazy danych, wszystkie wiersze i kolumny. Jest to struktura, z którą relacyjna baza danych jest zoptymalizowana do pracy. Kursory pracują jednocześnie w poszczególnych rzędach. Niektóre operacje tworzą tabele tymczasowe (znowu muszą to być wiersze i kolumny).

Pracując tylko z wierszami i zwracając tylko te wiersze, system jest w stanie lepiej radzić sobie z pamięcią i ruchem sieciowym.

Jak wspomniano, pozwala to na pewne optymalizacje (indeksy, złączenia, związki itp.)

Jeśli chcemy mieć zagnieżdżoną strukturę drzewa, wymaga to pobrania wszystkich danych naraz. Zniknęły optymalizacje kursorów po stronie bazy danych. Podobnie ruch w sieci staje się jedną dużą serią, która może trwać znacznie dłużej niż powolny strużka rząd po rzędzie (jest to coś, co czasami jest tracone w dzisiejszym świecie internetowym).

Każdy język ma w sobie tablice. Są to łatwe w obsłudze elementy i interfejs. Dzięki zastosowaniu bardzo prymitywnej struktury sterownik między bazą danych a programem - bez względu na język - może działać we wspólny sposób. Gdy zaczniesz dodawać drzewa, struktury w języku stają się bardziej złożone i trudniejsze do przejścia.

Język programowania nie jest tak trudny do konwersji wierszy zwróconych do innej struktury. Zrób z niego drzewo lub zestaw skrótów lub pozostaw go jako listę wierszy, nad którymi możesz iterować.

Działa tu także historia. Przenoszenie uporządkowanych danych było czymś brzydkim w dawnych czasach. Spójrz na format EDI, aby dowiedzieć się, o co możesz prosić. Drzewa sugerują również rekurencję - której niektóre języki nie obsługiwały (dwa najważniejsze języki dawnych czasów nie obsługiwały rekurencji - rekursja nie weszła do Fortran aż do F90, a epoki COBOL też nie).

I chociaż dzisiejsze języki obsługują rekurencję i bardziej zaawansowane typy danych, tak naprawdę nie ma dobrego powodu, aby to zmieniać. Działają i działają dobrze. Te, które zmienne rzeczy są NoSQL baz danych. Możesz przechowywać drzewa w dokumentach w dokumencie opartym na dokumencie. LDAP (tak naprawdę stary) jest również systemem opartym na drzewach (choć prawdopodobnie nie jest tym, czego szukasz). Kto wie, może następną rzeczą w bazach nosql będzie ta, która zwróci zapytanie jako obiekt json.

Jednak „stare” relacyjne bazy danych ... pracują z wierszami, ponieważ w tym są dobrzy i wszystko może z nimi rozmawiać bez problemów lub tłumaczenia.

  1. W projektowaniu protokołu doskonałość została osiągnięta nie wtedy, gdy nie ma już nic do dodania, ale kiedy nie ma już nic do zabrania.

Od RFC 1925 - Dwanaście prawd sieciowych


źródło
„Jeśli chcemy mieć zagnieżdżoną strukturę drzewa, wymaga to pobrania wszystkich danych jednocześnie. Nie ma już optymalizacji dla kursorów po stronie bazy danych”. - To nie brzmi prawdziwie. Musiałby po prostu utrzymać kilka kursorów: jeden dla głównego stołu, a następnie jeden dla każdego połączonego stołu. W zależności od interfejsu może zwracać jeden wiersz i wszystkie połączone tabele w jednym kawałku (częściowo przesyłane strumieniowo) lub może przesyłać strumieniowo poddrzewa (a może nawet nie pytać o nie), dopóki nie zaczniesz ich iterować. Ale tak, to bardzo komplikuje sprawy.
mpen
3
Każdy współczesny język powinien mieć jakąś klasę drzewa, prawda? I czy to nie zależy od kierowcy? Myślę, że chłopaki SQL nadal muszą zaprojektować wspólny format (niewiele o tym wiedzą). Rzeczą, która mnie przyciąga, jest to, że albo muszę wysłać 1 zapytanie z połączeniami, a następnie wrócić i odfiltrować zbędne dane, które każdy wiersz (informacje o użytkowniku, który zmienia tylko co N-ty wiersz), lub wysłać 1 zapytanie (użytkownicy) i przeglądaj wyniki, a następnie wyślij dwa kolejne zapytania (e-maile, telefony) dla każdego rekordu, aby pobrać potrzebne informacje. Każda z tych metod wydaje się marnotrawstwem.
mpen
51

Zwraca dokładnie to, o co prosiłeś: pojedynczy zestaw rekordów zawierający iloczyn kartezjański zdefiniowany przez złączenia. Istnieje wiele prawidłowych scenariuszy, w których dokładnie tego byś chciał, więc powiedzenie, że SQL daje zły wynik (a tym samym sugeruje, że byłoby lepiej, gdybyś go zmienił) faktycznie sprowadziłoby wiele zapytań.

To, czego doświadczasz, jest znane jako „ niedopasowanie obiektów / relacyjnych impedancji ”, trudności techniczne wynikające z faktu, że model danych obiektowych i model danych relacyjnych różnią się zasadniczo na kilka sposobów. LINQ i inne frameworki (znane jako ORM, obiektowe / relacyjne mapowania, nieprzypadkowo) nie magicznie „omijają to”; po prostu wydają różne zapytania. Można to zrobić również w języku SQL. Oto jak bym to zrobił:

SELECT * FROM users user where [criteria here]

Iteruj listę użytkowników i stwórz listę identyfikatorów.

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

A potem robisz dołączanie po stronie klienta. W ten sposób robią to LINQ i inne frameworki. Nie ma w tym żadnej prawdziwej magii; tylko warstwa abstrakcji.

Mason Wheeler
źródło
14
+1 za „dokładnie to, o co prosiłeś”. Zbyt często dochodzimy do wniosku, że coś jest nie tak z technologią, niż do wniosku, że musimy nauczyć się efektywnie korzystać z technologii.
Mat.
1
Hibernacji pobiera podmiot głównego i pewnych zbiorów w jednym zapytania gdy pragnie tryb pobierania danych stosuje się do tych zbiorów; w takim przypadku redukuje właściwości jednostki głównej w pamięci. Inne ORM mogą prawdopodobnie zrobić to samo.
Mike Partridge
3
W rzeczywistości nie można tego winić modelu relacyjnego. Bardzo dobrze radzi sobie z zagnieżdżonymi relacjami, dziękuję. Jest to wyłącznie błąd implementacyjny we wczesnych wersjach SQL. Myślę jednak, że dodały go nowsze wersje.
John Nilsson,
8
Czy na pewno jest to przykład impedancji obiektowo-relacyjnej? Wydaje mi się, że model relacyjny idealnie pasuje do koncepcyjnego modelu danych PO: każdy użytkownik jest powiązany z listą zero, jednego lub więcej adresów e-mail. Model ten doskonale nadaje się również do zastosowania w paradygmacie OO (agregacja: obiekt użytkownika ma kolekcję wiadomości e-mail). Ograniczeniem jest technika wykorzystywana do zapytania do bazy danych, która jest szczegółem implementacji. Istnieją techniki zapytań, wokół których zwracane są dane heirarchiczne, np.
Heirarchiczne DataSets
@ MarkJ powinieneś napisać to jako odpowiedź.
Mr.Mindor,
12

Możesz użyć wbudowanej funkcji, aby połączyć rekordy razem. W MySQL możesz użyć tej GROUP_CONCAT()funkcji, aw Oracle możesz użyć tej LISTAGG()funkcji.

Oto przykład, jak mogłoby wyglądać zapytanie w 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 

To zwróci coś w rodzaju

username    department       EmailAddresses                        PhoneNumbers
Tim_Burton  Human Resources  hr@m.com, tb@me.com, nunya@what.com   231-123-1234, 231-123-1235
Marudzić
źródło
To wydaje się być najbliższym rozwiązaniem (w SQL) do tego, co OP próbuje zrobić. Potencjalnie nadal będzie musiał wykonać przetwarzanie po stronie klienta, aby rozbić wyniki EmailAddresses i PhoneNumbers na listy.
Mr.Mindor,
2
Co się stanie, jeśli numer telefonu ma „typ”, na przykład „Komórka”, „Dom” lub „Praca”? Co więcej, przecinki są technicznie dozwolone w adresach e-mail (jeśli są cytowane) - jak więc mam to podzielić?
mpen
10

Problem polega na tym, że zwraca nazwę użytkownika, DOB, ulubiony kolor i wszystkie inne przechowywane informacje

Problem polega na tym, że nie jesteś wystarczająco selektywny. Prosiłeś o wszystko, kiedy mówiłeś

Select * from...

... i masz to (w tym DOB i ulubione kolory).

Prawdopodobnie powinieneś być trochę bardziej (ahm) ... selektywny i powiedział coś w stylu:

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

Możliwe jest również, że zobaczysz rekordy, które wyglądają jak duplikaty, ponieważ usermogą dołączyć do wielu emailrekordów, ale pole, które je rozróżnia, nie znajduje się w Selectwyciągu, więc możesz chcieć powiedzieć coś w rodzaju

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

... w kółko dla każdej płyty ...

Zauważam też, że robisz LEFT JOIN. Spowoduje to połączenie wszystkich rekordów po lewej stronie sprzężenia (tj. users) Do wszystkich rekordów po prawej stronie lub innymi słowy:

Lewe połączenie zewnętrzne zwraca wszystkie wartości z połączenia wewnętrznego plus wszystkie wartości w lewej tabeli, które nie pasują do prawej tabeli.

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

Kolejnym pytaniem jest to, czy faktycznie potrzebujesz lewego złączenia, czy może INNER JOINwystarczyłoby? Są to bardzo różne rodzaje złączeń.

Nie byłoby miło, gdyby zwrócił jeden wiersz dla każdego użytkownika, aw tym rekordzie znajdowała się lista e-maili

Jeśli chcesz, aby pojedyncza kolumna w zestawie wyników zawierała listę generowaną w locie, można to zrobić, ale różni się ona w zależności od używanej bazy danych. Oracle ma tę listaggfunkcję .


Ostatecznie myślę, że Twój problem może zostać rozwiązany, jeśli przepiszesz zapytanie blisko czegoś takiego:

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
FrustratedWithFormsDesigner
źródło
1
używanie * jest odradzane, ale nie sedno jego problemu. Nawet jeśli wybierze 0 kolumn użytkowników, nadal może wystąpić efekt duplikacji, ponieważ zarówno telefony, jak i e-maile mają 1-wiele relacji z użytkownikami. Wyraźny nie zapobiegnie dwukrotnemu pojawianiu się numeru telefonu ala phone1/[email protected], phone1/[email protected].
mike30
6
-1: „Twój problem może zostać rozwiązany” mówi, że nie wiesz, jaki efekt zmieni się z left joinna inner join. W takim przypadku nie ograniczy to „powtórzeń”, na które skarży się użytkownik; po prostu pomija użytkowników, którzy nie mają telefonu ani e-maila. prawie żadna poprawa. również przy interpretacji „wszystkich rekordów po lewej stronie do wszystkich rekordów po prawej stronie” pomija ONkryteria, które oczyszczają wszystkie „złe” relacje właściwe dla produktu kartezjańskiego, ale zachowują wszystkie powtarzające się pola.
Javier
@Javier: Tak, dlatego też powiedziałem, czy tak naprawdę potrzebujesz lewego połączenia, czy INNER JOIN byłby wystarczający? * Opis problemu przez OP sprawia, że ​​* brzmi to tak, jakby oczekiwali wyniku połączenia wewnętrznego. Oczywiście bez żadnych przykładowych danych lub opisu tego, czego naprawdę chcieli, trudno powiedzieć. Zasugerowałem, ponieważ faktycznie widziałem, jak ludzie (z którymi pracuję) robią to: wybierz niewłaściwe dołączenie, a następnie narzekaj, gdy nie rozumieją wyników, które otrzymują. Po widział go, pomyślałem, że może to się stało tutaj.
FrustratedWithFormsDesigner
3
Nie rozumiesz sedna pytania. W tym hipotetycznym przykładzie, ja chcę wszystkie dane użytkownika (nazwa, DOB, etc) i chcę wszystkie jego / jej numer telefonu. Połączenie wewnętrzne wyklucza użytkowników bez e-maili lub telefonów - jak to pomaga?
mpen
4

Kwerendy zawsze tworzą prostokątny (nie strzępiony) zestaw danych tabelarycznych. W zestawie nie ma zagnieżdżonych podzestawów. W świecie zestawów wszystko jest czystym nie zagnieżdżonym prostokątem.

Możesz pomyśleć o złączeniu jako umieszczeniu 2 zestawów obok siebie. Warunkiem włączenia jest dopasowanie rekordów w każdym zestawie. Jeśli użytkownik ma 3 numery telefonów, w informacjach o użytkowniku zobaczysz 3-krotne duplikowanie. Kwerenda musi wygenerować prostokątny zestaw bez postrzępień. To po prostu natura łączenia zestawów z relacją jeden do wielu.

Aby uzyskać to, czego potrzebujesz, musisz użyć osobnego zapytania, takiego jak opisany Mason Wheeler.

select * from Phones where user_id=344;

Wynikiem tego zapytania jest wciąż prostokątny zbiór bez postrzępień. Jak wszystko w świecie zestawów.

mike30
źródło
2

Musisz zdecydować, gdzie istnieją wąskie gardła. Przepustowość między bazą danych a aplikacją jest zwykle dość duża. Nie ma powodu, dla którego większość baz danych nie mogła zwrócić 3 oddzielnych zestawów danych w ramach jednego połączenia i żadnych połączeń. Następnie możesz dołączyć do tego wszystkiego razem w swojej aplikacji, jeśli chcesz.

W przeciwnym razie chcesz, aby baza danych połączyła ten zestaw danych, a następnie usunęła wszystkie powtarzające się wartości w każdym wierszu, które są wynikiem złączeń i niekoniecznie same wiersze zawierające zduplikowane dane, takie jak dwie osoby o tej samej nazwie lub numerze telefonu. Wydaje się, że dużo narzutów pozwala zaoszczędzić na przepustowości. Lepiej skoncentruj się na zwracaniu mniejszej ilości danych dzięki lepszemu filtrowaniu i usuwaniu niepotrzebnych kolumn. Ponieważ Select * nigdy nie jest stosowany w studniach produkcyjnych, które zależą.

JeffO
źródło
„Nie ma powodu, dla którego większość baz danych nie mogła zwrócić 3 oddzielnych zestawów danych w ramach jednego połączenia i żadnych połączeń” - Jak uzyskać zwrot 3 oddzielnych zestawów danych za jednym razem? Myślałem, że musisz wysłać 3 różne zapytania, które wprowadzają opóźnienia między nimi?
mpen
Procedurę składowaną można wywołać w 1 transakcji, a następnie zwrócić tyle zestawów danych, ile chcesz. Być może potrzebny jest pakiet „SelectUserWithEmailsPhones”.
Graham
1
@ Mark: możesz wysłać (przynajmniej na serwerze SQL) więcej niż jedno polecenie w ramach tej samej partii. cmdText = "wybierz * z b; wybierz * z a; wybierz * z c", a następnie użyj tego jako tekstu polecenia dla polecenia sql.
jmoreno
2

Bardzo prosto, nie dołączaj swoich danych, jeśli chcesz uzyskać odrębne wyniki dla zapytania użytkownika i zapytania numeru telefonu, w przeciwnym razie, jak zauważyli inni, „Zestaw” lub dane będą zawierać dodatkowe pola dla każdego wiersza.

Zadaj 2 różne zapytania zamiast jednego ze złączeniem.

W procedurze przechowywanej lub sparametryzowanej kwerendie sql craft 2 zapytania i zwróć wyniki obu z powrotem. Większość baz danych i języków obsługuje wiele zestawów wyników.

Na przykład SQL Server i C # realizują tę funkcję za pomocą IDataReader.NextResult().

Jon Raynor
źródło
1

Coś brakuje. Jeśli chcesz zdenormalizować swoje dane, musisz to zrobić sam.

;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
jmoreno
źródło
1

Pojęcie zamknięcia relacyjnego zasadniczo oznacza, że ​​wynikiem każdego zapytania jest relacja, która może być używana w innych zapytaniach, tak jakby była tabelą podstawową. Jest to potężna koncepcja, ponieważ sprawia, że ​​zapytania można komponować.

Jeśli SQL pozwoli ci pisać zapytania, które generują zagnieżdżone struktury danych, złamiesz tę zasadę. Zagnieżdżona struktura danych nie jest relacją, więc potrzebujesz nowego języka zapytań lub złożonych rozszerzeń SQL, aby móc dalej wyszukiwać lub dołączać do innych relacji.

Zasadniczo zbudowałbyś hierarchiczny DBMS na bazie relacyjnego DBMS. Będzie to znacznie bardziej skomplikowane dla wątpliwej korzyści, a ty stracisz zalety konsekwentnie relacyjnego systemu.

Rozumiem, dlaczego czasami wygodniej jest wyprowadzać hierarchicznie ustrukturyzowane dane z SQL, ale koszt dodatkowej złożoności w całym systemie DBMS z pewnością nie jest tego wart.

JacquesB
źródło
-4

Pls odnoszą się do użycia funkcji STUFF, która grupuje wiele wierszy (numery telefonów) kolumny (kontaktu), które można wyodrębnić jako pojedynczą komórkę z ograniczonymi wartościami wiersza (użytkownika).

Dzisiaj intensywnie z tego korzystamy, ale napotykamy na problemy z wysoką wydajnością i procesorem. Typ danych XML jest inną opcją, ale jest zmianą projektu, a nie poziomem zapytania.

Shriram Rajagopal
źródło
5
Proszę rozwinąć, w jaki sposób rozwiązuje to pytanie. Zamiast mówić do „Pls odnoszą się do użycia”, podaj przykład, w jaki sposób uzyskałoby to zadane pytanie. Pomocne może być również cytowanie źródeł innych firm, w których jest to bardziej zrozumiałe.
bitsoflogic
1
Wygląda na STUFFto, że jest podobny do łączenia. Nie jestem pewien, jak to odnosi się do mojego pytania.
mpen