Почему СУБД не возвращают объединенные таблицы во вложенном формате?

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

mpen
источник
6
Думайте о SQL как о электронной таблице, как в Microsoft Excel, затем попытайтесь выяснить, как создать значение ячейки, которая содержит внутренние ячейки. Он больше не работает как электронная таблица. То, что вы ищете, - это древовидная структура, но тогда у вас больше не будет преимуществ электронной таблицы (т.е. вы не можете суммировать столбец в дереве). Древовидные структуры не делают для очень удобочитаемых отчетов.
Reactgular
54
SQL не плохо возвращает данные, вы плохо запрашиваете то, что вам нужно. Как правило, если вы считаете, что широко используемый инструмент содержит ошибки или неисправен в общем случае, проблема заключается в вас.
Шон МакSomething
12
@SeanMcSomething Так верно, что это больно, я не мог бы сказать это лучше сам.
WernerCD
5
Это отличные вопросы. Ответы, которые говорят, что «это так», не имеют смысла. Почему невозможно вернуть строки со встроенными коллекциями строк?
Крис Питман
8
@SeanMcSomething: Если этим широко используемым инструментом не является C ++ или PHP, в этом случае вы, вероятно, правы. ;)
Мейсон Уилер

Ответы:

11

В глубине души в реляционной базе данных все строки и столбцы. Это структура, с которой оптимизирована реляционная база данных. Курсоры работают с отдельными строками одновременно. Некоторые операции создают временные таблицы (опять же, это должны быть строки и столбцы).

Работая только с строками и возвращая только строки, система может лучше справляться с памятью и сетевым трафиком.

Как уже упоминалось, это позволяет выполнять определенные оптимизации (индексы, объединения, объединения и т. Д.)

Если нужно хотеть вложенную древовидную структуру, это требует, чтобы каждый извлекал все данные одновременно. Прошли оптимизации для курсоров на стороне базы данных. Аналогично, трафик по сети становится одним большим взрывом, который может занять гораздо больше времени, чем медленный поток строк за строкой (это то, что иногда теряется в современном мире Интернета).

В каждом языке есть массивы. Это простые вещи для работы и взаимодействия. Используя очень примитивную структуру, драйвер между базой данных и программой - независимо от того, на каком языке - может работать обычным образом. Как только начинаются добавления деревьев, структуры в языке становятся более сложными и более сложными для прохождения.

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

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

И хотя современные языки поддерживают рекурсию и более продвинутые типы данных, на самом деле нет веских причин что-либо менять. Они работают, и они работают хорошо. Те, которые меняются вещи являются NoSQL базы данных. Вы можете хранить деревья в документах в документе. LDAP (на самом деле он старый) также является системой на основе дерева (хотя, вероятно, это не то, что вам нужно). Кто знает, может быть, следующая вещь в базах данных nosql будет возвращать запрос в виде объекта json.

Тем не менее, «старые» реляционные базы данных ... они работают со строками, потому что это то, в чем они хороши, и все может общаться с ними без проблем или перевода.

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

Из RFC 1925 - Двенадцать сетевых истин


источник
«Если кто-то хочет иметь вложенную древовидную структуру, для этого требуется, чтобы он извлекал все данные одновременно. Прошли оптимизации для курсоров на стороне базы данных». - Это не звучит правдой. Для этого потребуется всего лишь пара курсоров: один для основного стола, а затем один для каждого объединенного стола. В зависимости от интерфейса он может возвращать одну строку и все объединенные таблицы в одном чанке (частично потоковом) или может передавать потоки поддеревьев (и, возможно, даже не запрашивать их), пока вы не начнете выполнять их итерацию. Но да, это сильно усложняет.
2013 г.
3
У каждого современного языка должен быть какой-то класс дерева, хотя? И разве это не зависит от водителя? Я предполагаю, что ребятам по SQL все еще нужно разработать общий формат (не знаю об этом много). Однако меня поразило то, что я должен либо отправить 1 запрос с объединениями, но и вернуться и отфильтровать лишние данные каждой строки (пользовательская информация, которая изменяет только каждую N-ую строку) или выполнить 1 запрос (пользователи). и зациклите результаты, затем отправьте еще два запроса (электронные письма, телефоны) для каждой записи, чтобы получить нужную мне информацию. Любой метод кажется расточительным.
2013 г.
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
Вы уверены, что это пример объектно-реляционного импеданса? Мне кажется, что реляционная модель полностью соответствует концептуальной модели данных OP: каждому пользователю соответствует список из нуля, одного или нескольких адресов электронной почты. Эта модель также отлично подходит для парадигмы ОО (агрегация: у объекта пользователя есть коллекция электронных писем). Ограничение заключается в методике, используемой для запроса к базе данных, которая является подробностью реализации. Существуют методы запросов, которые возвращают иерархические данные, например, иерархические
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) к тому, что пытается сделать OP. Потенциально ему все равно придется выполнять обработку на стороне клиента, чтобы разбить результаты EmailAddresses и PhoneNumbers на списки.
Мистер Миндор
2
Что если номер телефона имеет «тип», например «Сотовый», «Домашний» или «Рабочий»? Кроме того, запятые технически разрешены в адресах электронной почты (если они указаны) - как бы я их разделил?
mpen
10

Проблема в том, что он возвращает имя пользователя, DOB, любимый цвет и всю другую сохраненную информацию

Проблема в том, что вы недостаточно избирательны. Вы просили все, когда вы сказали

Select * from...

... и вы получили его (включая DOB и любимые цвета).

Вы, вероятно, должны быть немного более (хм) ... избирательно, и сказал что-то вроде:

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

Также возможно, что вы видите записи, которые выглядят как дубликаты, потому что a 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
FrustratedWithFormsDesigner
источник
1
использование * не рекомендуется, но не суть его проблемы. Даже если он выберет 0 пользовательских столбцов, он все равно может столкнуться с эффектом дублирования, поскольку и телефоны, и электронные письма имеют отношение «1-много» к пользователям. Distinct не будет препятствовать тому, чтобы номер телефона появлялся дважды 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 телефонных номера, вы увидите трехкратное дублирование в информации о пользователе. По запросу должен быть получен прямоугольный набор без зазубрин. Это просто природа объединения наборов с отношением 1-ко-многим.

Чтобы получить то, что вы хотите, вы должны использовать отдельный запрос, как описал Мейсон Уилер.

select * from Phones where user_id=344;

Результатом этого запроса по-прежнему остается прямоугольный набор без зазубрин. Как и все в мире наборов.

mike30
источник
2

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

В противном случае вы хотите, чтобы база данных объединила этот набор данных, а затем удалила все повторяющиеся значения в каждой строке, которые являются результатом объединений, и не обязательно сами строки имеют дублирующиеся данные, например, два человека с одинаковым именем или номером телефона. Похоже, много накладных расходов, чтобы сэкономить на пропускной способности. Вам лучше сосредоточиться на возвращении меньшего количества данных с лучшей фильтрацией и удалением ненужных столбцов. Потому что Select * никогда не используется в производстве - это зависит от ситуации.

JeffO
источник
«Нет причин, по которым большинство баз данных не может вернуть 3 отдельных набора данных в течение одного вызова и без объединений». Как получить 3 отдельных набора данных за один вызов? Я думал, что вы должны были отправить 3 разных запроса, что вводит задержку между каждым?
2013 г.
Хранимая процедура может быть вызвана в 1 транзакции, а затем вернуть столько наборов данных, сколько вы хотите. Возможно, необходим спрок «SelectUserWithEmailsPhones».
Грэм
1
@Mark: вы можете отправить (как минимум на сервере sql) более одной команды как часть одного пакета. cmdText = "выберите * из b; выберите * из a; выберите * из c" и затем используйте это как текст команды для команды sql.
Jmoreno
2

Проще говоря, не объединяйте свои данные, если вам нужны разные результаты для запроса пользователя и запроса телефонного номера, в противном случае, как отмечают другие, «Установить» или данные будут содержать дополнительные поля для каждой строки.

Выпустите 2 различных запроса вместо одного с объединением.

В хранимой процедуре или параметризованном встроенном sql обработайте 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
jmoreno
источник
1

Концепция реляционного замыкания в основном означает, что результатом любого запроса является отношение, которое может использоваться в других запросах, как если бы это была базовая таблица. Это мощная концепция, потому что она делает запросы составными.

Если бы SQL позволял вам писать запросы, которые выводили бы вложенные структуры данных, вы бы нарушили этот принцип. Вложенная структура данных не является отношением, поэтому вам потребуется новый язык запросов или сложные расширения для SQL, чтобы запросить его дальше или присоединить к нему другие отношения.

По сути, вы должны построить иерархическую СУБД поверх реляционной СУБД. Это будет гораздо сложнее для сомнительной выгоды, и вы потеряете преимущества последовательно реляционной системы.

Я понимаю, почему иногда было бы удобно иметь возможность выводить иерархически структурированные данные из SQL, но затраты на дополнительную сложность во всей СУБД для поддержки этого определенно не стоят того.

JacquesB
источник
-4

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

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

Шрирам Раджагопал
источник
5
Пожалуйста, объясните, как это решает вопрос. Вместо того, чтобы говорить «Просьба обратиться к использованию», приведите пример того, как это позволит достичь заданного вопроса. Также может быть полезно процитировать сторонние источники, где это проясняет ситуацию.
битсфлогический
1
Похоже, STUFFэто похоже на сращивание. Не уверен, как это относится к моему вопросу.
mpen