Por que os RDBMSes não retornam tabelas unidas em um formato aninhado?


14

Por exemplo, digamos que eu queira buscar um usuário e todos os seus números de telefone e endereços de email. Os números de telefone e e-mails são armazenados em tabelas separadas, um usuário para muitos telefones / e-mails. Eu posso fazer isso facilmente:

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

O problema * disso é que ele retorna o nome do usuário, a data de nascimento, a cor favorita e todas as outras informações armazenadas na tabela de usuários repetidamente para cada registro (os usuários enviam e-mails para registros de telefones), presumivelmente consumindo largura de banda e diminuindo a velocidade abaixo os resultados.

Não seria melhor se retornasse uma única linha para cada usuário e, dentro desse registro, houvesse uma lista de e-mails e uma lista de telefones? Isso tornaria os dados muito mais fáceis de trabalhar também.

Eu sei que você pode obter resultados como esse usando o LINQ ou talvez outras estruturas, mas parece ser uma fraqueza no design subjacente dos bancos de dados relacionais.

Poderíamos contornar isso usando o NoSQL, mas não deveria haver meio termo?

Estou esquecendo de algo? Por que isso não existe?

* Sim, foi projetado dessa maneira. Entendi. Gostaria de saber por que não há uma alternativa mais fácil de se trabalhar. O SQL pode continuar fazendo o que está fazendo, mas pode adicionar uma ou duas palavras-chave para fazer um pós-processamento que retorna os dados em um formato aninhado, em vez de um produto cartesiano.

Eu sei que isso pode ser feito em uma linguagem de script de sua escolha, mas exige que o servidor SQL envie dados redundantes (exemplo abaixo) ou que você faça várias consultas como SELECT email FROM emails WHERE user_id IN (/* result of first query */).


Em vez de o MySQL retornar algo parecido com isto:

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

E, em seguida, tendo que agrupar algum identificador exclusivo (o que significa que também preciso buscá-lo!) Do lado do cliente para reformatar o conjunto de resultados como você deseja, basta retornar o seguinte:

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

Como alternativa, posso emitir 3 consultas: 1 para os usuários, 1 para os e-mails e 1 para os números de telefone, mas os conjuntos de resultados de e-mail e número de telefone precisam conter o user_id para que eu possa compará-los com os usuários Eu peguei anteriormente. Novamente, dados redundantes e pós-processamento desnecessário.


6
Pense no SQL como uma planilha, como no Microsoft Excel, e tente descobrir como criar um valor de célula que contenha células internas. Já não funciona bem como uma planilha. O que você procura é uma estrutura em árvore, mas não possui mais os benefícios de uma planilha (ou seja, não é possível totalizar uma coluna em uma árvore). As estruturas em árvore não geram relatórios legíveis para humanos.
Reactgular

54
SQL não é ruim em retornar dados, você é ruim em consultar o que deseja. Como regra geral, se você acha que uma ferramenta amplamente usada está com defeito ou com defeito para um caso de uso comum, o problema é você.
Sean McSomething

12
@SeanMcSomething Tão verdade que dói, eu não poderia ter dito melhor.
WernerCD

5
Esta é uma ótima pergunta. As respostas que estão dizendo "é assim" estão faltando. Por que não é possível retornar linhas com coleções de linhas incorporadas?
Chris Pitman

8
@SeanMcSomething: A menos que essa ferramenta amplamente usada seja C ++ ou PHP, nesse caso, você provavelmente está certo. ;)
Mason Wheeler

Respostas:


11

No fundo, nas entranhas de um banco de dados relacional, todas as linhas e colunas. Essa é a estrutura com a qual um banco de dados relacional é otimizado para trabalhar. Os cursores trabalham em linhas individuais por vez. Algumas operações criam tabelas temporárias (novamente, precisam ser linhas e colunas).

Trabalhando apenas com linhas e retornando apenas linhas, o sistema pode lidar melhor com o tráfego de memória e rede.

Como mencionado, isso permite que certas otimizações sejam feitas (índices, junções, uniões, etc ...)

Se alguém quiser uma estrutura de árvore aninhada, isso exige que você puxe todos os dados de uma vez. Longe vão as otimizações para os cursores no lado do banco de dados. Da mesma forma, o tráfego na rede se torna uma grande explosão que pode demorar muito mais do que o lento fluxo de linha por linha (isso é algo que ocasionalmente se perde no mundo da web de hoje).

Todo idioma possui matrizes dentro dele. Essas são coisas fáceis de trabalhar e interagir. Ao usar uma estrutura muito primitiva, o driver entre o banco de dados e o programa - independentemente da linguagem - pode funcionar de maneira comum. Quando alguém começa a adicionar árvores, as estruturas na linguagem se tornam mais complexas e mais difíceis de atravessar.

Não é tão difícil para uma linguagem de programação converter as linhas retornadas em alguma outra estrutura. Transforme-o em uma árvore ou conjunto de hash ou deixe-o como uma lista de linhas nas quais você pode iterar.

Também há história trabalhando aqui. A transferência de dados estruturados era algo feio nos tempos antigos. Veja o formato EDI para ter uma idéia do que você pode estar solicitando. As árvores também implicam recursão - que alguns idiomas não suportam (os dois idiomas mais importantes da antigüidade não suportavam recursão - a recursão não entrou no Fortran até F90 e, na época, a COBOL também não).

E embora os idiomas de hoje tenham suporte para recursão e tipos de dados mais avançados, não há realmente um bom motivo para mudar as coisas. Eles funcionam e funcionam bem. Os que estão mudando as coisas são os bancos de dados nosql. Você pode armazenar árvores em documentos em um baseado em documento. O LDAP (é realmente antigo) também é um sistema baseado em árvore (embora provavelmente não seja o que você procura). Quem sabe, talvez a próxima coisa nos bancos de dados nosql seja aquela que retorne a consulta como um objeto json.

No entanto, os bancos de dados relacionais 'antigos' ... eles estão trabalhando com linhas porque é nisso que eles são bons e tudo pode conversar com eles sem problemas ou tradução.

  1. No design do protocolo, a perfeição foi alcançada não quando não há mais nada a acrescentar, mas quando não há mais nada a ser retirado.

Da RFC 1925 - As doze verdades da rede


"Se alguém quiser uma estrutura de árvore aninhada, isso exige que você puxe todos os dados de uma vez. Longe estão as otimizações para os cursores no lado do banco de dados". - Isso não parece verdade. Teria apenas que manter alguns cursores: um para a tabela principal e um para cada tabela unida. Dependendo da interface, ele pode retornar uma linha e todas as tabelas unidas em um pedaço (parcialmente transmitido) ou transmitir as subárvores (e talvez nem mesmo consultá-las) até você começar a iterá-las. Mas sim, isso está complicando bastante as coisas.
fácil

3
Toda linguagem moderna deveria ter algum tipo de classe de árvore, não? E não caberia ao motorista lidar com isso? Eu acho que os caras do SQL ainda precisam criar um formato comum (não sabem muito sobre isso). O que me impressiona é que eu tenho que enviar 1 consulta com junções e voltar e filtrar os dados redundantes que cada linha (as informações do usuário, que apenas alteram a enésima linha), ou emitir 1 consulta (usuários) e faça um loop nos resultados e envie mais duas consultas (e-mails, telefones) para cada registro para buscar as informações necessárias. Qualquer um dos métodos parece um desperdício.
MPEN

51

Ele está retornando exatamente o que você pediu: um único conjunto de registros contendo o produto cartesiano definido pelas junções. Existem muitos cenários válidos nos quais é exatamente o que você deseja, portanto, dizer que o SQL está apresentando um resultado ruim (e, portanto, sugerindo que seria melhor se você o alterasse) realmente iria estragar muitas consultas.

O que você está enfrentando é conhecido como " Incompatibilidade de Impedância de Objeto / Relacional " , as dificuldades técnicas que surgem do fato de que o modelo de dados orientado a objetos e o modelo de dados relacionais são fundamentalmente diferentes de várias maneiras. O LINQ e outras estruturas (conhecidas como ORMs, Object / Relational Mappers, não por coincidência) não magicamente "contornam isso"; eles apenas emitem consultas diferentes. Isso também pode ser feito em SQL. Aqui está como eu faria isso:

SELECT * FROM users user where [criteria here]

Itere a lista de usuários e faça uma lista de IDs.

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

E então você faz a junção do lado do cliente. É assim que o LINQ e outras estruturas o fazem. Não há mágica real envolvida; apenas uma camada de abstração.


14
+1 em "exatamente o que você pediu". Frequentemente, chegamos à conclusão de que há algo errado com a tecnologia, em vez da conclusão de que precisamos aprender a usar a tecnologia de maneira eficaz.
Matt

1
Hibernate irá recuperar a entidade raiz e algumas coleções em uma única consulta, quando o ansioso modo de busca é usado para essas coleções; nesse caso, faz a redução das propriedades da entidade raiz na memória. Outros ORMs provavelmente podem fazer o mesmo.
Mike Partridge

3
Na verdade, isso não é culpa do modelo relacional. Ele lida muito bem com relações aninhadas, obrigado. Isso é puramente um bug de implementação nas versões anteriores do SQL. Acho que versões mais recentes o adicionaram.
John Nilsson

8
Tem certeza de que este é um exemplo de impedância objeto-relacional? Parece-me que o modelo relacional corresponde perfeitamente ao modelo de dados conceitual do OP: cada usuário está associado a uma lista de zero, um ou mais endereços de email. Esse modelo também é perfeitamente utilizável em um paradigma OO (agregação: o objeto de usuário possui uma coleção de emails). A limitação está na técnica usada para consultar o banco de dados, que é um detalhe de implementação. Existem técnicas de consulta em torno do qual fazem retornar dados hierárquica, por exemplo, conjuntos de dados hierárquica em .Net
MarkJ

@ MarkJ, você deve escrever isso como resposta.
Mr.Mindor

12

Você pode usar uma função interna para concatenar os registros juntos. No MySQL você pode usar a GROUP_CONCAT()função e no Oracle você pode usar a LISTAGG()função.

Aqui está um exemplo de como uma consulta pode parecer no 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 

Isso retornaria algo como

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

Essa parece ser a solução mais próxima (em SQL) do que o OP está tentando fazer. Ele ainda precisará processar o lado do cliente para dividir os resultados EmailAddresses e PhoneNumbers em listas.
Mr.Mindor

2
E se o número de telefone tiver um "tipo", como "Celular", "Casa" ou "Trabalho"? Além disso, as vírgulas são tecnicamente permitidas nos endereços de e-mail (se citadas) - como eu as dividiria?
M28:

10

O problema é que ele retorna o nome do usuário, a data de nascimento, a cor favorita e todas as outras informações armazenadas

O problema é que você não está sendo seletivo o suficiente. Você pediu tudo quando disse

Select * from...

... e você conseguiu (incluindo DOB ​​e cores favoritas).

Você provavelmente deveria ser um pouco mais (ahem) ... seletivo, e disse algo como:

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

Também é possível que você veja registros que se parecem com duplicatas porque um userpode se associar a vários emailregistros, mas o campo que distingue esses dois não está na sua Selectdeclaração, então você pode querer dizer algo como

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

... repetidamente para cada registro ...

Além disso, percebo que você está fazendo um LEFT JOIN. Isso unirá todos os registros à esquerda da união (ou seja users) a todos os registros à direita ou em outras palavras:

Uma junção externa esquerda retorna todos os valores de uma junção interna mais todos os valores da tabela esquerda que não correspondem à tabela direita.

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

Então, outra pergunta é: você realmente precisa de uma associação à esquerda ou seria INNER JOINsuficiente? Eles são tipos muito diferentes de junções.

Não seria melhor se ele retornasse uma única linha para cada usuário e, dentro desse registro, houvesse uma lista de e-mails

Se você realmente deseja que uma única coluna dentro do conjunto de resultados contenha uma lista gerada rapidamente, isso pode ser feito, mas isso varia dependendo do banco de dados que você está usando. Oracle tem a listaggfunção .


Por fim, acho que seu problema pode ser resolvido se você reescrever sua consulta próximo a algo assim:

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
usar * é desencorajado, mas não é o ponto crucial do seu problema. Mesmo se ele selecionar 0 colunas de usuário, ele ainda poderá sofrer um efeito de duplicação, pois os telefones e os emails têm uma relação de muitos com os usuários. O Distinct não impediria que um número de telefone aparecesse duas vezes ala phone1/name@hotmail.com, phone1/name@google.com.
precisa saber é o seguinte

6
-1: "seu problema pode estar resolvido" diz que você não sabe para qual efeito a alteração mudaria de left joinpara inner join. Nesse caso, isso não reduzirá as "repetições" das quais o usuário está reclamando; simplesmente omitiria os usuários que não possuem telefone ou email. quase nenhuma melhoria. além disso, ao interpretar "todos os registros à esquerda para todos os registros à direita" pula os ONcritérios, que eliminam todas as relações "erradas" inerentes ao produto cartesiano, mas mantêm todos os campos repetidos.
Javier

@Javier: Sim, é por isso que eu também disse que você realmente precisa de uma junção esquerda, ou seria uma INNER JOIN suficiente? A descrição do problema pelo OP faz parecer que eles estavam esperando o resultado de uma junção interna. Obviamente, sem dados de amostra ou uma descrição do que eles realmente queriam, é difícil dizer. Fiz a sugestão porque realmente vi pessoas (com quem trabalho) fazerem o seguinte: escolha a união errada e depois reclame quando não entenderem os resultados obtidos. Tendo visto , pensei que poderia ter acontecido aqui.
FrustratedWithFormsDesigner

3
Você está perdendo o objetivo da pergunta. Neste exemplo hipotético, quero todos os dados do usuário (nome, data de nascimento, etc.) e todos os seus números de telefone. Uma associação interna exclui usuários sem e-mails ou telefones - como isso ajuda?
M26:

4

As consultas sempre produzem um conjunto de dados tabular retangular (não irregular). Não há subconjuntos aninhados em um conjunto. No mundo dos sets, tudo é um retângulo não aninhado puro.

Você pode pensar em uma junção como colocar dois conjuntos lado a lado. A condição "on" é como os registros em cada conjunto são correspondidos. Se um usuário tiver três números de telefone, você verá uma duplicação de três vezes nas informações do usuário. Um conjunto retangular não irregular deve ser produzido pela consulta. É simplesmente a natureza de unir conjuntos com um relacionamento de 1 para muitos.

Para obter o que deseja, você deve usar uma consulta separada, como descrito por Mason Wheeler.

select * from Phones where user_id=344;

O resultado dessa consulta ainda é um conjunto não recortado retangular. Como é tudo no mundo dos sets.


2

Você precisa decidir onde existem os gargalos. A largura de banda entre o banco de dados e o aplicativo geralmente é bastante rápida. Não há motivo para que a maioria dos bancos de dados não retorne três conjuntos de dados separados em uma chamada e nenhuma associação. Depois, você poderá juntar tudo no seu aplicativo, se quiser.

Caso contrário, você deseja que o banco de dados junte esse conjunto de dados e remova todos os valores repetidos em cada linha que são o resultado das junções e não necessariamente as próprias linhas com dados duplicados, como duas pessoas com o mesmo nome ou número de telefone. Parece muita sobrecarga para economizar largura de banda. É melhor você se concentrar em retornar menos dados com uma melhor filtragem e remoção das colunas de que não precisa. Porque o Select * nunca é usado na produção - isso depende.


"Não há razão para a maioria dos bancos de dados não poder retornar 3 conjuntos de dados separados em uma chamada e nenhuma associação" - Como você consegue retornar 3 conjuntos de dados separados com uma chamada? Eu pensei que você tinha que enviar 3 consultas diferentes, o que introduz a latência entre cada uma?
MPEN

Um procedimento armazenado pode ser chamado em 1 transação e retornar quantos conjuntos de dados você desejar. Talvez seja necessário um sproc "SelectUserWithEmailsPhones".
Graham

1
@ Mark: você pode enviar (no servidor sql pelo menos) mais de um comando como parte do mesmo lote. cmdText = "selecione * de b; selecione * de a; selecione * de c" e use-o como texto de comando para o comando sql.
jmoreno

2

Muito simplesmente, não junte seus dados se desejar resultados distintos para uma consulta de usuário e uma consulta de número de telefone; caso contrário, como outros usuários apontaram, o "Conjunto" ou os dados conterão campos extras para cada linha.

Emita 2 consultas distintas em vez de uma com uma associação.

No procedimento armazenado ou consultas sql craft parametrizadas em linha 2 e retorne os resultados de ambas. A maioria dos bancos de dados e idiomas suporta vários conjuntos de resultados.

Por exemplo, SQL Server e C # realizam funcionalidade usando isso IDataReader.NextResult().


1

Está faltando alguma coisa. Se você quiser desnormalizar seus dados, precisará fazer isso sozinho.

;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

O conceito de fechamento relacional basicamente significa que o resultado de qualquer consulta é uma relação que pode ser usada em outras consultas como se fosse uma tabela base. Esse é um conceito poderoso, pois torna as consultas composíveis.

Se o SQL permitisse escrever consultas que produzissem estruturas de dados aninhadas, você quebraria esse princípio. Uma estrutura de dados aninhada não é uma relação; portanto, você precisaria de uma nova linguagem de consulta ou extensões complexas para o SQL, a fim de consultá-la ainda mais ou associá-la a outras relações.

Basicamente, você criaria um DBMS hierárquico em cima de um DBMS relacional. Será muito mais complexo para um benefício duvidoso, e você perderá as vantagens de um sistema relacional consistente.

Entendo por que às vezes seria conveniente poder gerar dados estruturados hierarquicamente a partir do SQL, mas o custo da complexidade adicional em todo o DBMS para suportar isso definitivamente não vale a pena.


-4

Pls referem-se ao uso da função STUFF, que agrupa várias linhas (números de telefone) de uma coluna (contato) que podem ser extraídas como uma única célula de valores delimitados de uma linha (usuário).

Hoje estamos usando extensivamente isso, mas enfrentamos alguns problemas altos de CPU e desempenho. O tipo de dados XML é outra opção, mas é uma alteração de design e não um nível de consulta.


5
Por favor, expanda como isso resolve a questão. Em vez de dizer "Pls se refere ao uso de", forneça um exemplo de como isso alcançaria a pergunta. Também pode ser útil citar fontes de terceiros, onde isso torna as coisas mais claras.
Bitsoflogic 17/07/2018

1
Parece que STUFFé semelhante a uma emenda. Não sei como isso se aplica à minha pergunta.
MJ #
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.