¿Por qué los RDBMS no devuelven tablas unidas en un formato anidado?

14

Por ejemplo, supongamos que quiero buscar un Usuario y todos sus números de teléfono y direcciones de correo electrónico. Los números de teléfono y correos electrónicos se almacenan en tablas separadas, un usuario para muchos teléfonos / correos electrónicos. Puedo hacer esto con bastante facilidad:

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

El problema * con esto es que está devolviendo el nombre del usuario, el DOB, el color favorito y toda la otra información almacenada en la tabla del usuario una y otra vez para cada registro (los usuarios envían correos electrónicos a los registros de los teléfonos), presumiblemente consumiendo ancho de banda y disminuyendo la velocidad abajo los resultados.

¿No sería mejor si devolviera una sola fila para cada usuario, y dentro de ese registro hubiera una lista de correos electrónicos y una lista de teléfonos? También haría mucho más fácil trabajar con los datos.

Sé que puede obtener resultados como este usando LINQ o quizás otros marcos, pero parece ser una debilidad en el diseño subyacente de las bases de datos relacionales.

Podríamos evitar esto usando NoSQL, pero ¿no debería haber algún término medio?

¿Me estoy perdiendo de algo? ¿Por qué no existe esto?

* Sí, está diseñado de esta manera. Lo entiendo. Me pregunto por qué no hay una alternativa con la que sea más fácil trabajar. SQL podría seguir haciendo lo que está haciendo, pero luego podrían agregar una o dos palabras clave para realizar un pequeño procesamiento posterior que devuelva los datos en un formato anidado en lugar de un producto cartesiano.

Sé que esto se puede hacer en un lenguaje de script de su elección, pero requiere que el servidor SQL envíe datos redundantes (ejemplo a continuación) o que emita múltiples consultas como SELECT email FROM emails WHERE user_id IN (/* result of first query */).


En lugar de que MySQL devuelva algo similar a esto:

[
    {
        "name": "John Smith",
        "dob": "1945-05-13",
        "fav_color": "red",
        "email": "[email protected]",
    },
    {
        "name": "John Smith",
        "dob": "1945-05-13",
        "fav_color": "red",
        "email": "[email protected]",
    },
    {
        "name": "Jane Doe",
        "dob": "1953-02-19",
        "fav_color": "green",
        "email": "[email protected]",
    }
]

Y luego tener que agrupar un identificador único (¡lo que significa que también debo buscarlo!) Del lado del cliente para volver a formatear el conjunto de resultados como lo desea, solo devuelva esto:

[
    {
        "name": "John Smith",
        "dob": "1945-05-13",
        "fav_color": "red",
        "emails": ["[email protected]", "[email protected]"]
    },
    {
        "name": "Jane Doe",
        "dob": "1953-02-19",
        "fav_color": "green",
        "emails": ["[email protected]"],
    }
]

Alternativamente, puedo emitir 3 consultas: 1 para los usuarios, 1 para los correos electrónicos y 1 para los números de teléfono, pero luego los conjuntos de resultados de correo electrónico y número de teléfono deben contener el ID de usuario para que pueda volver a emparejarlos con los usuarios Anteriormente fui a buscar. Nuevamente, datos redundantes y posprocesamiento innecesario.

mpen
fuente
66
Piense en SQL como una hoja de cálculo, como en Microsoft Excel, luego intente descubrir cómo crear un valor de celda que contenga celdas internas. Ya no funciona bien como una hoja de cálculo. Lo que está buscando es una estructura de árbol, pero ya no tiene los beneficios de una hoja de cálculo (es decir, no puede sumar una columna en un árbol). Las estructuras de los árboles no hacen informes legibles para humanos.
Reactgular
54
SQL no es malo para devolver datos, eres malo para consultar lo que quieres. Como regla general, si cree que una herramienta ampliamente utilizada tiene errores o está rota para un caso de uso común, el problema es usted.
Sean McSomething
12
@SeanMcSomething Tan cierto que duele, no podría haberlo dicho mejor.
WernerCD
55
Esta es una gran pregunta. Las respuestas que dicen "esta es la forma en que es" están perdiendo el punto. ¿Por qué no es posible devolver filas con colecciones incrustadas de filas?
Chris Pitman
8
@SeanMcSomething: a menos que esa herramienta ampliamente utilizada sea C ++ o PHP, en cuyo caso probablemente tenga razón. ;)
Mason Wheeler

Respuestas:

11

En el fondo, en las entrañas de una base de datos relacional, son todas las filas y columnas. Esa es la estructura con la que una base de datos relacional está optimizada para trabajar. Los cursores trabajan en filas individuales a la vez. Algunas operaciones crean tablas temporales (nuevamente, deben ser filas y columnas).

Al trabajar solo con filas y devolver solo filas, el sistema puede manejar mejor la memoria y el tráfico de red.

Como se mencionó, esto permite que se realicen ciertas optimizaciones (índices, uniones, uniones, etc.)

Si uno quisiera una estructura de árbol anidada, esto requiere que uno extraiga todos los datos a la vez. Atrás quedaron las optimizaciones para los cursores en el lado de la base de datos. Del mismo modo, el tráfico a través de la red se convierte en una gran explosión que puede tomar mucho más tiempo que el lento goteo de fila por fila (esto es algo que ocasionalmente se pierde en el mundo web actual).

Cada idioma tiene matrices dentro de él. Estas son cosas fáciles para trabajar e interactuar con ellas. Al usar una estructura muy primitiva, el controlador entre la base de datos y el programa, sin importar el idioma, puede funcionar de manera común. Una vez que uno comienza a agregar árboles, las estructuras en el lenguaje se vuelven más complejas y más difíciles de atravesar.

No es tan difícil para un lenguaje de programación convertir las filas devueltas en alguna otra estructura. Conviértalo en un árbol o un conjunto hash o déjelo como una lista de filas sobre las que puede iterar.

También hay historia en el trabajo aquí. La transferencia de datos estructurados era algo feo en los viejos tiempos. Mire el formato EDI para tener una idea de lo que podría estar pidiendo. Los árboles también implican recursividad, que algunos idiomas no admitían (los dos idiomas más importantes de los viejos tiempos no admitían la recursividad: la recursión no entró en Fortran hasta F90 y, en la época, COBOL tampoco).

Y aunque los idiomas de hoy tienen soporte para la recursividad y los tipos de datos más avanzados, no hay realmente una buena razón para cambiar las cosas. Funcionan y funcionan bien. Los que están cambiando las cosas son las bases de datos nosql. Puede almacenar árboles en documentos en uno basado en documentos. LDAP (en realidad es antiguo) también es un sistema basado en árbol (aunque probablemente no sea lo que buscas). Quién sabe, quizás lo siguiente en las bases de datos nosql será uno que devuelva la consulta como un objeto json.

Sin embargo, las bases de datos relacionales "antiguas" ... están trabajando con filas porque eso es en lo que son buenos y todo puede hablarles sin problemas ni traducción.

  1. En el diseño del protocolo, la perfección se ha alcanzado no cuando no queda nada por agregar, sino cuando no queda nada por quitar.

De RFC 1925 - Las doce verdades de redes


fuente
"Si uno quisiera una estructura de árbol anidada, esto requiere que uno extraiga todos los datos a la vez. Atrás quedaron las optimizaciones para los cursores en el lado de la base de datos". - Eso no suena cierto. Solo tendría que mantener un par de cursores: uno para la tabla principal y luego uno para cada tabla unida. Dependiendo de la interfaz, puede devolver una fila y todas las tablas unidas en un fragmento (parcialmente transmitido), o puede transmitir los subárboles (y tal vez ni siquiera consultarlos) hasta que comience a iterarlos. Pero sí, eso complica mucho las cosas.
mpen
3
Sin embargo, cada lenguaje moderno debería tener algún tipo de clase de árbol, ¿no? ¿Y no dependería del conductor lidiar con eso? Supongo que los chicos de SQL todavía necesitan diseñar un formato común (no sé mucho sobre eso). Sin embargo, lo que me sorprende es que tengo que enviar 1 consulta con combinaciones, y volver y filtrar los datos redundantes que cada fila (la información del usuario, que solo cambia cada enésima fila), o emitir 1 consulta (usuarios) , y repita los resultados, luego envíe dos consultas más (correos electrónicos, teléfonos) para cada registro para obtener la información que necesito. Cualquiera de los métodos parece un desperdicio.
mpen
51

Está devolviendo exactamente lo que solicitó: un conjunto de registros único que contiene el producto cartesiano definido por las uniones. Hay muchos escenarios válidos en los que eso es exactamente lo que desearía, por lo que decir que SQL está dando un mal resultado (y, por lo tanto, implica que sería mejor si lo cambiara) realmente arruinaría muchas consultas.

Lo que está experimentando se conoce como " desajuste de impedancia de objeto / relación " , las dificultades técnicas que surgen del hecho de que el modelo de datos orientado a objetos y el modelo de datos relacionales son fundamentalmente diferentes de varias maneras. LINQ y otros marcos (conocidos como ORM, Object / Relational Mappers, no por casualidad) no mágicamente "evitan esto"; solo emiten consultas diferentes. También se puede hacer en SQL. Así es como lo haría:

SELECT * FROM users user where [criteria here]

Itere la lista de usuarios y haga una lista de ID.

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

Y luego te unes al lado del cliente. Así es como LINQ y otros marcos lo hacen. No hay magia real involucrada; Solo una capa de abstracción.

Mason Wheeler
fuente
14
+1 para "exactamente lo que pediste". Con demasiada frecuencia llegamos a la conclusión de que hay algo mal con la tecnología en lugar de la conclusión de que necesitamos aprender a usar la tecnología de manera efectiva.
Matt
1
Hibernate recuperará la entidad raíz y ciertas colecciones en una sola consulta cuando se use el modo de búsqueda ansioso para esas colecciones; en ese caso, hace la reducción de las propiedades de la entidad raíz en la memoria. Otros ORM probablemente pueden hacer lo mismo.
Mike Partridge
3
En realidad, esto no tiene la culpa del modelo relacional. Se adapta muy bien a las relaciones anidadas, gracias. Esto es puramente un error de implementación en las primeras versiones de SQL. Sin embargo, creo que las versiones más recientes lo han agregado.
John Nilsson
8
¿Estás seguro de que este es un ejemplo de impedancia relacional de objetos? Me parece que el modelo relacional coincide perfectamente con el modelo de datos conceptual del OP: cada usuario está asociado con una lista de cero, una o más direcciones de correo electrónico. Ese modelo también es perfectamente utilizable en un paradigma OO (agregación: el objeto de usuario tiene una colección de correos electrónicos). La limitación está en la técnica que se utiliza para consultar la base de datos, que es un detalle de implementación. Existen técnicas de consulta en torno a las cuales se devuelven datos jerárquicos, por ejemplo, conjuntos de datos jerárquicos en .Net
MarkJ
@ MarkJ deberías escribir eso como respuesta.
Mr.Mindor
12

Puede usar una función integrada para concatenar los registros juntos. En MySQL puede usar la GROUP_CONCAT()función y en Oracle puede usar la LISTAGG()función.

Aquí hay una muestra de cómo se vería una consulta en 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 

Esto devolvería 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
Persistir
fuente
Esta parece ser la solución más cercana (en SQL) a lo que el OP está intentando hacer. Potencialmente, todavía tendrá que hacer el procesamiento del lado del cliente para dividir los resultados de EmailAddresses y PhoneNumbers en listas.
Mr.Mindor
2
¿Qué sucede si el número de teléfono tiene un "tipo", como "Celular", "Casa" o "Trabajo"? Además, las comas están técnicamente permitidas en las direcciones de correo electrónico (si están citadas), ¿cómo lo dividiría entonces?
mpen
10

El problema con esto es que está devolviendo el nombre del usuario, el DOB, el color favorito y toda la otra información almacenada

El problema es que no estás siendo lo suficientemente selectivo. Pediste todo cuando dijiste

Select * from...

... y lo tienes (incluyendo DOB ​​y colores favoritos).

Probablemente deberías ser un poco más (ejem) ... selectivo, y dijiste algo como:

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

También es posible que vea registros que parecen duplicados porque se userpuede unir a varios emailregistros, pero el campo que distingue estos dos no está en su Selectdeclaración, por lo que es posible que desee decir algo como

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

... una y otra vez para cada registro ...

Además, me doy cuenta de que estás haciendo un LEFT JOIN. Esto unirá todos los registros a la izquierda de la unión (es decir users) a todos los registros a la derecha, o en otras palabras:

Una combinación externa izquierda devuelve todos los valores de una combinación interna más todos los valores de la tabla izquierda que no coinciden con la tabla derecha.

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

Entonces, otra pregunta es ¿realmente necesita una unión izquierda, o habría INNER JOINsido suficiente? Son tipos muy diferentes de combinaciones.

No sería mejor si devolviera una sola fila para cada usuario, y dentro de ese registro había una lista de correos electrónicos

Si realmente desea que una sola columna dentro del conjunto de resultados contenga una lista que se genera sobre la marcha, puede hacerlo, pero varía según la base de datos que esté utilizando. Oracle tiene la listaggfunción .


En última instancia, creo que su problema podría resolverse si reescribe su consulta cerca de algo como esto:

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
fuente
1
se desaconseja el uso de * pero no es el quid de su problema. Incluso si selecciona 0 columnas de usuario, puede experimentar un efecto de duplicación ya que tanto los teléfonos como los correos electrónicos tienen una relación de 1 a muchos con los usuarios. Distinto no evitaría que un número de teléfono aparezca dos veces ala phone1/[email protected], phone1/[email protected].
mike30
66
-1: "su problema podría resolverse" dice que no sabe qué efecto cambiaría de left joina inner join. En este caso, esto no reducirá las "repeticiones" de las que se queja el usuario; simplemente omitiría a aquellos usuarios que carecen de teléfono o correo electrónico. Casi ninguna mejora. también, al interpretar "todos los registros de la izquierda a todos los registros de la derecha" omite los ONcriterios, que elimina todas las relaciones 'incorrectas' inherentes al producto cartesiano pero mantiene todos los campos repetidos.
Javier
@Javier: Sí, por eso también dije que realmente necesitas una unión izquierda, o ¿habría sido suficiente una UNIÓN INTERNA? * La descripción del problema por parte de OP lo hace * sonar como si esperaran el resultado de una unión interna. Por supuesto, sin datos de muestra o una descripción de lo que realmente querían, es difícil de decir. Hice la sugerencia porque realmente he visto a personas (con las que trabajo) hacer esto: elegir la unión incorrecta y luego quejarse cuando no entienden los resultados que obtienen. Al verlo , pensé que podría haber sucedido aquí.
FrustratedWithFormsDesigner
3
Te estás perdiendo el punto de la pregunta. En este ejemplo hipotético, quiero todos los datos del usuario (nombre, nombre, etc.) y quiero todos sus números de teléfono. Una combinación interna excluye a los usuarios sin correo electrónico o sin teléfonos, ¿cómo ayuda eso?
mpen
4

Las consultas siempre producen un conjunto de datos tabular rectangular (no irregular). No hay subconjuntos anidados dentro de un conjunto. En el mundo de los conjuntos, todo es un rectángulo puro no anidado.

Puedes pensar en una unión como poner 2 juegos uno al lado del otro. La condición "on" es cómo se combinan los registros de cada conjunto. Si un usuario tiene 3 números de teléfono, verá una duplicación de 3 veces en la información del usuario. La consulta debe generar un conjunto rectangular sin dientes. Es simplemente la naturaleza de unir conjuntos con una relación de 1 a muchos.

Para obtener lo que desea, debe usar una consulta separada como la descrita por Mason Wheeler.

select * from Phones where user_id=344;

El resultado de esta consulta sigue siendo un conjunto rectangular no dentado. Como es todo en el mundo de los sets.

mike30
fuente
2

Tienes que decidir dónde existen los cuellos de botella. El ancho de banda entre su base de datos y la aplicación suele ser bastante rápido. No hay razón para que la mayoría de las bases de datos no puedan devolver 3 conjuntos de datos separados dentro de una llamada y no se unan. Luego puedes unirte a todo en tu aplicación si quieres.

De lo contrario, desea que la base de datos reúna este conjunto de datos y luego elimine todos los valores repetidos en cada fila que son el resultado de las uniones y no necesariamente las filas mismas que tienen datos duplicados como dos personas con el mismo nombre o número de teléfono. Parece un montón de gastos generales para ahorrar en ancho de banda. Sería mejor concentrarse en devolver menos datos con un mejor filtrado y eliminar las columnas que no necesita. Porque Select * nunca se usa en la producción, eso depende.

JeffO
fuente
"No hay razón para que la mayoría de las bases de datos no puedan devolver 3 conjuntos de datos separados dentro de una llamada y no se unan" - ¿Cómo logra que devuelva 3 conjuntos de datos separados con una sola llamada? Pensé que tenía que enviar 3 consultas diferentes, lo que introduce la latencia entre cada una.
mpen
Se puede llamar a un procedimiento almacenado en 1 transacción y luego devolver tantos conjuntos de datos como desee. Tal vez se necesita un programa "SelectUserWithEmailsPhones".
Graham
1
@ Mark: puede enviar (en el servidor sql al menos) más de un comando como parte del mismo lote. cmdText = "select * from b; select * from a; select * from c" y luego úselo como texto de comando para el comando sqlcommand.
jmoreno 01 de
2

De manera muy simple, no una sus datos si desea resultados distintos para una consulta de usuario y una consulta de número de teléfono, de lo contrario, como otros han señalado, el "Conjunto" o los datos contendrán campos adicionales para cada fila.

Emita 2 consultas distintas en lugar de una con una combinación.

En el procedimiento almacenado o consultas en línea sql craft 2 parametrizadas y devuelve los resultados de ambas. La mayoría de las bases de datos y los idiomas admiten múltiples conjuntos de resultados.

Por ejemplo, SQL Server y C # logran esta funcionalidad usando IDataReader.NextResult() .

Jon Raynor
fuente
1

Te estás perdiendo algo. Si desea desnormalizar sus datos, debe hacerlo usted mismo.

;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
fuente
1

El concepto de cierre relacional básicamente significa que el resultado de cualquier consulta es una relación que puede usarse en otras consultas como si fuera una tabla base. Este es un concepto poderoso porque hace que las consultas sean componibles.

Si SQL le permitiera escribir consultas que generen estructuras de datos anidadas, rompería este principio. Una estructura de datos anidada no es una relación, por lo que necesitaría un nuevo lenguaje de consulta o extensiones complejas de SQL para consultarlo más o unirlo a otras relaciones.

Básicamente, construiría un DBMS jerárquico sobre un DBMS relacional. Será mucho más complejo para un beneficio dudoso, y perderá las ventajas de un sistema relacional consistente.

Entiendo por qué a veces sería conveniente poder generar datos estructurados jerárquicamente desde SQL, pero el costo en la complejidad adicional en todo el DBMS para respaldar esto definitivamente no vale la pena.

JacquesB
fuente
-4

Los Pls se refieren al uso de la función STUFF que agrupa varias filas (números de teléfono) de una columna (contacto) que se pueden extraer como una sola celda de valores delimitados de una fila (usuario).

Hoy lo usamos ampliamente pero enfrentamos algunos problemas de CPU y rendimiento elevados. El tipo de datos XML es otra opción, pero es un cambio de diseño, no una consulta de nivel uno.

Shriram Rajagopal
fuente
55
Expande cómo esto resuelve la pregunta. En lugar de decir "Pls se refieren al uso de", proporcione un ejemplo de cómo esto lograría la pregunta formulada. También puede ser útil citar fuentes de terceros donde aclara las cosas.
bitsoflogic
1
Parece que STUFFes similar al empalme. No estoy seguro de cómo se aplica eso a mi pregunta.
mpen