Visión general
Se añade soporte ANSI JOIN en N1QL a la versión 5.5 de Couchbase. En versiones anteriores de Couchbase, el soporte de join estaba limitado a lookup join e index join, que funcionan muy bien cuando la clave de documento de un lado del join puede ser producida por el otro lado del join - es decir, join en una relación padre-hijo o hijo-padre a través de una clave de documento.
Este método se queda corto cuando la unión se realiza sobre campos arbitrarios o expresiones de campos, o cuando se requieren múltiples condiciones de unión. ANSI JOIN es una sintaxis de unión estandarizada ampliamente utilizada en bases de datos relacionales. ANSI JOIN es mucho más flexible que lookup join e index join, ya que permite realizar join sobre expresiones arbitrarias en cualquier campo de un documento. Esto hace que las operaciones de unión sean mucho más sencillas y potentes.
Sintaxis ANSI JOIN:
lhs-expression [ join-type ] JOIN rhs-keyspace ON [ join-condition ]
El lado izquierdo de la unión, expresión lhs, puede ser un espacio de claves, una expresión N1QL, una subconsulta o una unión anterior. El lado derecho de la unión, espacio-clave-rhs, debe ser un espacio-clave. La cláusula ON especifica la condición de la unión, que puede ser cualquier expresión arbitraria, aunque debe contener predicados que permitan una búsqueda de índices en el espacio clave del lado derecho. El tipo de unión puede ser INNER, LEFT OUTER, RIGHT OUTER. Las palabras clave INNER y OUTER son opcionales, por lo que JOIN es lo mismo que INNER JOIN, y LEFT JOIN es lo mismo que LEFT OUTER JOIN. En bases de datos relacionales join-type también puede ser FULL OUTER o CROSS, aunque FULL OUTER JOIN y CROSS JOIN no son soportados actualmente en N1QL.
Detalles de la compatibilidad con ANSI JOIN
Usaremos ejemplos para mostrarle nuevas formas de ejecutar consultas usando la sintaxis ANSI JOIN, y cómo transformar sus consultas join existentes en N1QL de la sintaxis lookup join o index join a la nueva sintaxis ANSI JOIN. Cabe señalar que lookup join e index join continuarán siendo soportados en N1QL por compatibilidad con versiones anteriores, sin embargo no se puede mezclar lookup join o index join con la nueva sintaxis ANSI JOIN en el mismo bloque de consulta, por lo que se recomienda a los clientes migrar a la nueva sintaxis ANSI JOIN.
Para seguirlo, instalar la muestra de viaje cubo de muestra.
Ejemplo 1: ANSI JOIN con una condición de unión arbitraria
La condición de unión (cláusula ON) para ANSI JOIN puede ser cualquier expresión que incluya cualquier campo de los documentos que se van a unir. Por ejemplo:
Índice requerido:
|
1 |
CREATE INDEX route_airports ON `travel-sample`(sourceairport, destinationairport) WHERE type = "route"; |
Índice opcional:
|
1 |
CREATE INDEX airport_city_country ON `travel-sample`(city, country) WHERE type = "airport"; |
Consulta:
|
1 2 3 4 5 6 7 |
SELECT DISTINCT route.destinationairport FROM `travel-sample` airport JOIN `travel-sample` route ON airport.faa = route.sourceairport AND route.type = "route" WHERE airport.type = "airport" AND airport.city = "San Francisco" AND airport.country = "United States"; |
En esta consulta estamos uniendo un campo ("faa") del documento del aeropuerto con un campo ("sourceairport") del documento de la ruta (véase la cláusula ON de la unión). Este tipo de unión no es posible con lookup join o index join en N1QL, ya que ambos requieren la unión en la clave del documento solamente.
ANSI JOIN requiere un índice apropiado en el espacio clave del lado derecho ("Índice obligatorio" más arriba). También puede crear otros índices (por ejemplo, el "Índice opcional" anterior) para acelerar la consulta. Sin el índice opcional se utilizará un escaneo primario y la consulta seguirá funcionando, sin embargo sin el índice requerido la consulta no funcionará y devolverá un error.
Mirando las explicaciones:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
"plan": { "#operator": "Sequence", "~children": [ { "#operator": "IndexScan3", "as": "airport", "index": "airport_city_country", "index_id": "8e782fd1b124eec3", "index_projection": { "primary_key": true }, "keyspace": "travel-sample", "namespace": "default", "spans": [ { "exact": true, "range": [ { "high": "\"San Francisco\"", "inclusion": 3, "low": "\"San Francisco\"" }, { "high": "\"United States\"", "inclusion": 3, "low": "\"United States\"" } ] } ], "using": "gsi" }, { "#operator": "Fetch", "as": "airport", "keyspace": "travel-sample", "namespace": "default" }, { "#operator": "Parallel", "~child": { "#operator": "Sequence", "~children": [ { "#operator": "NestedLoopJoin", "alias": "route", "on_clause": "(((`airport`.`faa`) = cover ((`route`.`sourceairport`))) and (cover ((`route`.`type`)) = \"route\"))", "~child": { "#operator": "Sequence", "~children": [ { "#operator": "IndexScan3", "as": "route", "covers": [ "cover ((`route`.`sourceairport`))", "cover ((`route`.`destinationairport`))", "cover ((meta(`route`).`id`))" ], "filter_covers": { "cover ((`route`.`type`))": "route" }, "index": "route_airports", "index_id": "f1f4b9fbe85e45fd", "keyspace": "travel-sample", "namespace": "default", "nested_loop": true, "spans": [ { "exact": true, "range": [ { "high": "(`airport`.`faa`)", "inclusion": 3, "low": "(`airport`.`faa`)" } ] } ], "using": "gsi" } ] } }, { "#operator": "Filter", "condition": "((((`airport`.`type`) = \"airport\") and ((`airport`.`city`) = \"San Francisco\")) and ((`airport`.`country`) = \"United States\"))" }, { "#operator": "InitialProject", "distinct": true, "result_terms": [ { "expr": "cover ((`route`.`destinationairport`))" } ] }, { "#operator": "Distinct" }, { "#operator": "FinalProject" } ] } }, { "#operator": "Distinct" } ] } |
Verá que se utiliza un operador NestedLoopJoin para realizar la unión, y debajo se utiliza un operador IndexScan3 para acceder al espacio clave de la derecha, "route". Los spans para el escaneo del índice se ven así:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
"spans": [ { "exact": true, "range": [ { "high": "(`airport`.`faa`)", "inclusion": 3, "low": "(`airport`.`faa`)" } ] } ] |
La exploración de índices para el espacio clave del lado derecho ("ruta") utiliza un campo ("faa") del espacio clave del lado izquierdo ("aeropuerto") como clave de búsqueda. Para cada documento del espacio de claves del lado exterior "aeropuerto", el operador NestedLoopJoin realiza una exploración de índice en el espacio de claves del lado interior "ruta" para encontrar documentos coincidentes, y produce resultados de unión. La unión se realiza en forma de bucle anidado, en el que el bucle externo produce el documento del espacio de claves del lado externo y un bucle interno anidado busca el documento del lado interno que coincida con el documento actual del lado externo.
La información explicada también puede visualizarse gráficamente en el Query Workbench, haciendo clic en el botón Explicar seguido del botón Plan:
En este ejemplo, el escaneo de índice en el espacio clave de la derecha es un escaneo de índice cubierto. En caso de que el escaneo de índice no esté cubierto, un operador fetch seguirá al operador de escaneo de índice para obtener el documento.
Debe tenerse en cuenta que la unión de bucles anidados requiere un índice secundario apropiado en el espacio de claves del lado derecho de ANSI JOIN. El índice primario no se tiene en cuenta a estos efectos. Si no se puede encontrar un secundario apropiado, se devolverá un error para la consulta.
Además, habrá observado que el filtro route.type = "ruta" también aparece en la cláusula ON. La cláusula ON se diferencia de la cláusula WHERE en que la cláusula ON se evalúa como parte de la unión, mientras que la cláusula WHERE se evalúa una vez realizadas todas las uniones. Esta distinción es importante, especialmente para las uniones externas. Por lo tanto, se recomienda incluir filtros en el espacio de claves de la derecha para una unión en la cláusula ON también, además de cualquier filtro de unión.
Ejemplo 2: ANSI JOIN con múltiples condiciones de unión
Mientras que lookup join e index join sólo unen en una única condición de unión (igualdad de la clave del documento), la cláusula ON de ANSI JOIN puede contener múltiples condiciones de unión.
Índice requerido:
|
1 |
CREATE INDEX landmark_city_country ON `travel-sample`(city, country) WHERE type = "landmark"; |
Índice opcional:
|
1 |
CREATE INDEX hotel_title ON `travel-sample`(title) WHERE type = "hotel"; |
Consulta:
|
1 2 3 4 |
SELECT hotel.name hotel_name, landmark.name landmark_name, landmark.activity FROM `travel-sample` hotel JOIN `travel-sample` landmark ON hotel.city = landmark.city AND hotel.country = landmark.country AND landmark.type = "landmark" WHERE hotel.type = "hotel" AND hotel.title like "Yosemite%" AND array_length(hotel.public_likes) > 5; |
Si nos fijamos en la explicación, los intervalos de índice para el índice ("landmark_city_country") del espacio clave de la derecha ("landmark") son:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
"spans": [ { "exact": true, "range": [ { "high": "(`hotel`.`city`)", "inclusion": 3, "low": "(`hotel`.`city`)" }, { "high": "(`hotel`.`country`)", "inclusion": 3, "low": "(`hotel`.`country`)" } ] } ] |
Por lo tanto, múltiples predicados de unión pueden generar potencialmente múltiples claves de búsqueda de índice para la exploración de índice del lado interno de una unión de bucle anidado.
Ejemplo 3: ANSI JOIN con expresiones join complejas
La condición de unión en la cláusula ON puede ser una expresión de unión compleja. Por ejemplo, el campo "airlineid" del documento "route" corresponde a la clave del documento "airline", pero también puede construirse concatenando "airline_" con el campo "id" del documento "airline".
Índice requerido:
|
1 |
CREATE INDEX route_airlineid ON `travel-sample`(airlineid) WHERE type = "route"; |
Índice opcional:
|
1 |
CREATE INDEX airline_name ON `travel-sample`(name) WHERE type = "airline"; |
Consulta:
|
1 2 3 4 |
SELECT count(*) FROM `travel-sample` airline JOIN `travel-sample` route ON route.airlineid = "airline_" || tostring(airline.id) AND route.type = "route" WHERE airline.type = "airline" AND airline.name = "United Airlines"; |
La explicación contiene los siguientes espacios de índice para el espacio clave de la derecha("ruta"):
|
1 2 3 4 5 6 7 8 9 10 11 12 |
"spans": [ { "exact": true, "range": [ { "high": "(\"airline_\" || to_string((`airline`.`id`)))", "inclusion": 3, "low": "(\"airline_\" || to_string((`airline`.`id`)))" } ] } ] |
La expresión se evaluará en tiempo de ejecución para generar las claves de búsqueda para la exploración de índices en el lado interno de la unión de bucles anidados.
Ejemplo 4: ANSI JOIN con cláusula IN
No es necesario que la condición de unión sea un predicado de igualdad. Se puede utilizar una cláusula IN como condición de unión.
Índice requerido:
|
1 |
CREATE INDEX airport_faa_name ON `travel-sample`(faa, airportname) WHERE type = "airport"; |
Índice opcional:
|
1 |
CREATE INDEX route_airline_distance ON `travel-sample`(airline, distance) WHERE type = "route"; |
Consulta:
|
1 2 3 4 |
SELECT DISTINCT airport.airportname FROM `travel-sample` route JOIN `travel-sample` airport ON airport.faa IN [ route.sourceairport, route.destinationairport ] AND airport.type = "airport" WHERE route.type = "route" AND route.airline = "F9" AND route.distance > 3000; |
La explicación contiene los siguientes índices para el espacio clave de la derecha ("aeropuerto"):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
"spans": [ { "range": [ { "high": "(`route`.`sourceairport`)", "inclusion": 3, "low": "(`route`.`sourceairport`)" } ] }, { "range": [ { "high": "(`route`.`destinationairport`)", "inclusion": 3, "low": "(`route`.`destinationairport`)" } ] } ] |
Ejemplo 5: ANSI JOIN con cláusula OR
De forma similar a la cláusula IN, la condición de unión para un ANSI JOIN también puede contener una cláusula OR. Las diferentes ramas de la cláusula OR pueden hacer referencia a diferentes campos del espacio de claves del lado derecho, siempre que existan los índices adecuados.
Índice requerido (índice route_airports igual que en el ejemplo 1):
|
1 2 |
CREATE INDEX route_airports ON `travel-sample`(sourceairport, destinationairport) WHERE type = "route"; CREATE INDEX route_airports2 ON `travel-sample`(destinationairport, sourceairport) WHERE type = "route"; |
Índice opcional (igual que en el ejemplo 1):
|
1 |
CREATE INDEX airport_city_country ON `travel-sample`(city, country) WHERE type = "airport"; |
Consulta:
|
1 2 3 4 |
SELECT count(*) FROM `travel-sample` airport JOIN `travel-sample` route ON (route.sourceairport = airport.faa OR route.destinationairport = airport.faa) AND route.type = "route" WHERE airport.type = "airport" AND airport.city = "Denver" AND airport.country = "United States"; |
La explicación muestra un UnionScan que se utiliza bajo NestedLoopJoin, para manejar la cláusula OR:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
"#operator": "UnionScan", "scans": [ { "#operator": "IndexScan3", "as": "route", "index": "route_airports", "index_id": "f1f4b9fbe85e45fd", "index_projection": { "primary_key": true }, "keyspace": "travel-sample", "namespace": "default", "nested_loop": true, "spans": [ { "exact": true, "range": [ { "high": "(`airport`.`faa`)", "inclusion": 3, "low": "(`airport`.`faa`)" } ] } ], "using": "gsi" }, { "#operator": "IndexScan3", "as": "route", "index": "route_airports2", "index_id": "cdc9dca18c973bd3", "index_projection": { "primary_key": true }, "keyspace": "travel-sample", "namespace": "default", "nested_loop": true, "spans": [ { "exact": true, "range": [ { "high": "(`airport`.`faa`)", "inclusion": 3, "low": "(`airport`.`faa`)" } ] } ], "using": "gsi" } ] |
Ejemplo 6: ANSI JOIN con sugerencias
En las uniones por búsqueda y por índice, las sugerencias sólo pueden especificarse en el espacio de claves del lado izquierdo de la unión. Para ANSI JOIN, también se pueden especificar sugerencias en el espacio clave del lado derecho. Utilizando la misma consulta que en el ejemplo 1 (añadiendo la sugerencia USE INDEX):
|
1 2 3 4 |
SELECT DISTINCT route.destinationairport FROM `travel-sample` airport JOIN `travel-sample` route USE INDEX(route_airports) ON airport.faa = route.sourceairport AND route.type = "route" WHERE airport.type = "airport" AND airport.city = "San Francisco" AND airport.country = "United States"; |
La sugerencia USE INDEX limita el número de índices que el planificador debe tener en cuenta para realizar la unión.
También se pueden especificar sugerencias en el espacio de claves de la izquierda de ANSI JOIN.
|
1 2 3 4 5 |
SELECT DISTINCT route.destinationairport FROM `travel-sample` airport USE INDEX(airport_city_country) JOIN `travel-sample` route USE INDEX(route_airports) ON airport.faa = route.sourceairport AND route.type = "route" WHERE airport.type = "airport" AND airport.city = "San Francisco" AND airport.country = "United States"; |
Ejemplo 7: ANSI LEFT OUTER JOIN
Hasta ahora hemos visto las uniones internas. También puede realizar un LEFT OUTER JOIN simplemente incluyendo las palabras clave LEFT o LEFT OUTER delante de la palabra clave JOIN en la especificación de la unión.
Índice requerido (igual que en el ejemplo 1):
|
1 |
CREATE INDEX route_airports ON `travel-sample`(sourceairport, destinationairport) WHERE type = "route"; |
Índice opcional (igual que en el ejemplo 1):
|
1 |
CREATE INDEX airport_city_country ON `travel-sample`(city, country) WHERE type = "airport"; |
Consulta:
|
1 2 3 4 |
SELECT airport.airportname, route.airlineid FROM `travel-sample` airport LEFT JOIN `travel-sample` route ON airport.faa = route.sourceairport AND route.type = "route" WHERE airport.type = "airport" AND airport.city = "Denver" AND airport.country = "United States"; |
El conjunto de resultados de esta consulta contiene todos los resultados unidos, así como cualquier documento del lado izquierdo ("aeropuerto") que no se una con el documento del lado derecho ("ruta"), de acuerdo con la semántica de LEFT OUTER JOIN. Así, encontrará resultados que sólo contengan airport.airportname pero no route.airlineid (que falta). También puede seleccionar sólo el documento del lado izquierdo ("airport") que no se une con el documento del lado derecho ("route") añadiendo un predicado IS MISSING en el espacio clave del lado derecho ("route"):
|
1 2 3 4 5 |
SELECT airport.airportname, route.airlineid FROM `travel-sample` airport LEFT JOIN `travel-sample` route ON airport.faa = route.sourceairport AND route.type = "route" WHERE airport.type = "airport" AND airport.city = "Denver" AND airport.country = "United States" AND route.airlineid IS MISSING; |
Ejemplo 8: ANSI RIGHT OUTER JOIN
ANSI RIGHT OUTER JOIN es similar a ANSI LEFT OUTER JOIN, salvo que conservamos el documento del lado derecho si no se produce ninguna unión. Podemos modificar la consulta del ejemplo 7 cambiando los espacios de claves izquierdo y derecho y sustituyendo la palabra clave LEFT por RIGHT:
|
1 2 3 4 |
SELECT airport.airportname, route.airlineid FROM `travel-sample` route RIGHT JOIN `travel-sample` airport ON airport.faa = route.sourceairport AND route.type = "route" WHERE airport.type = "airport" AND airport.city = "Denver" AND airport.country = "United States"; |
Observe que, aunque hemos cambiado el aeropuerto y la ruta en la especificación del join, el filtro de la ruta (ahora el espacio clave del lado izquierdo) sigue apareciendo en la cláusula ON del join, ya que la ruta sigue estando en el lado subordinado en este join externo.
RIGHT OUTER JOIN se convierte internamente en LEFT OUTER JOIN.
Si una consulta contiene múltiples uniones, una RIGHT OUTER JOIN sólo puede ser la primera unión especificada. Dado que N1QL sólo soporta uniones lineales, es decir, el lado derecho de cada unión debe ser un único espacio clave, si un RIGHT OUTER JOIN no es la primera unión especificada, entonces después de convertir a LEFT OUTER JOIN, el lado derecho de la unión ahora contiene múltiples espacios clave, lo cual no es soportado. Si especifica RIGHT OUTER JOIN en cualquier posición que no sea la primera unión, se devolverá un error de sintaxis.
Ejemplo 9: ANSI JOIN utilizando Hash Join
N1QL soporta dos métodos de unión para ANSI JOIN. El método de unión por defecto para un ANSI JOIN es nested-loop join. La alternativa es hash join. La unión hash utiliza una tabla hash para emparejar documentos de ambos lados de la unión. Hash join tiene un lado de construcción y un lado de sondeo, donde cada documento del lado de construcción se inserta en una tabla hash basada en los valores de la expresión equi-join del lado de construcción; posteriormente, cada documento del lado de sondeo se busca en la tabla hash basada en los valores de la expresión equi-join del lado de sondeo. Si se encuentra una coincidencia, se realiza la operación de unión.
En comparación con la unión de bucles anidados, la unión hash puede ser más eficiente cuando la unión es grande, por ejemplo, cuando hay decenas de miles de documentos o más en el lado izquierdo de la unión (después de aplicar filtros). Si se utiliza la unión de bucle anidado, por cada documento del lado izquierdo se debe realizar un escaneo de índice en el índice del lado derecho. A medida que aumenta el número de documentos del lado izquierdo, la unión en bucle anidado pierde eficacia.
En el caso de la unión por hash, el lado más pequeño de la unión debe utilizarse para construir la tabla hash, y el lado más grande de la unión debe utilizarse para sondear la tabla hash. Hay que tener en cuenta que el hash join requiere más memoria que el nested-loop join, ya que se necesita una tabla hash en memoria. La cantidad de memoria necesaria es proporcional al número de documentos del lado de la construcción, así como al tamaño medio de cada documento.
Hash join es compatible con sólo edición enterprise. Para utilizar la unión hash, debe especificarse una sugerencia USE HASH en el espacio de claves del lado derecho de ANSI JOIN. Utilizando la misma consulta que en el ejemplo 1:
|
1 2 3 4 |
SELECT DISTINCT route.destinationairport FROM `travel-sample` airport JOIN `travel-sample` route USE HASH(build) ON airport.faa = route.sourceairport AND route.type = "route" WHERE airport.type = "airport" AND airport.city = "San Jose" AND airport.country = "United States"; |
La sugerencia USE HASH(build) indica al planificador N1QL que realice una unión hash para el ANSI JOIN especificado, y el espacio de claves del lado derecho ("route") se utiliza en el lado de construcción de la unión hash. Mirando la explicación, hay un operador HashJoin:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
{ "#operator": "HashJoin", "build_aliases": [ "route" ], "build_exprs": [ "cover ((`route`.`sourceairport`))" ], "on_clause": "(((`airport`.`faa`) = cover ((`route`.`sourceairport`))) and (cover ((`route`.`type`)) = \"route\"))", "probe_exprs": [ "(`airport`.`faa`)" ], "~child": { "#operator": "Sequence", "~children": [ { "#operator": "IndexScan3", "as": "route", "covers": [ "cover ((`route`.`sourceairport`))", "cover ((`route`.`destinationairport`))", "cover ((meta(`route`).`id`))" ], "filter_covers": { "cover ((`route`.`type`))": "route" }, "index": "route_airports", "index_id": "f1f4b9fbe85e45fd", "keyspace": "travel-sample", "namespace": "default", "spans": [ { "range": [ { "inclusion": 0, "low": "null" } ] } ], "using": "gsi" } ] } } |
El operador hijo ("~hijo") de un operador HashJoin es siempre el lado de construcción de la unión hash. Para esta consulta, se trata de una exploración de índice en el espacio de claves "route" del lado derecho.
Observe que para acceder al documento "ruta" ya no podemos utilizar la información del espacio de claves del lado izquierdo ("aeropuerto") como clave de búsqueda del índice (mire la información sobre "spans" en la sección de explicación anterior). A diferencia de la unión de bucles anidados, la exploración del índice en "ruta" ya no está vinculada a un documento individual del lado izquierdo y, por lo tanto, no se puede utilizar ningún valor del documento "aeropuerto" como clave de búsqueda para la exploración del índice en "ruta".
La sugerencia USE HASH(build) utilizada en la consulta anterior indica al planificador que utilice el espacio de claves del lado derecho como lado de construcción de la unión hash. También puede especificar la sugerencia USE HASH(probe) para indicar al planificador que utilice el espacio de claves del lado derecho como el lado de sondeo de la unión hash.
|
1 2 3 4 |
SELECT DISTINCT route.destinationairport FROM `travel-sample` airport JOIN `travel-sample` route USE HASH(probe) ON airport.faa = route.sourceairport AND route.type = "route" WHERE airport.type = "airport" AND airport.city = "San Jose" AND airport.country = "United States"; |
Si miras la explicación, encontrarás el operador HashJoin:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
{ "#operator": "HashJoin", "build_aliases": [ "airport" ], "build_exprs": [ "(`airport`.`faa`)" ], "on_clause": "(((`airport`.`faa`) = cover ((`route`.`sourceairport`))) and (cover ((`route`.`type`)) = \"route\"))", "probe_exprs": [ "cover ((`route`.`sourceairport`))" ], "~child": { "#operator": "Sequence", "~children": [ { "#operator": "IntersectScan", "scans": [ { "#operator": "IndexScan3", "as": "airport", "index": "airport_city_country", "index_id": "8e782fd1b124eec3", "index_projection": { "primary_key": true }, "keyspace": "travel-sample", "namespace": "default", "spans": [ { "exact": true, "range": [ { "high": "\"San Jose\"", "inclusion": 3, "low": "\"San Jose\"" }, { "high": "\"United States\"", "inclusion": 3, "low": "\"United States\"" } ] } ], "using": "gsi" }, { "#operator": "IndexScan3", "as": "airport", "index": "airport_faa", "index_id": "c302afbf811470f5", "index_projection": { "primary_key": true }, "keyspace": "travel-sample", "namespace": "default", "spans": [ { "exact": true, "range": [ { "inclusion": 0, "low": "null" } ] } ], "using": "gsi" } ] }, { "#operator": "Fetch", "as": "airport", "keyspace": "travel-sample", "namespace": "default" } ] } } |
El operador hijo ("~hijo") para HashJoin es un escaneo de índice de intersección en el espacio clave del lado izquierdo del ANSI JOIN, "aeropuerto", seguido de un operador fetch.
La sugerencia USE HASH sólo se puede especificar en el espacio de claves del lado derecho en un ANSI JOIN. Por lo tanto, dependiendo de si desea que el espacio de claves del lado derecho sea el lado de construcción o el lado de sondeo de una unión hash, se debe especificar una sugerencia USE HASH(build) o USE HASH(probe) en el espacio de claves del lado derecho.
La unión hash sólo se tiene en cuenta cuando se especifica la sugerencia USE HASH(build) o USE HASH(probe). La unión hash requiere predicados de unión de igualdad para funcionar. La unión en bucle anidado requiere un índice secundario apropiado en el espacio de claves del lado derecho, mientras que la unión hash no (la exploración del índice primario es una opción para la unión hash). Sin embargo, la unión por hash requiere más memoria que la unión por bucle anidado, ya que se necesita una tabla hash en memoria para que la unión por hash funcione. Además, la unión por hash se considera una operación "bloqueante", lo que significa que el motor de consulta debe terminar de construir la tabla hash antes de poder producir el primer resultado de la unión, por lo que para las consultas que sólo necesitan los primeros resultados rápidamente (por ejemplo, con una cláusula LIMIT) la unión por hash puede no ser la mejor opción.
Si se especifica una sugerencia USE HASH, pero no se puede generar correctamente una unión hash (por ejemplo, falta de predicados de unión de igualdad), se considerará una unión de bucle anidado.
Ejemplo 10: ANSI JOIN con múltiples sugerencias
Ahora puede especificar varias sugerencias para un espacio clave en el lado derecho de un ANSI JOIN. Por ejemplo, la sugerencia USE HASH puede utilizarse junto con la sugerencia USE INDEX.
|
1 2 3 4 |
SELECT DISTINCT route.destinationairport FROM `travel-sample` airport JOIN `travel-sample` route USE HASH(probe) INDEX(route_airports) ON airport.faa = route.sourceairport AND route.type = "route" WHERE airport.type = "airport" AND airport.city = "San Jose" AND airport.country = "United States"; |
Tenga en cuenta que cuando se utilizan varias sugerencias a la vez, sólo es necesario especificar la palabra clave "USE" una vez, como en el ejemplo anterior.
La sugerencia USE HASH también puede combinarse con la sugerencia USE KEYS.
Ejemplo 11: ANSI JOIN con uniones múltiples
ANSI JOIN se pueden encadenar. Por ejemplo:
Índices necesarios (el índice route_airports es el mismo que en el ejemplo 1):
|
1 2 |
CREATE INDEX route_airports ON `travel-sample`(sourceairport, destinationairport) WHERE type = "route"; CREATE INDEX airline_iata ON `travel-sample`(iata) WHERE type = "airline"; |
Índice opcional (igual que en el ejemplo 1):
|
1 |
CREATE INDEX airport_city_country ON `travel-sample`(city, country) WHERE type = "airport"; |
Consulta:
|
1 2 3 4 5 6 7 |
SELECT DISTINCT airline.name FROM `travel-sample` airport INNER JOIN `travel-sample` route ON airport.faa = route.sourceairport AND route.type = "route" INNER JOIN `travel-sample` airline ON route.airline = airline.iata AND airline.type = "airline" WHERE airport.type = "airport" AND airport.city = "San Jose" AND airport.country = "United States"; |
Dado que no se ha especificado ninguna sugerencia USE HASH en la consulta, la explicación debería mostrar dos operadores NestedLoopJoin.
Puede mezclar uniones hash con uniones de bucles anidados añadiendo la sugerencia USE HASH a cualquiera de las uniones de una cadena de ANSI JOINs.
|
1 2 3 4 5 6 7 |
SELECT DISTINCT airline.name FROM `travel-sample` airport INNER JOIN `travel-sample` route ON airport.faa = route.sourceairport AND route.type = "route" INNER JOIN `travel-sample` airline USE HASH(build) ON route.airline = airline.iata AND airline.type = "airline" WHERE airport.type = "airport" AND airport.city = "San Jose" AND airport.country = "United States"; |
o
|
1 2 3 4 5 6 7 |
SELECT DISTINCT airline.name FROM `travel-sample` airport INNER JOIN `travel-sample` route USE HASH(probe) ON airport.faa = route.sourceairport AND route.type = "route" INNER JOIN `travel-sample` airline ON route.airline = airline.iata AND airline.type = "airline" WHERE airport.type = "airport" AND airport.city = "San Jose" AND airport.country = "United States"; |
La explicación visual de la última consulta es la siguiente:

Como ya se ha mencionado, N1QL sólo admite uniones lineales, es decir, el lado derecho de cada unión debe ser un espacio de claves.
Ejemplo 12: ANSI JOIN con matrices del lado derecho
Aunque ANSI JOIN viene del estándar SQL, como Couchbase N1QL maneja documentos JSON y los arrays son un aspecto importante de JSON, extendimos el soporte de ANSI JOIN a los arrays también.
Para ejemplos en el manejo de arreglos por favor cree un cubo "default" e inserte los siguientes documentos:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
INSERT INTO default (KEY,VALUE) VALUES("test11_ansijoin", {"c11": 1, "c12": 10, "a11": [ 1, 2, 3, 4 ], "type": "left"}), VALUES("test12_ansijoin", {"c11": 2, "c12": 20, "a11": [ 3, 3, 5, 10 ], "type": "left"}), VALUES("test13_ansijoin", {"c11": 3, "c12": 30, "a11": [ 3, 4, 20, 40 ], "type": "left"}), VALUES("test14_ansijoin", {"c11": 4, "c12": 40, "a11": [ 30, 30, 30 ], "type": "left"}); INSERT INTO default (KEY,VALUE) VALUES("test21_ansijoin", {"c21": 1, "c22": 10, "a21": [ 1, 10, 20], "a22": [ 1, 2, 3, 4 ], "type": "right"}), VALUES("test22_ansijoin", {"c21": 2, "c22": 20, "a21": [ 2, 3, 30], "a22": [ 3, 5, 10, 3 ], "type": "right"}), VALUES("test23_ansijoin", {"c21": 2, "c22": 21, "a21": [ 2, 20, 30], "a22": [ 3, 3, 5, 10 ], "type": "right"}), VALUES("test24_ansijoin", {"c21": 3, "c22": 30, "a21": [ 3, 10, 30], "a22": [ 3, 4, 20, 40 ], "type": "right"}), VALUES("test25_ansijoin", {"c21": 3, "c22": 31, "a21": [ 3, 20, 40], "a22": [ 4, 3, 40, 20 ], "type": "right"}), VALUES("test26_ansijoin", {"c21": 3, "c22": 32, "a21": [ 4, 14, 24], "a22": [ 40, 20, 4, 3 ], "type": "right"}), VALUES("test27_ansijoin", {"c21": 5, "c22": 50, "a21": [ 5, 15, 25], "a22": [ 1, 2, 3, 4 ], "type": "right"}), VALUES("test28_ansijoin", {"c21": 6, "c22": 60, "a21": [ 6, 16, 26], "a22": [ 3, 3, 5, 10 ], "type": "right"}), VALUES("test29_ansijoin", {"c21": 7, "c22": 70, "a21": [ 7, 17, 27], "a22": [ 30, 30, 30 ], "type": "right"}), VALUES("test30_ansijoin", {"c21": 8, "c22": 80, "a21": [ 8, 18, 28], "a22": [ 30, 30, 30 ], "type": "right"}); |
A continuación, cree los siguientes índices:
|
1 2 |
CREATE INDEX default_ix_left on default(c11, DISTINCT a11) WHERE type = "left"; CREATE INDEX default_ix_right on default(c21, DISTINCT a21) WHERE type = "right"; |
Cuando el predicado de unión implica una matriz en el lado derecho de ANSI JOIN, es necesario crear un índice de matriz en el espacio de claves del lado derecho.
Consulta:
|
1 2 3 4 |
SELECT b1.c11, b2.c21, b2.c22 FROM default b1 JOIN default b2 ON b2.c21 = b1.c11 AND ANY v IN b2.a21 SATISFIES v = b1.c12 END AND b2.type = "right" WHERE b1.type = "left"; |
Observe que parte de la condición de unión es una cláusula ANY que especifica que el campo del lado izquierdo b1.c12 puede coincidir con cualquier elemento de la matriz del lado derecho b2.a21. Para que esta unión funcione correctamente, necesitamos un índice de matriz en b2.a21, por ejemplo, el índice default_ix_right creado anteriormente.
El plan explain muestra un NestedLoopJoin, siendo el operador hijo un escaneo distinto en el índice del array default_ix_right.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
{ "#operator": "NestedLoopJoin", "alias": "b2", "on_clause": "((((`b2`.`c21`) = (`b1`.`c11`)) and any `v` in (`b2`.`a21`) satisfies (`v` = (`b1`.`c12`)) end) and ((`b2`.`type`) = \"right\"))", "~child": { "#operator": "Sequence", "~children": [ { "#operator": "DistinctScan", "scan": { "#operator": "IndexScan3", "as": "b2", "index": "default_ix_right", "index_id": "ef4e7fa33f33dce", "index_projection": { "primary_key": true }, "keyspace": "default", "namespace": "default", "nested_loop": true, "spans": [ { "exact": true, "range": [ { "high": "(`b1`.`c11`)", "inclusion": 3, "low": "(`b1`.`c11`)" }, { "high": "(`b1`.`c12`)", "inclusion": 3, "low": "(`b1`.`c12`)" } ] } ], "using": "gsi" } }, { "#operator": "Fetch", "as": "b2", "keyspace": "default", "namespace": "default", "nested_loop": true } ] } } |
Ejemplo 13: ANSI JOIN con matrices del lado izquierdo
Si ANSI JOIN incluye una matriz en el lado izquierdo, existen dos opciones para realizar la unión.
Opción 1: utilizar UNNEST
Utilice la cláusula UNNEST para aplanar primero la matriz de la izquierda antes de realizar la unión.
|
1 2 3 4 |
SELECT b1.c11, b2.c21, b2.c22 FROM default b1 UNNEST b1.a11 AS ba1 JOIN default b2 ON ba1 = b2.c21 AND b2.type = "right" WHERE b1.c11 = 2 AND b1.type = "left"; |
Después del UNNEST el array se convierte en campos individuales, y la unión subsiguiente es como un ANSI JOIN "normal" con campos de ambos lados.
Opción 2: utilizar la cláusula IN
Alternativamente, utilice la cláusula IN como condición de unión.
|
1 2 3 4 |
SELECT b1.c11, b2.c21, b2.c22 FROM default b1 JOIN default b2 ON b2.c21 IN b1.a11 AND b2.type = "right" WHERE b1.c11 = 2 AND b1.type = "left"; |
La cláusula IN se cumple cuando cualquier elemento de la matriz del espacio clave del lado izquierdo ("b1.a11") coincide con el campo del lado derecho ("b2.c21").
Tenga en cuenta que hay una diferencia semántica entre las dos opciones. Cuando hay duplicados en la matriz, a la opción UNNEST no le importan los duplicados y aplanará los documentos del lado izquierdo a tantos documentos como elementos haya en la matriz, por lo que puede producir resultados duplicados; la opción IN-clause no producirá resultados duplicados si hay elementos duplicados en la matriz. Además, cuando se realiza un LEFT OUTER JOIN, puede haber un número diferente de documentos conservados debido al aplanamiento de la matriz con la opción UNNEST. Por lo tanto, se aconseja al usuario que elija la opción que refleje la semántica necesaria para la consulta.
Ejemplo 14: ANSI JOIN con matrices en ambos lados
Aunque es poco común, es posible realizar un ANSI JOIN cuando ambos lados de la unión son matrices. En estos casos, puede utilizar una combinación de las técnicas descritas anteriormente. Utilice el índice de array para manejar el array del lado derecho, y utilice la opción UNNEST o la opción IN-clause para manejar el array del lado izquierdo.
Opción 1: utilizar la cláusula UNNEST
|
1 2 3 4 |
SELECT b1.c11, b2.c21, b2.c22 FROM default b1 UNNEST b1.a11 AS ba1 JOIN default b2 ON b2.c21 = b1.c11 AND ANY v IN b2.a21 SATISFIES v = ba1 END AND b2.type = "right" WHERE b1.type = "left"; |
Opción 2: utilizar la cláusula IN
|
1 2 3 4 |
SELECT b1.c11, b2.c21, b2.c22 FROM default b1 JOIN default b2 ON b2.c21 = b1.c11 AND ANY v IN b2.a21 SATISFIES v IN b1.a11 END AND b2.type = "right" WHERE b1.type = "left"; |
Una vez más, las dos opciones no son semánticamente idénticas y pueden dar resultados diferentes. Elija la opción que refleje la semántica deseada.
Ejemplo 15: migración lookup join
N1QL continuará soportando lookup join e index join por compatibilidad con versiones anteriores, sin embargo, no puede mezclar ANSI JOIN con lookup join o index join en la misma consulta. Puede convertir sus consultas existentes usando lookup join e index join a la sintaxis ANSI JOIN. Este ejemplo muestra cómo convertir una consulta lookup join a la sintaxis ANSI JOIN.
Cree el siguiente índice para acelerar la consulta (igual que en el ejemplo 1):
|
1 |
CREATE INDEX route_airports ON `travel-sample`(sourceairport, destinationairport) WHERE type = "route"; |
Se trata de una consulta que utiliza la sintaxis lookup join (observe la cláusula ON KEYS):
|
1 2 3 4 |
SELECT airline.name FROM `travel-sample` route JOIN `travel-sample` airline ON KEYS route.airlineid WHERE route.type = "route" AND route.sourceairport = "SFO" AND route.destinationairport = "JFK"; |
En lookup join, el lado izquierdo de la unión ("route") debe producir claves de documento para el lado derecho de la unión ("airline"), lo que se consigue mediante la cláusula ON KEYS. La condición de unión (implícita en la sintaxis) es route.airlineid = meta(aerolinea).idpor lo que la misma consulta puede especificarse utilizando la sintaxis ANSI JOIN:
|
1 2 3 4 |
SELECT airline.name FROM `travel-sample` route JOIN `travel-sample` airline ON route.airlineid = meta(airline).id WHERE route.type = "route" AND route.sourceairport = "SFO" AND route.destinationairport = "JFK"; |
En este ejemplo, la cláusula ON KEYS contiene una única clave de documento. Es posible que la cláusula ON KEYS contenga una matriz de claves de documento, en cuyo caso la cláusula ON convertida tendrá la forma de una cláusula IN en lugar de una cláusula de igualdad. Supongamos que cada documento de ruta tiene un array de claves de documento para aerolínea, entonces la cláusula ON KEYS original:
|
1 |
ON KEYS route.airlineids |
se puede convertir en:
|
1 |
ON meta(airline).id IN route.airlineids |
Ejemplo 16: migración de index join
Este ejemplo muestra cómo convertir una unión de índice en sintaxis ANSI JOIN.
Índice requerido (igual que en el ejemplo 3):
|
1 |
CREATE INDEX route_airlineid ON `travel-sample`(airlineid) WHERE type = "route"; |
Índice opcional (igual que en el ejemplo 3):
|
1 |
CREATE INDEX airline_name ON `travel-sample`(name) WHERE type = "airline"; |
Consulta utilizando la sintaxis de unión de índices (observe la cláusula ON KEY ... FOR ...):
|
1 2 3 4 |
SELECT count(*) FROM `travel-sample` airline JOIN `travel-sample` route ON KEY route.airlineid FOR airline WHERE airline.type = "airline" AND route.type = "route" AND airline.name = "United Airlines"; |
En la unión por índices, la clave de documento del lado izquierdo ("compañía aérea") se utiliza para sondear un índice en una expresión ("route.airlineid" que aparece en la cláusula ON KEY) del lado derecho ("route") que corresponde a la clave de documento del lado izquierdo ("compañía aérea" que aparece en la cláusula FOR). La condición de unión (implícita en la sintaxis) es route.airlineid = meta(aerolinea).idpor lo que la misma consulta puede especificarse utilizando la sintaxis ANSI JOIN:
|
1 2 3 4 |
SELECT count(*) FROM `travel-sample` airline JOIN `travel-sample` route ON route.airlineid = meta(airline).id WHERE airline.type = "airline" AND route.type = "route" AND airline.name = "United Airlines"; |
Ejemplo 17: ANSI NEST
Couchbase N1QL soporta la operación NEST. Anteriormente NEST se podía hacer usando lookup nest o index nest, similar a lookup join e index join, respectivamente. Con el soporte ANSI JOIN, la operación NEST también puede hacerse usando una sintaxis similar, es decir, usando la cláusula ON en lugar de las cláusulas ON KEYS (lookup nest) o ON KEY ... FOR ... (index nest). Esta nueva variante se denomina ANSI NEST.
Índice requerido (índice route_airports igual que en el ejemplo 1, índice route_airline_distance igual que en el ejemplo 4):
|
1 2 |
CREATE INDEX route_airports ON `travel-sample`(sourceairport, destinationairport) WHERE type = "route"; CREATE INDEX route_airline_distance ON `travel-sample`(airline, distance) WHERE type = "route"; |
Índice opcional:
|
1 |
CREATE INDEX airline_country_iata_name ON `travel-sample`(country, iata, name) WHERE type = "airline"; |
Consulta:
|
1 2 3 4 |
SELECT airline.name, ARRAY {"destination": r.destinationairport} FOR r in route END as destinations FROM `travel-sample` airline NEST `travel-sample` route ON airline.iata = route.airline AND route.type = "route" AND route.sourceairport = "SFO" WHERE airline.type = "airline" AND airline.country = "United States"; |
Como puede ver, la sintaxis de ANSI NEST es muy similar a la de ANSI JOIN. Sin embargo, hay una propiedad peculiar para nest. Por definición, la operación nest crea una matriz de todos los documentos del lado derecho coincidentes para cada documento del lado izquierdo, lo que significa que la referencia al espacio clave del lado derecho, "ruta" en esta consulta, tiene un significado diferente dependiendo de dónde se encuentre la referencia. La cláusula ON se evalúa como parte de la operación NEST, por lo que las referencias a "route" hacen referencia a un único documento. Por el contrario, las referencias en la cláusula de proyección, o la cláusula WHERE, se evalúan después de la operación NEST, y por lo tanto las referencias a "route" significan el array anidado, por lo que debe tratarse como un array. Observe que la cláusula de proyección de la consulta anterior tiene una construcción ARRAY con una cláusula FOR para acceder a cada documento individual dentro del array (es decir, la referencia a "route" está ahora en un contexto de array).
Resumen
ANSI JOIN proporciona mucha más flexibilidad en las operaciones de unión en Couchbase N1QL, en comparación con el soporte previo de lookup join e index join, los cuales requieren la unión en la clave del documento solamente. Los ejemplos anteriores muestran varias formas de usar ANSI JOIN en consultas. Ya que ANSI JOIN es ampliamente usado en el mundo relacional, el soporte para ANSI JOIN en Couchbase N1QL debería hacer mucho más fácil migrar aplicaciones de una base de datos relacional a Couchbase N1QL.
por fin :)
¡Ejemplos impresionantes!