- El inner join en una base de datos relacional va más allá de una simple sintaxis de SQL: la misma estructura puede interpretarse de distintas formas desde la consulta, los bucles anidados, el modelo lógico, la verificación de tipos y el álgebra
- En tablas normalizadas, el join se vuelve la herramienta más práctica para seguir referencias y volver a combinar información almacenada sin duplicación
- Desde la perspectiva de implementación, puede verse como recorrer pares de filas y conservar solo las combinaciones que cumplen la condición, o como seleccionar, desde los dominios de columnas, solo las combinaciones de valores presentes en ambas relaciones
- En el modelo de programación, el join puede explicarse con
flatMap,LATERALde SQL, la resolución del problema N+1 en un ORM, la verificación de tipos basada en traits de Rust yandThendel mónada Set - Matemáticamente, los caminos en grafos, el modelo mínimo, la relación admisible más grande, la mínima cota superior de un orden parcial y el producto de anillo de expresiones relacionales revelan la misma propiedad del join
En datos normalizados, un join se convierte en una consulta
- De la forma más práctica, un join puede verse como una operación para consultar un valor o adjuntar información redundante a datos existentes
- El ejemplo parte de una forma de guardar
user,countryycountry_codeen una sola tabla- El mismo valor de
country_codese repite para cadacountry, lo que genera duplicación - Si se trata de datos que cambian con frecuencia, hay que actualizar todas las ubicaciones al mismo tiempo, lo que aumenta los errores y la ineficiencia
- El mismo valor de
- En la forma normalizada, la relación entre
countryycountry_codese separa en otra tabla, y la tabla de usuarios solo referenciacountry_id - Si se hace
INNER JOINentreusersycountriesporcountry_id, se recupera otra vez la forma original deuser,countryycountry_code - A partir de aquí, la explicación asume joins implícitos basados en columnas con el mismo nombre, sin apegarse estrictamente a la sintaxis detallada de SQL
Perspectiva de implementación: joins recorriendo filas y columnas
- Dado dos conjuntos
R,Sy un predicadop, el join recorre todos losr ∈ Rys ∈ S, y luego emite solo los casos en quep(r, s)es verdadero- Si el producto cartesiano de dos colecciones representa todas las conexiones posibles entre filas, el join es el subconjunto que satisface la condición
- Viéndolo desde las columnas, el dominio de cada columna se toma como el conjunto de valores posibles, y se recorren combinaciones de valores de columnas
- Si existen
R(a, b)yS(b, c), se recorren los dominios dea,byc - Solo se emite
[a, b, c]cuando(a, b)está enRy(b, c)está enS
- Si existen
El join como realidades alternativas compatibles
- El ejemplo de John y Sally explica el join como una forma de conservar solo las realidades compatibles cuando cada uno posee solo parte de la información
- John conoce las posibles combinaciones entre su mascota y animales callejeros, y Sally también conoce las posibles combinaciones entre su mascota y animales callejeros
- Si John tiene un dog y el animal callejero también es un dog, y Sally tiene un cat mientras el animal callejero es un mouse, eso no puede ser cierto al mismo tiempo
- Esto se debe a que ambos tendrían que estar observando al mismo animal callejero
- Si se hace join entre las dos tablas usando
stray, solo permanecen las combinaciones de mascota de John, animal callejero y mascota de Sally que no se contradicen entre sí
El join en modelos de programación
flatMapes una función que crea un nuevo arreglo para cada elemento del arreglo original y luego concatena los resultados, y puede usarse para implementar un joinSELECT * FROM r INNER JOIN s ON pse expresa comor.flatMap(x => s.filter(y => p(x, y)))- La sintaxis
LATERALde algunas variantes de SQL transforma el join en una forma deflatMap
- Si el lado derecho de
LATERALno hace referencia a columnas del lado izquierdo, es equivalente a un producto cartesiano- La decorrelación de consultas depende de eliminar esas referencias a columnas del lado derecho mediante reescrituras sucesivas
- El problema N+1, común en ORM, también puede explicarse con joins
- Si se ejecuta una consulta adicional por cada fila del conjunto de resultados, en bases de datos que usan conexiones como Postgres el costo fijo de cada consulta individual es alto
- El resultado de pedirle a la base de datos “haz todas estas consultas” es un join como
users INNER JOIN countries - En bases de datos in-process como Sqlite, este problema es menor
Caminos en grafos y modelos lógicos
- Como una relación “relaciona” dos conjuntos, puede verse como un grafo
- La tabla
usersconecta el conjunto de nombres de usuario con el conjunto decountry_id - La relación que conecta
country_idcon códigos de país de dos letras también puede representarse como otro grafo
- La tabla
- Si el conjunto de la derecha del primer grafo y el conjunto de la izquierda del segundo comparten el mismo conjunto de vértices, pueden verse como uno solo
- Si se enumeran todos los caminos que van desde el conjunto izquierdo, pasan por el vértice del medio y llegan al conjunto derecho, se obtiene el join de las dos relaciones
- En lógica formal, una relación se ve como un predicado, y un modelo es el conjunto de hechos que hace verdaderas a las oraciones
- Si
users(A, B)ycountries(B, C, D)son verdaderos, entonces se establece la implicación de queQ(A, B, C, D)es verdadero - Puede haber múltiples modelos que satisfagan esta condición
- Para obtener un resultado estándar, se elige el modelo mínimo entre los modelos que satisfacen la condición
- Ese modelo mínimo es igual al resultado del join entre
usersycountry
- Si
El join visto como verificación de tipos
- Los sistemas de tipos estilo ML se parecen mucho a Prolog y Datalog, por lo que pueden expresarse de manera similar a un join
- En el ejemplo con Rust, las relaciones se definen como traits
UsersyCountryCodecumplen el papel de relacionesSmudge,Sissel,Petee,Canada,UnitedStates,CAyUSse definen como tipos concretos
- Implementaciones de traits como
(Smudge, Canada): Usersy(Canada, CA): CountryCodecorresponden a filas de la relación - Para que
(A, B, C)esté incluido en el join, deben cumplirse(A, B): Usersy(B, C): CountryCode test::<(Smudge, _, CA)>()pasa la verificación de tipos, perotest::<(Smudge, _, US)>()falla porque(Canada, US): CountryCodeno está implementado
El join como operación del mónada Set
- El ejemplo de
SomeyNoneen JavaScript comienza uniendo records opcionales- Si dos records tienen el mismo
country, se fusionan y devuelvenSome - Si no son compatibles o no hay valor, devuelven
None
- Si dos records tienen el mismo
andThenextrae el valor dentro del opcional y aplica la función de combinación- Si se mantiene la misma función
combinepero se cambia el contenedor aRel, se puede trabajar con conjuntos relacionalesRel.mapaplica una función a todas las filasRel.andThenconcatena conflatMaplas relaciones producidas desde cada fila
- Si se ejecuta la misma función
combinesobre la relaciónusersy la relacióncountries, se obtiene el resultado del join con códigos de país añadidos aSmudge,SisselyPetee
La relación admisible más grande y el join en órdenes parciales
- Si una tercera relación
Tque contiene todas las columnas de dos relacionesRySno inventa nueva información, se define como admisible- Si cualquier fila de
Tse restringe a las columnas deR, esa fila debe existir enR - Del mismo modo, si se restringe a las columnas de
S, también debe existir enS
- Si cualquier fila de
- Por ejemplo,
Smudge, Canada, USno es admisible- Si se mira solo
countryycountry_code, se obtieneCanada, US, pero esa fila no existe enS
- Si se mira solo
- Incluso la relación vacía es admisible, pero la relación admisible más grande incluye
Smudge-Canada-CA,Sissel-Canada-CAyPetee-United States-US - Esa relación admisible más grande es el join de las dos relaciones
- Desde la perspectiva de orden parcial,
R ≤ Qse define asíQcontiene todas las columnas deR- Si cada fila de
Qse restringe a las columnas deR, se obtiene una fila deR
- En este orden parcial existe la mínima cota superior
R ∨ Sde las relacionesRyS, y esa es la misma idea de join relacional
El join como producto de anillo
- Las relaciones también pueden expresarse algebraicamente
- Una fila puede representarse como un producto de pares columna-valor
- Una relación puede representarse como una suma de varias filas
- Por ejemplo, el término producto de
user = Smudgeycountry_id = 1representa una fila - Se agregan reglas para simplificar la expresión
- Idempotence:
[x = y][x = y] = [x = y] - Contradiction:
[x = y][x = z] = 0ify ≠ z
- Idempotence:
- Si se multiplican la relación de usuarios
Ry la relación lookup de paísesS, y se desarrolla usando la distributividad y la conmutatividad, los términos contradictorios desaparecen y solo quedan los compatibles - La expresión restante es
Smudge-1-Canada-CA,Sissel-1-Canada-CA,Petee-2-United States-US, que es exactamente el join de las dos relaciones - Esta forma también puede verse como una contracción de tensores
1 comentarios
Comentarios de Hacker News
Empecé a pensar los joins en términos de dimensiones espaciales y se volvió mucho más fácil de entender
Si cada dimensión se pone en una tabla separada, como
Dim_X,Dim_Y,Dim_Z, y se unen con el mismoEntityId, puede verse como si construyeran la posición tridimensional de una entidadPara crear 3 dimensiones se necesitan al menos 2 inner joins, y también puede extenderse del mismo modo a dimensiones no espaciales como el tiempo
Si no limitas un momento específico, termina siendo un reporte con todas las ubicaciones que una entidad tuvo a lo largo del tiempo
Los otros tipos de join también se vuelven más fáciles de entender con esta variante una vez que fijas el concepto al punto de poder “rotar” mentalmente el esquema
https://dbdb.io/db/hyperdex
EntityPosition(EntityId, X, Y, Z)Aun así, la parte de ensamblar piezas de varias dimensiones y manejar agregaciones me hace pensar en el mundo del data warehouse
JOIN,INNER JOIN. Me parece mucho más claro listar las tablas enFROMy escribir la condición de join en la cláusulaWHEREcomo una ecuaciónCuando se mezclan varios
JOINen una cláusulaFROMcompleja, se vuelve difícil de leer, y leer las condiciones equivalentes enWHEREse siente más intuitivoLa perspectiva 0 es que un join es un operador del álgebra relacional
https://en.m.wikipedia.org/wiki/Relational_algebra
El natural join
R ⋈ Ses el conjunto de combinaciones de tuplas cuyos nombres de atributos compartidos coinciden, y es una operación relacional que corresponde alANDlógico⋈puede verse como un producto cartesiano que filtra con un predicado las filas que no deben entrar en el resultado, y gran parte de SQL se entiende bien desde esa perspectivaLlevo varios días buscando material sobre la implementación de ejecución/planificación de consultas, pero no aparecen fácilmente recursos del lado de implementación sobre predicados, índices existentes o joins
Los resultados de Google están contaminados con material de uso básico
Hasta ahora solo encontré material del CMU Database Group, y es excelente
https://pi3.informatik.uni-mannheim.de/~moer/querycompiler.p...
También podría ayudar el curso de TUM “Database Systems on Modern CPU Architectures”, y el material de 2020 incluye las clases completas en video
https://db.in.tum.de/teaching/ss20/moderndbs/?lang=en
https://www.sqlite.org/optoverview.html
https://www.sqlite.org/queryplanner.html
https://www.postgresql.org/docs/current/planner-optimizer.ht...
La ejecución de consultas y la planificación de consultas son casi cosas separadas
En cuanto a artículos sobre optimización de joins, sigo pensando que el artículo original de Selinger es el mejor
https://courses.cs.duke.edu/compsci516/cps216/spring03/paper...
No soporta outer joins y desde entonces aparecieron técnicas más eficientes, pero para quien vea optimizadores de la familia System R sigue siendo una lectura familiar
El
src/backend/optimizer/READMEde Postgres también tiene mucho contenido que es difícil encontrar en otros ladosLas clases de Andy Pavlo en CMU son casi el único material en línea que explica esto, y el PDF de “Building Query Compilers”, aunque incompleto, reúne artículos clave de Moerkotte y otros, así que vale la pena si quieres implementar algo moderno
Encontrar índices aplicables normalmente no es tan difícil si revisas si hay un sargable predicate, pero estimar selectividad es difícil, y estimar selectividad a través de joins está muy cerca de ser el problema más difícil dentro de un optimizador
Por ejemplo, si tienes
A=x AND B=y AND C=zy solo cuentas con información de selectividad/cardinalidad para los índices(A,B)y(B,C), no es nada trivial estimar la selectividad conjunta de las tres condicionesIncluso hay artículos que para resolver esto requieren un solucionador de “programación cónica de segundo orden”
query planning, y aun a simple vista aparecieron más resultados orientados a implementaciónLa 14.ª forma es el multiway join, también llamado “worst-case optimal join”, aunque el nombre no suena muy bien
En vez de ir uniendo tablas de dos en dos y seguir generando resultados intermedios, significa unir 3 o más tablas a la vez sin resultados intermedios
Hay una entrada de blog relacionada y un video corto en https://relational.ai/blog/dovetail-join, y el artículo original está en https://dl.acm.org/doi/pdf/10.1145/3180143
Trabajo en RelationalAI, y nosotros junto con algunas otras startups de bases de datos estamos llevando al mercado este nuevo algoritmo de join que se ha investigado por unos 10 años en la academia
https://justinjaffray.com/a-gentle-ish-introduction-to-worst...
ANDdel join se vuelveNOR, y Tetris aprovecha esoEl límite en el peor caso no se vuelve más estricto que en un WCOJ sin estado/streaming, pero los datos reales suelen tener certificados box mucho más pequeños
No he visto si dovetail join soporta consultas recursivas, o sea, datalog arbitrario donde solo se especifica la relación de salida y el motor resuelve por su cuenta las relaciones intermedias
Me da curiosidad si soporta ese tipo de consultas
Debería haber más textos como este, que muestran las sutilezas del modelo relacional sobre todo a desarrolladores a nivel de aplicación
La explicación y exploración desde la perspectiva de programación funcional también es concisa y convincente
Parece que se perdió otra oportunidad de enseñar el problema N+1
Hacer join contra un índice no clusterizado sigue siendo N+1; simplemente es N+1 en disco en vez de N+1 yendo y viniendo entre red y disco
Un inner join es un producto cartesiano con una condición
Un inner join con condición de igualdad genera la condición directamente, mientras que una condición de join por desigualdad sí requiere evaluación real
Buena explicación. Eso de que “la forma correcta es normalizar las tablas” es cierto en bases de datos transaccionales, pero en data warehouses cierto grado de desnormalización es ampliamente aceptado
El ejemplo de normalización me recordó cuando antes diseñaba tablas pensando que las claves primarias numéricas eran más rápidas que las cadenas
Eso terminaba creando
idsin significado, y hacía falta un join para obtener el valor único que realmente queríasUn día me di cuenta de que si usabas la misma clave única en dos tablas podías reducir joins, y aunque era algo simple, resultó muy efectivo
idúnico por tabla. Ayuda con el logging y evita tener que preocuparse por la clave “real” compuesta por varios camposEn cambio, pongo índices únicos sobre los valores de cadena y, más importante todavía, aplico ahí las restricciones de integridad
Una tabla llena de cadenas con significado es mucho más fácil de leer que una llena de
idnuméricos o UUID