A medida que las bases de datos NoSQL evolucionaban, cada una añadía API o lenguajes de más alto nivel para ayudar a los programadores a realizar cosas complejas con facilidad. SQL, después de haber hecho eso para los datos relacionales, mostró el camino. En SQL, los programadores dicen QUÉ hay que hacer y el motor de la base de datos resuelve el CÓMO. El CÓMO es el procedimiento/algoritmo eficiente para ejecutar la sentencia. Seleccionar, unir y proyectar son las operaciones básicas del procesamiento SQL. Incluso en los sistemas NoSQL, cuando se modelan los datos sin mucha normalización, sigue siendo necesario unir una colección de objetos. Clientes con pedidos, pedidos con inventario, inventario con proveedores, proveedores con créditos y así sucesivamente. Por lo tanto, Couchbase N1QL ha soportado operaciones de unión desde su primera versión. Después de eso, MongoDB, en la versión 3.2, añadió el operador $lookup al marco de agregación para realizar las operaciones de unión.
Sin una función de consulta expresiva y de alto rendimiento, los desarrolladores de aplicaciones tienen que hacerlo dentro de la aplicación o exportar los datos a un sistema que lo haga. Ambas propuestas son caras.
En este artículo, vamos a comparar Couchbase vs. MongoDB y sus diferentes enfoques para unir documentos JSON. Específicamente, haremos un estudio comparativo del uso de joins para colecciones de MongoDB frente a cómo podemos ejecutarlos en Couchbase. Cassandra CQL y DynamoDB no admiten uniones de forma nativa. Los desarrolladores deben hacer el trabajo ellos mismos o utilizar otras capas como Spark o Amazon EMR para lograr el mismo resultado. Por lo tanto, no las trataremos en este artículo..
Uniones en Couchbase
Couchbase introdujo las uniones INNER y LEFT OUTER a partir de Couchbase 4.0 (2015). Esto soporta uniones de una relación hijo a padre. Los documentos hijos (por ejemplo, pedidos) se pueden unir con los documentos padres (por ejemplo, clientes). En 4.5 (2016), Couchbase introdujo uniones de índices para consultar uniones de padre a hijo. En ambos casos, existía un predicado implícito de igualdad entre atributo-valor y clave de documento, especificado por la cláusula ON KEY.
Couchbase 5.5 tiene el estándar ANSI SQL extendido para JSON. Soporta INNER JOIN, LEFT OUTER JOIN y RIGHT OUTER join limitado. Usaremos ejemplos basados en Couchbase 5.5.
Couchbase se une a la documentación: https://developer.couchbase.com/documentation/server/5.5/n1ql/n1ql-language-reference/from.html
Uniones y colecciones de MongoDB:
Las uniones se realizan mediante el operador $lookup dentro del marco de agregación.
A continuación se presentan extractos de la documentación de MongoDB.
Nuevo en la versión 3.2.
Realiza una unión externa izquierda con una colección no separada en la tabla mismo para filtrar los documentos de la colección "unida" para su procesamiento. Para cada documento de entrada, el 1TP4Búsqueda añade un nuevo campo de matriz cuyos elementos son los documentos coincidentes de la colección "unida". La dirección 1TP4Búsqueda pasa estos documentos remodelados a la etapa siguiente.
Eliot Horowitz, CTO de MongoDB, dijo: "La agregación de MongoDB es similar al pipeline de Unix. La salida de una etapa va a otra....[es] muy procedimental. Te permite pensar de una manera muy procedimental".
MongoDB $lookup : https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/
¿Quiere saber más sobre las JOIN? Lea el artículo de Lukas Eder. https://dzone.com/articles/a-probably-incomplete-comprehensive-guide-to-the-many-different-ways-to-join-tables-in-sql
Comparación de alto nivel entre Couchbase N1Ql y MongoDB.
Couchbase N1QL: Soporta INNER JOIN, LEFT OUTER JOIN y RIGHT OUTER JOIN limitado. El lenguaje de consulta, como SQL, es declarativo. Los desarrolladores escriben, las herramientas generan la consulta según la sintaxis N1QL. El motor calcula el plan y ejecuta la consulta.
MongoDB: Soporta LEFT OUTER JOIN sólo para valores escalares. El diseño de las uniones en el lenguaje de consulta de MongoDB se realiza para ayudar a escribir la consulta y procesar los datos de forma procedimental.
Implicación:
- El conjunto de resultados de la unión externa izquierda es un superconjunto del conjunto de resultados de la unión interna. Es posible añadir predicados adicionales para eliminar los documentos no coincidentes (proyectados como nulos o que faltan en el lado subordinado de la unión) después de realizar la unión externa izquierda. Es como ir de San Francisco a Chicago pasando por Londres. Se puede hacer, pero es caro. Para la ejecución de la consulta se necesita tiempo, memoria y recursos de cpu, lo que afecta al rendimiento general del sistema.
- El soporte de N1QL para joins es declarativo. El lenguaje MongoDB es algo procedimental. Hay que separar los predicados, pensar en el orden de unión entre colecciones, pensar en cuándo agrupar, ordenar, etc. Escribir consultas con agregación en MongoDB es como escribir planes de consulta, paso a paso.
Ejemplos:
Utilizamos el modelo y los datos de la muestra de viaje simple. He aquí los detalles de los datos del modelo. https://developer.couchbase.com/documentation/server/4.5/travel-app/travel-app-data-model.html
Simplemente exportamos los datos desde Couchbase y los importamos a una base de datos mongo llamada travel-sample. En MongoDB, los 5 tipos diferentes del documento (punto de referencia, ruta, aerolínea, aeropuerto, hotel) se almacenan en 5 colecciones con nombres respectivos.
Ejemplo 1: LEFT OUTER JOIN con cláusula ON sobre valores escalares.
Couchbase N1QL
1 2 3 4 5 6 7 |
SELECCIONAR recuento(*) DESDE Viajar-muestra ruta LEFT OUTER JOIN Viajar-muestra aerolínea EN (route.airlineid = META(aerolínea).id) DONDE ruta.tipo = ruta; |
Consulta MongoDB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
db.ruta.agregado([ { $buscar: { de:"aerolínea", localField: "airlineid", foreignField: "_id", como: "airline_docs" } }, { $grupo: { _id: null, miCuenta: { $suma: 1 } } }, { $proyecto: { _id: 0 } } ]); |
Observaciones:
Se trata de una consulta bastante sencilla de tipo left outer join que une dos colecciones y luego simplemente cuenta el número total de documentos producidos. Tenga en cuenta que, a diferencia de N1QL (y SQL), en MongoDB, todavía tiene que agrupar el conjunto de resultados para obtener el recuento, incluso si tiene un único grupo.
Ejemplo 2: Enumerar los aeropuertos y lugares emblemáticos de una misma ciudad, ordenados por los aeropuertos.
Couchbase N1QL:
1 2 3 4 5 6 7 8 9 10 11 12 |
SELECCIONE hito.nombre AS Nombre_del_hito, MIN(aeropuerto.nombreaeropuerto) AS Nombre_del_aeropuerto, MIN(airport.tz) AS Hora_de_hito DESDE Viajar-muestra aeropuerto INNER JOIN Viajar-muestra hito EN aeropuerto.ciudad = hito.ciudad DONDE landmark.country = "Estados Unidos" Y aeropuerto.tipo = "aeropuerto" Y hito.tipo = "hito" GRUPO POR hito.nombre ORDENAR POR Nombre_del_aeropuerto |
MongoDB:
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 |
db.aeropuerto.agregado([ { $buscar: { de:"hito", localField: "ciudad", foreignField: "ciudad", como: "aplm_docs" } }, { $match: {"airline_docs": {$ne: []}} }, { $desenrollar: { ruta: "$aplm_docs", preserveNullAndEmptyArrays: verdadero }}, { $grupo: { _id: "$aplm_docs.name", Nombre_del_aeropuerto: { $min: "$airportname" } , Hora_de_hito: { $min: "$tz"} } }, { $ordenar : { Nombre_del_aeropuerto: 1 } }, { $proyecto: { _id: 1, Nombre_del_aeropuerto:1, Hora_de_hito:1 } } ]); |
Observaciones:
- Esta consulta utiliza INNER JOIN, que MongoDB no tiene. Por lo tanto, en MongoDB, primero se realiza la unión de búsqueda para obtener el LEFT OUTER JOIN y, a continuación, se eliminan los documentos no coincidentes pero proyectados (debido al left outer) mediante la etapa de coincidencia (código: $match: {"airline_docs": {$ne: []}}).
- Luego, hay que recordar que los documentos coincidentes están en una estructura de datos de matriz, desenrollarlos antes de agruparlos por el landmark.name. A continuación, realice la ordenación y la proyección final.
Como era de esperar, la consulta join de MongoDB es procedimental y tienes que entender el plan de ejecución y escribir código para cada etapa.
Ejemplo 3: Partiendo de San Francisco, busque todos los aeropuertos de destino (los que tengan rutas desde SFO).
Couchbase N1QL
1 2 3 4 5 6 7 8 9 |
SELECCIONE DISTINTO ruta.destinoaeropuerto DESDE Viajar-muestra aeropuerto ÚNASE A Viajar-muestra ruta EN (airport.faa = ruta.origenaeropuerto Y ruta.tipo = "ruta") DONDE aeropuerto.tipo = "aeropuerto" Y aeropuerto.ciudad = "San Francisco" Y aeropuerto.país = "Estados Unidos" ORDENAR POR ruta.destinoaeropuerto |
MongoDB:
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 |
db.aeropuerto.agregado([ { $match: { $y: [ {"tipo": "aeropuerto"}, { ciudad: "San Francisco"}, { "país": "Estados Unidos"} ] } }, { $buscar: { de:"ruta", deje: { rfaa : "$faa"}, tubería: [ { $match: { $expr: { $y: [ { $eq: ["$sourceairport", "$$rfaa"]} , { $eq: ["$type", "ruta"] } ] } } } ], como: "airline_docs" } }, { $match: {"airline_docs": {$ne: []}} }, { $desenrollar: { ruta: "$airline_docs", preserveNullAndEmptyArrays: verdadero }}, { $proyecto: { _id:0, "airline_docs.destinationairport" : 1 }}, { $grupo: { _id : "$airline_docs.destinationairport" } }, { $ordenar: { _id : 1 }}, ]); |
Observaciones:
- La cláusula join de esta consulta es un poco más compleja, con dos predicados (airport.faa = (route.sourceairport AND route.type = "route"). Esto requiere una sintaxis de canalización engorrosa en la consulta de MongoDB.
- Y como hay que diferenciar entre las dos colecciones, se necesita otra etapa let para crear las variables locales de los atributos del aeropuerto.
- Como antes, requiere una cláusula de coincidencia adicional para eliminar los documentos de aerolíneas no coincidentes (vacíos), seguida de agrupación y clasificación.
- Como se puede ver visualmente, la consulta MongoDB es cada vez más grande para hacer el mismo trabajo que Couchbase N1QL.
Ejemplo 4: Encontrar todos los hoteles y lugares emblemáticos de Yosemite. Los hoteles deben tener al menos 5 likes.
Couchbase N1QL
1 2 3 4 5 6 7 8 9 10 |
SELECCIONE nombre.hotel nombre_del_hotel, hito.nombre nombre_hito, hito.actividad DESDE Viajar-muestra hotel INNER JOIN Viajar-muestra hito EN (hotel.ciudad = hito.ciudad Y hotel.country = landmark.country Y hito.tipo = "hito") DONDE hotel.tipo = "hotel" Y hotel.title como "Yosemite%" Y array_length(hotel.public_likes) > 5; |
MongoDB:
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 |
db.hotel.agregado([ { $match: { título: { $regex: /^Yosemite/ } }, }, { $buscar: { de:"hito", deje: { hcity : "$city", hcountry : "$country"}, tubería: [ { $match: { $expr: { $y: [ { $eq: ["$city", "$$hcity"]} , { $eq: ["$country", "$$hcountry"] } ] } } } ], como: "hotel_lm_docs" } }, { $match : {"hotel_lm_docs": { $ne: [] }}}, { $proyecto: {_id:0, hname: "$name", me gusta_público: 1, hotel_lm_docs:1}}, { $desenrollar: { ruta: "$hotel_lm_docs", preserveNullAndEmptyArrays: verdadero }}, { $proyecto: { _id: 1, hname : 1 , "hotel_lm_docs.name" : 1, "hotel_lm_docs.name" : 1, "hotel_lm_docs.activity" : 1, mt5 : {$gt: [ {$talla: "$public_likes"}, 5]}}}, { $match: { mt5 : verdadero } }, { $proyecto: {_$id:0}} ]); |
Observación:
- Traducir el predicado LIKE en una expresión regular era sencillo, pero determinar si había al menos cinco public_likes no lo era. Se necesitó una fase adicional de proyección y concordancia para calcular el tamaño de los public_likes al final.
- Cuando se tienen muchos atributos que comparar, manipular y proyectar, hay que renombrarlos correctamente en el momento oportuno; de lo contrario, la consulta no puede hacer referencia a ellos. Por ejemplo, hotel.name tuvo que ser renombrado a hname antes del desenrollado. Quizá haya una forma mejor de escribir esta etapa.
- N1QL expresó la consulta en 370 caracteres. MongoDB requirió 956 caracteres. Todo esto para una unión de dos tablas. A medida que aumenta la complejidad, la proporción también aumenta, ya que la consulta de MongoDB está escrita de forma procedimental.
Ejemplo 5: Encuentra todos los hoteles y lugares emblemáticos de Yosemite. Los hoteles deben tener al menos 5 likes.
Esto es como el ejemplo 4, ¡pero más rápido!
Couchbase N1QL
1 2 3 4 5 6 7 8 9 10 |
SELECCIONE nombre.hotel nombre_del_hotel, hito.nombre nombre_hito, hito.actividad DESDE Viajar-muestra hotel INNER JOIN Viajar-muestra hito UTILICE HASH(construir) EN (hotel.ciudad = hito.ciudad Y hotel.country = landmark.country Y hito.tipo = "hito") DONDE hotel.tipo = "hotel" Y hotel.title como "Yosemite%" Y array_length(hotel.public_likes) > 5; |
Observación:
El método join por defecto en Couchbase N1QL es nested loop join. Esto funciona bien cuando tienes un pequeño número de documentos involucrados en cada lado de la unión. Cuando tienes un conjunto de datos más grande, típicamente en consultas de reportes, la unión de bucles anidados se vuelve más lenta. Couchbase N1QL tiene uniones hash y esto acelera las uniones significativamente. Cuando cada lado de la unión tiene de miles a millones de documentos, el incremento de velocidad puede ser de 2x a 20x o más. Ver el blog detallado de Couchbase en Uniones ANSI para más información.
A partir de la documentación y el plan de explicación, no está claro qué método de unión utiliza MongoDB. Algunos de los blogs indican que han utilizado una unión de bucle anidado para implementar el operador $lookup.
Resumen:
Couchbase N1QL | MongoDB | |
Enfoque JOIN | Declarativo, como SQL.
Permite unir conjuntos de datos de cualquier tamaño y distribuidos. |
Procedimental con algunos aspectos declarativos (por ejemplo, selección de índices).
Sólo puede unirse a una colección sharded en un colección sin triturar. Para unir dos colecciones fragmentadas, las aplicaciones tendrán que escribir el algoritmo de unión. |
JOINs compatibles | LEFT OUTER JOIN
INNER JOIN UNIÓN EXTERNA DERECHA |
$lookup implementa el LEFT OUTER JOIN en valores escalares. |
Apoyo a la cláusula ON | Expresiones completas.
Escamas Matrices |
Igualdad implícita
Expresión de la tubería Las matrices deben ser $unwind antes de la $lookup |
Aplicación de JOIN | Bloque Bucle anidado
Hash join con construcción y sondas definidas por el usuario. |
Bucle anidado |
Cláusula ON | cláusula ON con cualquier expresión. | 1TP4Expresión de la línea de producción |
Expresiones de matriz en la cláusula ON | Utilice las expresiones ANY, IN.
Admite UNNEST |
Tubería con $unwind antes de $match |
Explique | Explicación visual y
JSON explicar |
Explicación visual y
JSON explicar |
JOIN orden | De izquierda a derecha, según especifique el usuario. El optimizador se basa en reglas. | Como se especifica en la canalización. |
JOINs anidados | Soportado mediante tablas derivadas.
La cláusula FROM puede tener subselectas que a su vez pueden tener uniones o subselectas. |
No |
Tratamiento del predicado JOIN | El optimizador procesa los predicados join, predicados constantes y empuja los predicados al índice automáticamente. | Diseño manual de predicados para cada colección, ordenación cuidadosa de las etapas del pipeline sin ayuda completa del optimizador. |
¿Y el rendimiento? Buena pregunta. Eso es para un futuro blog.
Y ahora, una cita:
"Una frase no debe contener palabras innecesarias, un párrafo frases innecesarias, por la misma razón que un dibujo no debe tener líneas innecesarias y una máquina piezas innecesarias".
- William Strunk, Jr. Elementos de estilo.
Referencias:
- Documentación de Couchbase: https://docs.couchbase.com
- Documentación de MongoDB: https://docs.mongodb.com/
- Uniones ANSI en Couchbase N1QL: https://www.couchbase.com/blog/ansi-join-support-n1ql/
- Tutorial N1QL: https://query-tutorial.couchbase.com/tutorial/#1