Mengapa RDBMS tidak mengembalikan tabel bergabung dalam format bersarang?


14

Misalnya, katakan saya ingin mengambil Pengguna dan semua nomor telepon serta alamat emailnya. Nomor-nomor telepon dan email disimpan dalam tabel terpisah, Satu pengguna ke banyak telepon / email. Saya bisa melakukan ini dengan mudah:

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

Masalah * dengan ini adalah bahwa itu mengembalikan nama pengguna, DOB, warna favorit, dan semua informasi lain yang disimpan dalam tabel pengguna berulang-ulang untuk setiap catatan (pengguna email catatan telepon), mungkin memakan bandwidth dan memperlambat bawah hasilnya.

Bukankah lebih baik jika mengembalikan satu baris untuk setiap pengguna, dan dalam catatan itu ada daftar email dan daftar telepon? Itu akan membuat data lebih mudah untuk dikerjakan.

Saya tahu Anda bisa mendapatkan hasil seperti ini menggunakan LINQ atau mungkin kerangka kerja lain, tetapi tampaknya menjadi kelemahan dalam desain yang mendasari database relasional.

Kita bisa menyiasatinya dengan menggunakan NoSQL, tetapi tidakkah seharusnya ada jalan tengah?

Apakah saya melewatkan sesuatu? Mengapa ini tidak ada?

* Ya, didesain seperti ini. Saya mengerti. Saya bertanya-tanya mengapa tidak ada alternatif yang lebih mudah untuk dikerjakan. SQL dapat terus melakukan apa yang dilakukannya tetapi kemudian mereka dapat menambahkan satu atau dua kata kunci untuk melakukan sedikit pemrosesan pasca yang mengembalikan data dalam format bersarang alih-alih produk kartesius.

Saya tahu ini dapat dilakukan dalam bahasa scripting pilihan Anda, tetapi mengharuskan server SQL baik mengirim data yang berlebihan (contoh di bawah) atau agar Anda mengeluarkan beberapa pertanyaan seperti SELECT email FROM emails WHERE user_id IN (/* result of first query */).


Alih-alih meminta MySQL mengembalikan sesuatu yang mirip dengan ini:

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

Dan kemudian harus mengelompokkan pada beberapa pengidentifikasi unik (yang berarti saya perlu mengambil itu juga!) Sisi klien untuk memformat ulang hasil yang ditetapkan seperti yang Anda inginkan, cukup kembalikan ini:

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

Sebagai alternatif, saya dapat mengeluarkan 3 pertanyaan: 1 untuk pengguna, 1 untuk email, dan 1 untuk nomor telepon, tetapi kemudian set hasil email dan nomor telepon harus berisi user_id sehingga saya dapat mencocokkannya kembali dengan pengguna. Saya sebelumnya mengambil. Sekali lagi, data yang berlebihan dan pasca pengolahan yang tidak perlu.


6
Pikirkan SQL sebagai spreadsheet, seperti di Microsoft Excel, lalu coba cari tahu cara membuat nilai sel yang berisi sel dalam. Ini tidak lagi berfungsi dengan baik sebagai spreadsheet. Apa yang Anda cari adalah struktur pohon, tetapi kemudian Anda tidak lagi memiliki manfaat spreadsheet (yaitu Anda tidak dapat total kolom di pohon). Struktur pohon tidak membuat laporan yang sangat bisa dibaca manusia.
Reactgular

54
SQL tidak buruk dalam mengembalikan data, Anda buruk dalam menanyakan apa yang Anda inginkan. Sebagai aturan praktis, jika Anda berpikir alat yang banyak digunakan adalah buggy atau rusak untuk kasus penggunaan umum, masalahnya adalah Anda.
Sean McSomething

12
@SeanMcSomething Begitu benar sampai sakit, saya sendiri tidak bisa mengatakannya dengan lebih baik.
WernerCD

5
Ini pertanyaan yang bagus. Jawaban yang mengatakan "begini adanya" tidak ada gunanya. Mengapa tidak mungkin mengembalikan baris dengan koleksi baris yang disematkan?
Chris Pitman

8
@SeanMcSomething: Kecuali jika alat yang banyak digunakan adalah C ++ atau PHP, dalam hal ini Anda mungkin benar. ;)
Mason Wheeler

Jawaban:


11

Jauh di lubuk hati, dalam nyali database relasional, semua baris dan kolomnya. Itu adalah struktur di mana database relasional dioptimalkan untuk bekerja dengannya. Kursor bekerja pada baris individual pada satu waktu. Beberapa operasi membuat tabel sementara (sekali lagi, perlu baris dan kolom).

Dengan bekerja hanya dengan baris dan hanya mengembalikan baris, sistem dapat menangani lebih baik dengan memori dan lalu lintas jaringan.

Seperti yang disebutkan, ini memungkinkan dilakukannya optimasi tertentu (indeks, gabungan, serikat pekerja, dll ...)

Jika seseorang ingin struktur pohon bersarang, ini mengharuskan seseorang menarik semua data sekaligus. Lewatlah sudah optimasi untuk kursor di sisi database. Demikian juga, lalu lintas melalui jaringan menjadi satu ledakan besar yang bisa memakan waktu lebih lama daripada tetesan lambat baris demi baris (ini adalah sesuatu yang kadang-kadang hilang di dunia web saat ini).

Setiap bahasa memiliki array di dalamnya. Ini adalah hal-hal mudah untuk bekerja dan berinteraksi dengan Dengan menggunakan struktur yang sangat primitif, penggerak antara database dan program - tidak peduli bahasa apa - dapat bekerja dengan cara yang sama. Begitu seseorang mulai menambahkan pohon, struktur dalam bahasa menjadi lebih kompleks dan lebih sulit untuk dilintasi.

Tidaklah sulit bagi bahasa pemrograman untuk mengubah baris yang dikembalikan ke struktur lain. Jadikan itu menjadi pohon atau hash set atau biarkan sebagai daftar baris yang bisa Anda ulangi.

Ada juga sejarah yang bekerja di sini. Mentransfer data terstruktur adalah sesuatu yang buruk di masa lalu. Lihatlah format EDI untuk mendapatkan gambaran tentang apa yang mungkin Anda minta. Pohon juga menyiratkan rekursi - yang beberapa bahasa tidak mendukung (dua bahasa paling penting di masa lalu tidak mendukung rekursi - rekursi tidak memasuki Fortran sampai F90 dan era COBOL tidak melakukannya).

Dan sementara bahasa saat ini memiliki dukungan untuk rekursi dan tipe data yang lebih maju, sebenarnya tidak ada alasan yang baik untuk mengubah banyak hal. Mereka bekerja, dan mereka bekerja dengan baik. Orang-orang yang sedang mengubah hal-hal yang database NoSQL. Anda dapat menyimpan pohon dalam dokumen dalam satu dokumen berbasis. LDAP (yang sebenarnya sudah tua) juga merupakan sistem berbasis pohon (meskipun mungkin bukan yang Anda cari). Siapa tahu, mungkin hal berikutnya dalam database nosql akan menjadi salah satu yang mengembalikan kembali kueri sebagai objek json.

Namun, database relasional 'lama' ... mereka bekerja dengan baris karena itulah yang mereka kuasai dan semuanya dapat berbicara dengan mereka tanpa masalah atau terjemahan.

  1. Dalam desain protokol, kesempurnaan telah dicapai bukan ketika tidak ada yang tersisa untuk ditambahkan, tetapi ketika tidak ada yang tersisa untuk diambil.

Dari RFC 1925 - The Twelve Networking Truths


"Jika seseorang menginginkan struktur pohon bersarang, ini mengharuskan seseorang menarik semua data sekaligus. Lewatlah sudah optimasi untuk kursor di sisi database." - Itu tidak terdengar benar. Itu hanya harus mempertahankan beberapa kursor: satu untuk tabel utama, dan kemudian satu untuk setiap tabel yang bergabung. Bergantung pada antarmuka, mungkin mengembalikan satu baris dan semua tabel bergabung dalam satu chunk (sebagian streaming), atau dapat melakukan streaming subtree (dan mungkin bahkan tidak meminta mereka) sampai Anda mulai mengulanginya. Tapi ya, banyak hal yang menyulitkan.
mpen

3
Setiap bahasa modern seharusnya memiliki semacam kelas pohon, bukan? Dan bukankah itu tergantung pada pengemudi untuk menghadapinya? Saya kira orang-orang SQL masih perlu merancang format umum (tidak tahu banyak tentang itu). Hal yang membuat saya demikian adalah bahwa saya harus mengirim 1 permintaan dengan bergabung, dan kembali dan memfilter data berlebihan yang setiap baris (info pengguna, yang hanya mengubah setiap baris ke-N), atau mengeluarkan 1 permintaan (pengguna) , dan ulangi hasilnya, lalu kirim dua pertanyaan lagi (email, telepon) untuk setiap catatan untuk mengambil info yang saya butuhkan. Metode mana pun tampaknya boros.
mpen

51

Ini mengembalikan persis apa yang Anda minta: satu set rekaman berisi produk Cartesian yang ditentukan oleh gabungan. Ada banyak skenario yang valid di mana itu persis apa yang Anda inginkan, sehingga mengatakan bahwa SQL memberikan hasil yang buruk (dan dengan demikian menyiratkan bahwa akan lebih baik jika Anda mengubahnya) sebenarnya akan mengacaukan banyak permintaan.

Apa yang Anda alami dikenal sebagai " Object / Relational Impedance Mismatch, " kesulitan teknis yang timbul dari kenyataan bahwa model data berorientasi objek dan model data relasional secara fundamental berbeda dalam beberapa cara. LINQ dan kerangka kerja lainnya (dikenal sebagai ORM, Object / Relational Mappers, bukan secara kebetulan,) tidak secara ajaib "menyiasati ini;" mereka hanya mengeluarkan pertanyaan yang berbeda. Itu bisa dilakukan dalam SQL juga. Begini cara saya melakukannya:

SELECT * FROM users user where [criteria here]

Iterasi daftar pengguna dan buat daftar ID.

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

Dan kemudian Anda melakukan sisi klien bergabung. Ini adalah bagaimana LINQ dan kerangka kerja lain melakukannya. Tidak ada sihir nyata yang terlibat; hanya lapisan abstraksi.


14
+1 untuk "apa yang Anda minta". Terlalu sering kita melompat ke kesimpulan bahwa ada sesuatu yang salah dengan teknologi daripada kesimpulan bahwa kita perlu belajar bagaimana menggunakan teknologi secara efektif.
Matt

1
Hibernate akan mengambil entitas root dan koleksi tertentu dalam satu kueri saat mode eager fetch digunakan untuk koleksi tersebut; dalam hal ini ia melakukan pengurangan properti entitas root dalam memori. ORM lain mungkin bisa melakukan hal yang sama.
Mike Partridge

3
Sebenarnya ini tidak bisa disalahkan pada model relasional. Ini mengatasi dengan sangat baik dengan hubungan yang bersarang terima kasih. Ini murni bug implementasi dalam versi awal SQL. Saya pikir versi yang lebih baru telah menambahkannya.
John Nilsson

8
Apakah Anda yakin ini adalah contoh impedansi objek-relasional? Menurut saya, model relasional sangat cocok dengan model data konseptual OP: setiap pengguna dikaitkan dengan daftar nol, satu, atau lebih alamat email. Model itu juga dapat digunakan secara sempurna dalam paradigma OO (agregasi: objek pengguna memiliki koleksi email). Batasannya adalah teknik yang digunakan untuk query database, yang merupakan detail implementasi. Ada teknik kueri di sekitar yang mengembalikan data heirarchical, mis. Heirarchical DataSets di .Net
MarkJ

@ MarkJ Anda harus menuliskannya sebagai jawaban.
Mr.Mindor

12

Anda bisa menggunakan fungsi bawaan untuk menggabungkan catatan bersama. Di MySQL Anda dapat menggunakan GROUP_CONCAT()fungsi dan di Oracle Anda dapat menggunakan LISTAGG()fungsi.

Berikut ini contoh tampilan kueri di 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 

Ini akan mengembalikan sesuatu seperti

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

Ini tampaknya menjadi solusi terdekat (dalam SQL) dengan apa yang OP coba lakukan. Dia berpotensi masih harus melakukan pemrosesan sisi klien untuk memecah hasil EmailAddresses dan PhoneNumber ke dalam daftar.
Mr.Mindor

2
Bagaimana jika nomor telepon memiliki "tipe", seperti "Sel", "Rumah", atau "Kantor"? Selain itu, koma diizinkan secara teknis di alamat email (jika dikutip) - bagaimana cara saya membaginya?
mpen

10

Masalahnya adalah ini mengembalikan nama pengguna, DOB, warna favorit, dan semua informasi lain yang disimpan

Masalahnya adalah Anda tidak cukup selektif. Anda meminta segalanya ketika Anda mengatakannya

Select * from...

... dan Anda mendapatkannya (termasuk DOB dan warna favorit).

Anda mungkin harus sedikit lebih (ahem) ... selektif, dan mengatakan sesuatu seperti:

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

Mungkin juga Anda mungkin melihat catatan yang terlihat seperti duplikat karena a usermungkin bergabung dengan banyak emailcatatan, tetapi bidang yang membedakan keduanya tidak ada dalam Selectpernyataan Anda , jadi Anda mungkin ingin mengatakan sesuatu seperti

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

... berulang-ulang untuk setiap rekaman ...

Juga, saya perhatikan Anda sedang melakukan LEFT JOIN . Ini akan bergabung dengan semua catatan di sebelah kiri gabungan (yaitu users) untuk semua catatan di sebelah kanan, atau dengan kata lain:

Gabung luar kiri mengembalikan semua nilai dari gabung dalam ditambah semua nilai di tabel kiri yang tidak cocok dengan tabel kanan.

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

Jadi pertanyaan lain adalah apakah Anda benar - benar perlu bergabung dengan kiri, atau akan sebuahINNER JOIN sudah cukup? Mereka adalah tipe gabungan yang sangat berbeda.

Tidak akan lebih baik jika mengembalikan satu baris untuk setiap pengguna, dan dalam catatan itu ada daftar email

Jika Anda benar-benar ingin satu kolom dalam set hasil berisi daftar yang dihasilkan saat itu juga, itu bisa dilakukan tetapi bervariasi tergantung pada basis data yang Anda gunakan. Oracle memiliki listaggfungsi .


Pada akhirnya, saya pikir masalah Anda mungkin terpecahkan jika Anda menulis ulang kueri Anda dekat dengan sesuatu seperti ini:

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
menggunakan * tidak disarankan tetapi bukan inti masalahnya. Sekalipun ia memilih 0 kolom pengguna, ia mungkin masih mengalami efek duplikasi karena Ponsel dan Email memiliki hubungan 1-banyak dengan Pengguna. Perbedaan tidak akan mencegah nomor telepon muncul dua kali ala phone1/name@hotmail.com, phone1/name@google.com.
mike30

6
-1: "masalah Anda mungkin terpecahkan" mengatakan bahwa Anda tidak tahu apa efek perubahan dari left joinmenjadi inner join. Dalam hal ini, ini tidak akan mengurangi "pengulangan" yang dikeluhkan oleh pengguna; itu hanya akan menghilangkan pengguna yang tidak memiliki ponsel atau email. hampir tidak ada perbaikan. juga, ketika menafsirkan "semua catatan di sebelah kiri untuk semua catatan di sebelah kanan" melompati ONkriteria, yang memangkas semua hubungan 'salah' yang melekat dalam produk Cartesian tetapi menyimpan semua bidang yang berulang.
Javier

@Javier: Ya, itulah sebabnya saya juga mengatakan apakah Anda benar-benar membutuhkan join kiri, atau apakah INNER JOIN sudah cukup? * Deskripsi OP tentang masalah membuatnya * terdengar seolah-olah mereka mengharapkan hasil gabungan batin. Tentu saja, tanpa data sampel atau deskripsi tentang apa yang sebenarnya mereka inginkan, sulit untuk dikatakan. Saya membuat saran karena saya benar-benar melihat orang (yang bekerja sama dengan saya) melakukan ini: pilih salah bergabung dan kemudian mengeluh ketika mereka tidak memahami hasil yang mereka dapatkan. Setelah melihatnya , saya pikir itu mungkin terjadi di sini.
FrustratedWithFormsDesigner

3
Anda melewatkan inti pertanyaan. Dalam contoh hipotetis ini, saya ingin semua data pengguna (nama, dob, dll) dan saya ingin semua nomor teleponnya. Gabungan dalam mengecualikan pengguna tanpa email atau telepon - bagaimana hal itu membantu?
buka

4

Kueri selalu menghasilkan set data tabular persegi panjang (tidak bergerigi). Tidak ada sub-set bersarang dalam satu set. Dalam dunia set semuanya adalah persegi panjang murni yang tidak bersarang.

Anda dapat menganggap gabungan sebagai menempatkan 2 set berdampingan. Kondisi "aktif" adalah bagaimana catatan di setiap set dicocokkan. Jika pengguna memiliki 3 nomor telepon, maka Anda akan melihat duplikasi 3 kali dalam info pengguna. Set persegi panjang bergerigi harus diproduksi oleh kueri. Ini hanya sifat bergabung dengan set dengan hubungan 1-ke-banyak.

Untuk mendapatkan yang Anda inginkan, Anda harus menggunakan kueri terpisah seperti yang dijelaskan Mason Wheeler.

select * from Phones where user_id=344;

Hasil kueri ini masih berupa set persegi panjang yang tidak bergerigi. Seperti segala sesuatu di dunia set.


2

Anda harus memutuskan di mana kemacetan ada. Bandwidth antara database dan aplikasi Anda biasanya cukup cepat. Tidak ada alasan kebanyakan database tidak dapat mengembalikan 3 dataset terpisah dalam satu panggilan dan tidak ada yang bergabung. Maka Anda bisa bergabung bersama-sama di aplikasi Anda jika Anda mau.

Jika tidak, Anda ingin database untuk menyatukan dataset ini dan kemudian menghapus semua nilai yang diulang di setiap baris yang merupakan hasil dari gabungan dan tidak harus baris itu sendiri memiliki data duplikat seperti dua orang dengan nama atau nomor telepon yang sama. Sepertinya banyak over-head untuk menghemat bandwidth. Anda akan lebih baik fokus mengembalikan data yang lebih sedikit dengan pemfilteran yang lebih baik dan menghapus kolom yang tidak Anda butuhkan. Karena Select * tidak pernah digunakan dalam sumur produksi yang tergantung.


"Tidak ada alasan kebanyakan database tidak dapat mengembalikan 3 dataset terpisah dalam satu panggilan dan tidak ada yang bergabung" - Bagaimana Anda mendapatkannya untuk mengembalikan 3 dataset terpisah dengan satu panggilan? Saya pikir Anda harus mengirim 3 pertanyaan berbeda, yang memperkenalkan latensi antara masing-masing?
mpen

Prosedur tersimpan dapat dipanggil dalam 1 transaksi, dan kemudian mengembalikan set data sebanyak yang Anda inginkan. Mungkin sproc "SelectUserWithEmailsPhones" diperlukan.
Graham

1
@ Mark: Anda dapat mengirim (setidaknya dalam sql server) lebih dari satu perintah sebagai bagian dari batch yang sama. cmdText = "pilih * dari b; pilih * dari; pilih * dari c" dan kemudian gunakan itu sebagai teks perintah untuk perintah sql.
jmoreno

2

Sederhananya, jangan bergabung dengan data Anda jika Anda ingin hasil yang berbeda untuk permintaan pengguna dan permintaan nomor telepon, jika tidak karena orang lain telah menunjukkan "Set" atau data akan berisi bidang tambahan untuk setiap baris.

Menerbitkan 2 kueri berbeda alih-alih satu dengan gabungan.

Dalam prosedur tersimpan atau inline parameterisasi sql craft 2 kueri dan kembalikan hasil keduanya. Sebagian besar basis data dan bahasa mendukung beberapa set hasil.

Sebagai contoh, SQL Server dan C # menyelesaikan fungsionalitas ini dengan menggunakan IDataReader.NextResult().


1

Anda melewatkan sesuatu. Jika Anda ingin mendenormalisasi data Anda, Anda harus melakukannya sendiri.

;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

Konsep penutupan relasional pada dasarnya berarti bahwa hasil dari setiap kueri adalah relasi yang dapat digunakan dalam pertanyaan lain seolah-olah itu adalah tabel dasar. Ini adalah konsep yang kuat karena membuat kueri dapat dikomposisi.

Jika SQL memungkinkan Anda untuk menulis kueri yang menghasilkan struktur data bersarang, Anda akan melanggar prinsip ini. Struktur data bersarang bukan merupakan relasi, jadi Anda akan memerlukan bahasa query baru, atau ekstensi kompleks untuk SQL, untuk menanyakannya lebih lanjut atau untuk bergabung dengannya yang memiliki relasi lain.

Pada dasarnya Anda akan membangun DBMS hirarkis di atas DBMS relasional. Ini akan jauh lebih kompleks untuk keuntungan yang meragukan, dan Anda kehilangan keuntungan dari sistem relasional yang konsisten.

Saya mengerti mengapa kadang-kadang akan lebih nyaman untuk dapat menghasilkan data terstruktur secara hierarkis dari SQL, tetapi biaya dalam kompleksitas tambahan di seluruh DBMS untuk mendukung ini jelas tidak sepadan.


-4

Pls merujuk pada penggunaan fungsi STUFF yang mengelompokkan beberapa baris (nomor telepon) dari sebuah kolom (kontak) yang dapat diekstraksi sebagai sel tunggal dengan nilai yang dibatasi dari satu baris (pengguna).

Hari ini kami menggunakan ini secara ekstensif tetapi menghadapi beberapa masalah CPU dan kinerja yang tinggi. Tipe data XML adalah pilihan lain tetapi perubahan desain bukan tingkat permintaan.


5
Harap kembangkan bagaimana ini memecahkan pertanyaan. Alih-alih mengatakan "Tolong lihat penggunaan", berikan contoh bagaimana ini akan mencapai pertanyaan yang diajukan. Mengutip sumber-sumber pihak ke-3 juga dapat membantu jika hal itu membuat segalanya lebih jelas.
bitsoflogic

1
Sepertinya STUFFmirip dengan sambatan. Tidak yakin bagaimana itu berlaku untuk pertanyaan saya.
mpen
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.