Чому RDBMS не повертають об'єднані таблиці у вкладеному форматі?


14

Наприклад, скажіть, що я хочу отримати Користувача та всі його телефонні номери та адреси електронної пошти. Номери телефонів та електронні листи зберігаються в окремих таблицях, один користувач на багатьох телефонах / електронних листах. Я можу це зробити досить легко:

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

Проблема * в цьому полягає в тому, що він повертає ім’я користувача, DOB, улюблений колір та всю іншу інформацію, що зберігається в таблиці користувачів, знову і знову для кожного запису (записи електронної пошти користувачів електронною поштою ), імовірно, збільшуючи пропускну здатність і сповільнюючи зниження результатів.

Чи не було б приємніше, якби він повертав по одному рядку для кожного користувача, і всередині цього запису був список електронних листів та список телефонів? Це також полегшило б роботу з даними.

Я знаю, що ви можете отримати такі результати, використовуючи LINQ або, можливо, інші рамки, але це, здається, є слабкістю в базовій конструкції реляційних баз даних.

Ми могли б обійти це за допомогою NoSQL, але чи не повинно бути середнього?

Я щось пропускаю? Чому цього не існує?

* Так, це розроблено таким чином. Я розумію. Мені цікаво, чому немає альтернативи, з якою легше працювати. SQL може продовжувати робити те, що він робить, але потім вони могли б додати ключове слово або два, щоб зробити трохи постінгової обробки, яка повертає дані у вкладеному форматі замість декартового продукту.

Я знаю, що це можна зробити на мові сценаріїв за вашим вибором, але це вимагає, щоб сервер SQL або надсилав зайві дані (приклад нижче), або щоб ви видавали кілька запитів на кшталт SELECT email FROM emails WHERE user_id IN (/* result of first query */).


Замість того, щоб MySQL повертав щось подібне до цього:

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

І тоді потрібно згрупувати якийсь унікальний ідентифікатор (а це означає, що мені потрібно це також отримати!) На стороні клієнта, щоб переформатувати набір результатів так, як вам потрібно, просто поверніть це:

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

Крім того, я можу задати 3 запити: 1 для користувачів, 1 для електронних листів та 1 для номерів телефонів, але тоді набори результатів електронної пошти та номера телефону повинні містити user_id, щоб я міг співставити їх із резервними копіями з користувачами Я раніше забирав. Знову-таки, зайві дані та непотрібна післяобробка.


6
Подумайте про SQL як електронну таблицю, як у Microsoft Excel, а потім спробуйте з'ясувати, як створити значення комірки, що містить внутрішні комірки. Це більше не працює як електронна таблиця. Що ви шукаєте - це структура дерева, але тоді ви більше не маєте переваг електронної таблиці (тобто ви не можете скласти стовпець у дереві). Деревоподібні структури не створюють для людини читаних звітів.
Реакційний

54
SQL не поганий у поверненні даних, ви погано запитуєте, що ви хочете. Як правило, якщо ви вважаєте, що широко використовуваний інструмент баггі чи зламаний для звичайного випадку використання, проблема у вас.
Sean McSomething

12
@SeanMcSomething Настільки правдиво, що це боляче, я не міг би сказати це краще сам.
WernerCD

5
Це чудові питання. У відповідях, які говорять "так воно і є", відсутні суть. Чому не можна повертати рядки із вбудованими колекціями рядків?
Кріс Пітман

8
@SeanMcSomething: Якщо тільки цей широко використовуваний інструмент не є C ++ або PHP, тоді ви, мабуть, маєте рацію. ;)
Мейсон Уілер

Відповіді:


11

Внизу, в кишках реляційної бази даних, всі її рядки та стовпці. Це структура, з якою реляційна база даних оптимізована для роботи. Курсори працюють за окремими рядами одночасно. Деякі операції створюють тимчасові таблиці (знову ж таки це повинні бути рядки та стовпці).

Працюючи лише з рядками та повертаючи лише рядки, система здатна краще справлятися з пам’яттю та мережевим трафіком.

Як уже згадувалося, це дозволяє проводити певні оптимізації (індекси, вступ, спілки тощо).

Якщо потрібно було вкласти структуру вкладеного дерева, для цього потрібно витягнути всі дані одразу. Пройшли оптимізації для курсорів на стороні бази даних. Так само трафік по мережі стає одним великим сплеском, який може зайняти набагато більше часу, ніж повільний струк за рядом (це те, що періодично втрачається в сучасному веб-світі).

Кожна мова має масиви всередині себе. З цими простими речами працювати та взаємодіяти. Використовуючи дуже примітивну структуру, драйвер між базою даних та програмою - незалежно від мови - може працювати загальноприйнятим способом. Як тільки ви почнете додавати дерева, структури мови стають складнішими і важче перетинати.

Мові програмування не так важко перетворити рядки, повернуті в якусь іншу структуру. Складіть його у дерево або хеш-набір або залиште його як список рядків, над якими ви можете перебрати.

Тут також є історія на роботі. За старих часів передача структурованих даних була чимось потворною. Подивіться на формат EDI, щоб отримати уявлення про те, що ви можете запитати. Дерева також передбачають рекурсію - яку деякі мови не підтримували (дві найважливіші мови старих часів не підтримували рекурсії - рекурсія не входила у Фортран до F90, а також епохи COBOL).

І хоча сьогоднішні мови підтримують рекурсію та більш вдосконалені типи даних, насправді немає вагомих причин змінити речі. Вони працюють, і вони добре працюють. Ті, які змінюються речі є NoSQL бази даних. Ви можете зберігати дерева в документах в одному документі. LDAP (його насправді давньоруський) - це також система на основі дерев (хоча, ймовірно, це не те, що ви хочете). Хто знає, можливо, наступним у базі даних nosql буде те, що повертає запит як об’єкт json.

Однак, "старі" реляційні бази даних ... вони працюють з рядками, тому що в чому вони хороші, і все може говорити з ними без проблем і перекладу.

  1. У дизайні протоколів досконалість була досягнута не тоді, коли не залишається нічого додати, а коли не залишається нічого, щоб забрати.

З RFC 1925 - Дванадцять мережевих правд


"Якщо потрібно було вкласти структуру вкладеного дерева, для цього потрібно витягнути всі дані одразу. Пройшли оптимізації для курсорів на стороні бази даних." - Це не здається правдою. Було б просто підтримувати пару курсорів: по одному для основної таблиці, а по одному для кожної приєднаної таблиці. Залежно від інтерфейсу, він може повертати один рядок і всі об'єднані таблиці в один фрагмент (частково переданий), або він може передавати потокові підряди (а може навіть і не запитувати їх), поки ви не почнете їх ітераціювати. Але так, це дуже ускладнює справи.
30

3
Кожна сучасна мова повинна мати якийсь клас дерева, хоча, ні? І хіба не водієві з цим боротися? Я думаю, хлопцям SQL все ще потрібно розробити загальний формат (про це не знаю багато). Що мене отримує, це те, що я або повинен надсилати 1 запит з приєднанням, і повертатися, і фільтрувати зайві дані, які кожен рядок (інформація про користувача, яка змінює лише кожен N-й рядок), або видавати 1 запит (користувачі) та перегляньте результати, а потім надішліть ще два запити (електронні листи, телефони) для кожного запису, щоб отримати потрібну мені інформацію. Будь-який метод здається марним.
1313

51

Повертається саме те, що ви просили: єдиний набір записів, що містить декартовий продукт, визначений приєднаннями. Існує безліч дійсних сценаріїв, де саме цього ви хотіли б, тому мовляв, що SQL дає поганий результат (і, таким чином, означає, що було б краще, якщо ви змінили його), насправді накрутить багато запитів.

Те, що ви переживаєте, відоме як " Невідповідність об'єктних / реляційних імпедансів ", технічні труднощі, які виникають у зв'язку з тим, що об'єктно-орієнтована модель даних та модель реляційних даних принципово відрізняються кількома способами. LINQ та інші рамки (відомі як ORM, Object / Relational Mappers, не випадково) не магічно "обходять це"; вони просто видають різні запити. Це можна зробити і в SQL. Ось як я це зробив:

SELECT * FROM users user where [criteria here]

Ітерацію списку користувачів та складання списку ідентифікаторів.

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

І тоді ви робите приєднання на стороні клієнта. Так роблять LINQ та інші рамки. Тут немає жодної реальної магії; просто шар абстракції.


14
+1 для "саме те, про що ви просили". Занадто часто ми підскакуємо до висновку, що з технологією щось не так, ніж висновок, що нам потрібно навчитися ефективно використовувати технологію.
Метт

1
Hibernate отримає кореневу сутність та певні колекції в одному запиті, коли для цих колекцій буде використаний режим нетерпіння ; у цьому випадку це зменшує властивості кореневої сутності у пам'яті. Інші ORM, ймовірно, можуть зробити те ж саме.
Майк Партрідж

3
Насправді в цьому не винна реляційна модель. Дуже добре справляється з вкладеними відносинами дякую. Це суто помилка реалізації у ранніх версіях SQL. Я думаю, що новіші версії додали його, хоча.
Джон Нільссон

8
Ви впевнені, що це приклад об'єктно-реляційного опору? Мені здається, що реляційна модель ідеально відповідає концептуальній моделі даних ОП: кожен користувач асоціюється зі списком нуля, однієї чи більше електронних адрес. Ця модель також ідеально підходить для парадигми OO (агрегація: об’єкт користувача має колекцію електронних листів). Обмеження полягає в техніці, яка використовується для запиту до бази даних, яка є деталлю реалізації. Існують методи запитів, навколо яких повертаються герархічні дані, наприклад, герархічні набори даних у .Net
MarkJ

@MarkJ ви повинні написати це як відповідь.
Містер Міндор

12

Ви можете використовувати вбудовану функцію для об'єднання записів разом. У MySQL ви можете використовувати GROUP_CONCAT()функцію, а в Oracle ви можете використовувати цю LISTAGG()функцію.

Ось зразок того, як може виглядати запит у 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 

Це поверне щось подібне

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

Це, здається, є найближчим рішенням (у SQL) того, що намагається зробити ОП. Йому, можливо, все ж доведеться виконати обробку на стороні клієнта, щоб розділити результати електронних адрес і телефонні номери на списки.
Містер Міндор

2
Що робити, якщо номер телефону має "тип", наприклад "Стільниковий", "Домашній" або "Робота"? Крім того, коси технічно дозволені в електронних адресах (якщо вони цитуються) - як би я розділив їх тоді?
mpen

10

Проблема в цьому полягає в тому, що він повертає ім'я користувача, DOB, улюблений колір та всю іншу інформацію, що зберігається

Проблема полягає в тому, що ви недостатньо вибіркові. Ви все просили, коли говорили

Select * from...

... і ви його отримали (включаючи DOB та улюблені кольори).

Ви, напевно, повинні бути трохи більше (ах) ... вибірково, і сказали щось на кшталт:

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

Можливо також, що ви можете побачити записи, схожі на дублікати, тому що вони userможуть приєднатися до декількох emailзаписів, але поле, яке відрізняє ці два, відсутнє у вашому Selectвиписці, тож ви можете сказати щось на зразок

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

... знову і знову для кожного запису ...

Також я помічаю, що ви займаєтесь LEFT JOIN. Це приєднає всі записи зліва від з'єднання (тобто users) до всіх записів праворуч або іншими словами:

Ліве зовнішнє з'єднання повертає всі значення з внутрішнього з'єднання плюс усі значення в лівій таблиці, які не відповідають правій таблиці.

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

Отже, ще одне питання - чи справді вам потрібен лівий приєднання, чи це INNER JOINбуло б достатньо? Вони дуже різні типи приєднань.

Не було б приємніше, якби він повертав один рядок для кожного користувача, і всередині цього запису був список електронних листів

Якщо ви дійсно хочете, щоб один стовпець у наборі результатів містив список, який генерується на ходу, це можна зробити, але він змінюється залежно від бази даних, яку ви використовуєте. Oracle має listaggфункцію .


Зрештою, я думаю, що ваша проблема може бути вирішена, якщо ви перепишете свій запит близько до такого:

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
використання * не відволікає, але не суть його проблеми. Навіть якщо він вибрав 0 стовпців користувачів, він все ще може відчути ефект дублювання, оскільки і телефони, і електронна пошта мають відношення до користувачів. Розрізнення не перешкоджало б появі номера телефону двічі ala phone1/name@hotmail.com, phone1/name@google.com.
mike30

6
-1: «Ваша проблема може бути вирішена» , говорить , що ви не знаєте , який ефект буде перехід від left joinдо inner join. У цьому випадку це не зменшить "повторень", на які скаржаться користувач; вона просто опустить тих користувачів, яким не вистачає телефону чи електронної пошти. навряд чи будь-яке поліпшення. також при інтерпретації "всі записи зліва на всі записи праворуч" пропускає ONкритерії, які виправляють усі "неправильні" відносини, притаманні декартовому продукту, але зберігають усі повторювані поля.
Хав'єр

@Javier: Так, саме тому я також сказав, чи вам насправді потрібен лівий приєднання, чи буде ВНУТРІШНЕ ПРИЄДНАННЯ? * Опис проблеми OP дає змогу звучати * так, ніби вони очікували результату внутрішнього з'єднання. Звичайно, без будь-яких зразкових даних або опису того, що вони дійсно хотіли, важко сказати. Я зробив пропозицію, тому що насправді я бачив, як люди (з якими я працюю) роблять це: вибирають неправильне приєднання, а потім скаржаться, коли вони не розуміють отриманих результатів. Після бачив його, я думав , що це могло статися тут.
FrustratedWithFormsDesigner

3
Ви пропускаєте суть питання. У цьому гіпотетичному прикладі я хочу всіх даних користувача (ім’я, dob тощо), і я хочу всі його / її номери телефонів. Внутрішнє приєднання виключає користувачів без електронних листів чи телефонів - як це допомагає?
mpen

4

Запити завжди створюють прямокутний (не зазубрований) табличний набір даних. В межах набору немає вкладених підмножин. У світі наборів все є чистим вкладеним прямокутником.

Ви можете подумати про об'єднання як про встановлення 2-х наборів. Умова "увімкнено" - це відповідність записів у кожному наборі. Якщо у користувача є 3 телефонні номери, ви побачите 3-разове дублювання в інформації про користувача. Прямокутний беззубчастий набір повинен бути створений за запитом. Це просто природа об'єднання наборів, які мають відношення 1 до багатьох.

Щоб отримати те, що ви хочете, ви повинні використовувати окремий запит, як описаний Mason Wheeler.

select * from Phones where user_id=344;

Результатом цього запиту є все ще прямокутний нерозмитий набір. Як і все у світі наборів.


2

Ви повинні вирішити, де існують вузькі місця. Пропускна здатність між вашою базою даних та додатком зазвичай досить швидка. Немає причини, що більшість баз даних не можуть повернути 3 окремі набори даних протягом одного дзвінка і не приєднується. Потім ви можете приєднатися до цього все разом у своєму додатку, якщо хочете.

В іншому випадку ви хочете, щоб база даних зібрала цей набір даних, а потім видалила всі повторювані значення в кожному рядку, які є результатом об'єднань, а не обов'язково самі рядки, що мають дублікати даних, як двоє людей з тим самим іменем або номером телефону. Здається, багато перенапруги, щоб заощадити на пропускній здатності. Вам було б краще зосередитись на поверненні менших даних з кращою фільтрацією та видаленням стовпців, які вам не потрібні. Тому що Select * ніколи не використовується у виробництві, а це залежить.


"Немає причин, що більшість баз даних не можуть повернути 3 окремі набори даних за один дзвінок і не приєднуються" - Як змусити його повертати 3 окремі набори даних одним викликом? Я думав, що вам доведеться надіслати 3 різні запити, що вводить затримку між кожним?
1313

Збережену процедуру можна викликати в 1 транзакції, а потім повернути стільки наборів даних, скільки ви хотіли. Можливо, потрібен відросток "SelectUserWithEmailsPhones".
Грем

1
@Mark: ви можете надіслати (хоча б на сервері sql) більше однієї команди в рамках однієї партії. cmdText = "select * from b; select * from a; select * from c", а потім використовувати це як текст команди для sqlcommand.
jmoreno

2

Дуже просто, не приєднуйтесь до своїх даних, якщо ви хочете чітких результатів для запиту користувача та запиту на номер телефону, інакше як інші вказали на "Встановити", або дані містять додаткові поля для кожного рядка.

Випустіть 2 різних запити замість одного із з'єднанням.

У збереженій процедурі або вбудованому параметризованому sql craft 2 запити і повернути результати обох назад. Більшість баз даних та мов підтримують декілька наборів результатів.

Наприклад, SQL Server і C # досягають цього функціоналу, використовуючи IDataReader.NextResult().


1

Вам щось не вистачає. Якщо ви хочете денормалізувати свої дані, ви повинні зробити це самостійно.

;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

Концепція реляційного закриття основному означає, що результат будь-якого запиту - це відношення, яке може використовуватися в інших запитах так, ніби це базова таблиця. Це потужна концепція, оскільки робить запити композиційними.

Якщо SQL дозволив вам писати запити, які виводять вкладені структури даних, ви порушите цей принцип. Вкладена структура даних не є відношенням, тому вам знадобиться нова мова запитів або складні розширення до SQL, щоб далі запитувати його або приєднуватися до яких інших відносин.

В основному ви б створили ієрархічну СУБД на основі реляційних СУБД. Це буде набагато складніше за сумнівну користь, і ви втратите переваги послідовно реляційної системи.

Я розумію, чому іноді було б зручно виводити ієрархічно структуровані дані з SQL, але витрати на додаткову складність у СУБД на підтримку цього, безумовно, не варті.


-4

Pls посилається на використання функції STUFF, яка групує кілька рядків (телефонних номерів) стовпця (контакту), який може бути вилучений у вигляді однієї комірки з обмеженими значеннями рядка (користувач).

Сьогодні ми широко використовуємо це, але стикаємося з деякими проблемами з процесором та продуктивністю. Тип даних XML - це ще одна можливість, але це зміна дизайну, а не рівень запиту.


5
Розкрийте, будь ласка, про те, як це вирішує питання. Замість того, щоб сказати "Pls посилайтесь на використання", наведіть приклад, як це дозволить досягти заданого питання. Це також може бути корисним для цитування сторонніх джерел, де це робить зрозуміліше.
bitsoflogic

1
Схоже, STUFFце схоже на зрощення. Не впевнений, як це стосується мого питання.
mpen
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.