为什么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 Server发送冗余数据(下面的示例),或者发出多个查询,例如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
像在Microsoft Excel中一样,将SQL视为电子表格,然后尝试找出如何创建包含内部单元格的单元格值。它不再适合作为电子表格。您正在寻找的是树状结构,但是您再也无法享受电子表格的好处(即,您无法将树中的一栏加起来)。树形结构无法生成易于阅读的报告。
Reactgular 2013年

54
SQL并不擅长返回数据,也不擅长查询所需内容。根据经验,如果您认为广泛使用的工具在常见用例中存在错误或损坏,那么问题就出在您身上。
肖恩·麦克索明

12
@SeanMcSomething真的很痛,我不能说自己更好。
WernerCD 2013年

5
这是一个很大的问题。回答“这就是事实”的答案没有抓住重点。为什么无法返回带有嵌入式行集合的行?
克里斯·皮特曼

8
@SeanMcSomething:除非该广泛使用的工具是C ++或PHP,否则您可能是对的。;)
梅森·惠勒2013年

Answers:


11

在关系数据库的内胆深处,它的所有行和列。这是关系数据库经过优化可使用的结构。 游标一次在单个行上工作。一些操作会创建临时表(同样,它必须是行和列)。

通过仅处理行并仅返回行,系统可以更好地处理内存和网络流量。

如前所述,这允许进行某些优化(索引,联接,联合等)。

如果要使用嵌套树结构,则需要一次提取所有数据。数据库端游标的优化已经一去不复返了。同样,通过网络的流量成为一个大的突发事件,它可能比逐行缓慢的trick流所花费的时间更长(这在当今的网络世界中偶尔会丢失)。

每种语言都包含数组。这些都是易于使用和交互的事情。通过使用非常原始的结构,数据库和程序之间的驱动程序(无论使用哪种语言)都可以以通用方式工作。一旦开始添加树木,该语言中的结构就会变得更加复杂,遍历起来也更加困难。

编程语言将返回的行转换为其他结构并不难。将其设置为树或哈希集,或将其保留为可迭代的行列表。

这里也有历史在起作用。在过去,传输结构化数据非常麻烦。查看EDI格式以大致了解您的要求。树还暗示着递归-某些语言不支持递归(过去两种最重要的语言不支持递归- 递归直到F90才进入Fortran,而COBOL时代也没有。)

尽管当今的语言支持递归和更高级的数据类型,但确实没有充分的理由来进行更改。他们工作,而且工作良好。那那些改变的东西是NoSQL数据库。您可以将树存储在基于文档的树中。LDAP(实际上已经很老了)也是一个基于树的系统(尽管可能不是您想要的)。谁知道呢,也许nosql数据库中的下一件事就是将查询作为json对象返回的对象。

但是,“旧的”关系数据库...它们正在处理行,因为那是它们擅长的事,并且所有内容都可以与他们交谈,而不会遇到麻烦或翻译。

  1. 在协议设计中,达到完美不是在没有什么可以补充的时候,而是在没有什么可以补充的时候。

RFC 1925-十二个网络真相


“如果要使用嵌套树结构,则需要一次提取所有数据。数据库端游标的优化已一去不复返了。” -听起来不对。它只需要维护几个游标:一个用于主表,然后一个用于每个联接表。根据接口的不同,它可能返回一行并将所有连接的表分成一个块(部分流式传输),或者它可以流式传输子树(甚至不查询它们),直到您开始对其进行迭代为止。但是,是的,这使事情变得非常复杂。
mpen

3
每种现代语言都应该具有某种树类,不是吗?而且不是由驾驶员来处理吗?我猜SQL专家仍然需要设计一种通用格式(对此了解不多)。但是让我感到困惑的是,我要么必须通过联接发送1个查询,然后取回并过滤掉每行的冗余数据(用户信息,它仅每N行更改一次),要么发出1条查询(用户) ,然后遍历结果,然后针对每条记录再发送两个查询(电子邮件,电话)以获取所需的信息。两种方法似乎都是浪费。
mpen

51

它正好返回您要的内容:单个记录集,其中包含由联接定义的笛卡尔乘积。在很多有效的情况下,这正是您想要的,因此说SQL给出了不好的结果(因此暗示,如果更改它会更好),实际上会使很多查询陷入困境。

您所经历的被称为“ 对象/关系阻抗不匹配 ”,这是由于面向对象的数据模型和关系数据模型在几个方面根本不同的事实而引起的技术难题。LINQ和其他框架(并非巧合地称为ORM,对象/关系映射器,并非巧合)不会神奇地“绕过此问题”。他们只是发出不同的查询。也可以在SQL中完成。这是我的处理方式:

SELECT * FROM users user where [criteria here]

迭代用户列表并创建ID列表。

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。我们常常得出这样的结论:技术存在问题,而不是得出结论:我们需要学习如何有效地使用技术。
马特2013年

1
热切获取模式用于这些集合时,Hibernate将在单个查询中检索根实体和某些集合。在这种情况下,它将减少内存中的根实体属性。其他ORM可能也可以这样做。
Mike Partridge 2013年

3
实际上,这不应该归咎于关系模型。它很好地处理了嵌套关系,谢谢。这纯粹是早期SQL版本中的实现错误。我认为虽然有更新的版本。
John Nilsson 2013年

8
您确定这是对象关系阻抗的示例吗?在我看来,关系模型与OP的概念数据模型完全匹配:每个用户都与一个零,一个或多个电子邮件地址列表相关联。该模型也可以在OO范式中完美使用(聚合:用户对象具有电子邮件的集合)。限制在于用于查询数据库的技术,这是一个实现细节。有一些查询技术可以返回分层数据,例如.Net中的分层数据集
MarkJ 2013年

@MarkJ,您应该将其写为答案。
Mindor先生,2013年

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结果分成列表。
Mindor先生2013年

2
如果电话号码具有“类型”,例如“小区”,“家庭”或“工作”,该怎么办?此外,从技术上讲,电子邮件地址中允许使用逗号(如果用引号引起来),那么我该如何拆分呢?
mpen 2015年

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

1
不鼓励使用*,但不能解决问题的症结所在。即使他选择了0个用户列,由于“电话”和“电子邮件”与“用户”的关系都是“ 1-很多”,因此他仍然可能会遇到重复现象。不同不会阻止电话号码两次出现,例如phone1 / name @ hotmail.com,phone1 / name @ google.com。
mike30 2013年

6
-1:“你的问题可能会得到解决”说,你不知道什么样的影响会改变从left joininner join。在这种情况下,这不会减少用户抱怨的“重复”次数;它只会忽略那些缺少电话或电子邮件的用户。几乎没有任何改善。同样,在解释“左侧的所有记录到右侧的所有记录”时,也会跳过该ON标准,该标准会修剪笛卡尔积中固有的所有“错误”关系,但会保留所有重复字段。
哈维尔2013年

@Javier:是的,这就是为什么我也说您实际上需要左联接,还是INNER JOIN就足够了?* OP对问题的描述使其听起来*听起来好像他们期望内部联接的结果。当然,如果没有任何示例数据或他们真正想要的描述,这很难说。我之所以提出建议,是因为我实际上已经看到人们(我与之共事的人)这样做:选择错误的联接,然后在他们不了解所获得的结果时抱怨。已经看到它,我认为这可能是发生在这里。
FrustratedWithFormsDesigner 2013年

3
您错过了问题的重点。在此假设示例中,我想要所有用户数据(姓名,身份证等),并且想要他/她的所有电话号码。内部联接不包括没有电子邮件或没有电话的用户-这有什么帮助?
mpen 2015年

4

查询总是产生一个矩形(无锯齿)的表格数据集。集中没有嵌套的子集。在布景世界中,所有东西都是纯净的非嵌套矩形。

您可以将联接并排放置2套。“打开”条件是每个集合中的记录如何匹配。如果用户有3个电话号码,那么您会在用户信息中看到3次重复。查询必须生成一个矩形的无锯齿的集合。这仅仅是具有一对多关系的集合的本质。

要获得所需的内容,必须使用类似Mason Wheeler所述的单独查询。

select * from Phones where user_id=344;

该查询的结果仍然是矩形无锯齿的集合。正如布景世界中的一切一样。


2

您必须确定瓶颈在哪里。数据库和应用程序之间的带宽通常非常快。大多数理由没有理由在一个调用和没有联接的情况下无法返回3个单独的数据集。然后,您可以根据需要将其全部加入到您的应用程序中。

否则,您希望数据库将此数据集放在一起,然后删除连接结果中每一行的所有重复值,而不必删除行本身具有重复数据的行,例如两个具有相同名称或电话号码的人。似乎需要大量开销来节省带宽。您最好集中精力返回较少的数据,并进行更好的过滤并删除不需要的列。因为Select *从未在依赖生产的生产井中使用。


“没有理由大多数数据库一次调用都不会返回3个单独的数据集,而且没有联接” –如何通过一次调用返回3个单独的数据集?我以为您必须发送3个不同的查询,从而导致每个查询之间的延迟?
mpen 2013年

可以在1个事务中调用一个存储过程,然后返回所需数量的数据集。也许需要“ SelectUserWithEmailsPhones”存储过程。
格雷厄姆

1
@Mark:作为同一批处理的一部分,您可以发送(至少在sql server中)多个命令。cmdText =“从b选择*;从a选择*;从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的复杂扩展,以便进一步查询它或将它与其他关系联系起来。

基本上,您将在关系DBMS之上构建分层DBMS。要获得可疑的好处,它将变得更加复杂,并且您将失去一致的关系系统的好处。

我理解为什么有时从SQL输出分层结构的数据有时会很方便,但是在整个DBMS中增加支持此功能的复杂性所付出的代价绝对不值得。


-4

Pl指的是STUFF函数的用法,该函数将一列(联系人)的多行(电话号码)分组,这些列可以提取为一行(用户)的分隔值的单个单元格。

今天,我们广泛使用了此功能,但面临一些较高的CPU和性能问题。XML数据类型是另一种选择,但它是设计更改,而不是查询级别。


5
请扩大这是如何解决问题的。与其说“请参考使用”,不如提供一个示例来说明如何解决所提出的问题。在使事情变得更清晰的地方引用第三方消息来源也很有帮助。
bitsoflogic

1
看起来就像STUFF是拼接。不确定如何应用于我的问题。
mpen
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.