¿Cómo funcionan los joins en bases de documentos?

Actualizado: agosto de 2026

Una consulta relacional en una base de documentos es un join hecho con referencias y lookups en vez de claves foráneas — o evitado al embeber. Ese “o” es todo el tema: las bases de documentos te dan tres maneras de relacionar datos, y la decisión de diseño es cuándo ocurre el join — al escribir (embeber), al consultar en el servidor (lookup), o al consultar en la aplicación (referencias más un fetch de seguimiento).

Puntos clave

PreguntaRespuesta
¿Las bases de documentos pueden hacer joins?Sí — los lookups hacen left outer joins, devolviendo arrays, no filas
La decisión realEmbeber vs. referenciar — ¿cuándo ocurre el join?
Regla por defectoEmbebe lo que se lee junto y es acotado; referencia lo que vive solo o crece
La verdad de performanceLos lookups server-side son joins de nested loop — bien por solicitud, mal para analítica
El bug clásicoConsultas N+1 — se arregla con batching, includes o embebiendo

Las tres maneras de relacionar documentos

// 1 · Embeber — el "join" ocurrió al momento de escribir
{ _id: 1, title: "Dune", author: { name: "Frank Herbert", born: 1920 } }

// 2 · Referencia + $lookup — el join ocurre al consultar, server-side
db.books.aggregate([
  { $lookup: { from: "authors", localField: "authorId",
               foreignField: "_id", as: "author" } },  // left outer join → array
  { $unwind: "$author" }                               // aplanar → inner join
])

// 3 · Referencia + join en la aplicación — la vía del ODM/SDK (abajo)

La tercera ruta es donde vive la mayoría del código de aplicación — referencias tipadas recorridas con eager loading en una sola solicitud, para que los documentos relacionados lleguen juntos sin un pipeline:

// JavaScript / Node.js — Back4app JS SDK
// A join in a document database: Pointer + include, one request
const query = new Parse.Query('Comment');
query.equalTo('post', postPointer);   // Comment.post is a Pointer<Post>
query.include('author');              // "join" the author document in
const comments = await query.find();

const name = comments[0].get('author').get('username'); // already loaded — no N+1

Embeber vs. referenciar: la decisión

Flujo de decisión entre embeber o referenciarLos datos que siempre se leen con su padre y tienen tamaño acotado deben embeberse; los datos compartidos, actualizados de forma independiente o sin límite de crecimiento deben referenciarse, invirtiendo la dirección de la referencia para conjuntos de hijos enormes.

no

no

no

¿Se lee siempre junto
con el padre?

Referenciar

¿Tamaño acotado?
(sin crecimiento infinito)

¿Compartido con
otros padres?

Embeber

¿Cantidad enorme de hijos?

Inviértelo: guarda la referencia
al padre en cada hijo

Los datos que siempre se leen con su padre y tienen tamaño acotado deben embeberse; los datos compartidos, actualizados de forma independiente o sin límite de crecimiento deben referenciarse, invirtiendo la dirección de la referencia para conjuntos de hijos enormes.
DimensiónEmbeberReferenciar
Patrón de lecturaSiempre se trae con el padreSe trae solo o bajo demanda
Patrón de escrituraSe actualiza con el padre, atómicamenteSe actualiza de forma independiente
CardinalidadUno-a-pocosUno-a-muchos y más allá
CrecimientoAcotado (las direcciones de una persona)Sin límite (los comentarios de un post)
ComparticiónPertenece a un solo padreCompartido entre padres
El costo del joinCero — pagado al escribirPagado por consulta — lookup, include o batch

Los niveles de cardinalidad dan la misma tabla como reglas rápidas: uno-a-pocos embebe el array, uno-a-muchos referencia por ID, uno-a-enormes invierte la dirección — el hijo guarda la referencia al padre, porque ningún documento padre puede sostener un array que crece sin parar. Lo que apunta a los dos anti-patrones detrás de la mayoría de los incidentes de modelado de documentos: los arrays sin límite y el tope de 16MB por documento que eventualmente amenazan. Los arreglos estándar — referenciar en su lugar, embeber un subconjunto acotado (los N más nuevos con una colección de desborde), o agrupar los hijos en documentos-bucket — son todos versiones de “detén el crecimiento del documento”.

El lookup, honestamente

$lookup es un join real con dos asteriscos honestos. La forma: devuelve las coincidencias como un array embebido por documento de entrada — $unwind lo aplana a filas y, sin la semántica de preservar vacíos, convierte el left join en comportamiento de inner join. La performance: los motores de documentos solo ejecutan joins de nested loop, y los benchmarks públicos son directos — hacer join sobre un millón de documentos tomó decenas de segundos con índices, contra medio segundo del equivalente embebido. Las reglas operativas que se desprenden: indexa siempre el campo foráneo (sin índice, cada documento de entrada dispara un escaneo de colección), usa lookups para joins por solicitud sobre un puñado de documentos, y nunca construyas fan-outs de analítica sobre ellos — esa carga pertenece a un warehouse o a un motor relacional.

El problema N+1 y sus cuatro salidas

El bug clásico: una consulta para N padres, y luego un loop que emite una query por padre para sus hijos — N+1 idas a la base que escalan con el tamaño de la página. Las salidas, de mejor a peor: batch — junta los IDs de los padres y trae todos los hijos en una sola query de contenido-en (el populate de los buenos ODMs lo hace por ti); include — eager loading a nivel del SDK que devuelve los documentos referenciados en la misma solicitud, como en las pestañas de arriba; lookup — un solo pipeline server-side; embeber — el join deja de existir. Lo que convierte el N+1 de bug en arquitectura es no notarlo: sale rápido con diez registros de prueba y se derrite con mil — la patología completa tiene su propia entrada en el registro de este glosario.

Casos de uso comunes

  • Contenido con autoría. Posts, comentarios, autores — referencias con includes para las vistas de lista, embebidos para los campos snapshot de solo lectura.
  • Catálogos y pedidos. La referencia extendida canónica: un pedido embebe el nombre y el precio del producto tal como se vendió (snapshot inmutable) más una referencia al producto vivo.
  • Streams de actividad y eventos. Uno-a-enormes — documentos hijos con referencias al padre, nunca arrays en el padre.
  • Perfiles de usuario. La vitrina del embebido: direcciones, preferencias, configuraciones — se leen juntas, acotadas, con dueño.
  • Grafos sociales. Arrays de referencias muchos-a-muchos — y la señal honesta de que, pasado un punto, esta forma pide un motor relacional o de grafos.

¿Embeber, referenciar o cambiar de motor? Matriz de decisión

Embebe cuando…Referencia cuando…Usa una base relacional cuando…
Se leen y actualizan juntosSe acceden por separadoLos joins son la carga de trabajo, no la excepción
Pequeño y acotadoSin límite o de alta cardinalidadAnalítica ad-hoc entre entidades
Con un solo padre dueñoCompartido entre padresSe exige integridad referencial estricta
Importan los updates atómicos con el padreCiclos de actualización independientesTransacciones complejas multi-fila
El caso clásico: campos de perfilEl caso clásico: comentariosEl caso clásico: dominios cargados de muchos-a-muchos

La tercera columna es la sección que las páginas de proveedores no van a escribir: si cada pantalla necesita tres lookups y la integridad referencial te quita el sueño, los datos están pidiendo tablas — los modelos de documentos ganan cuando los patrones de acceso son conocidos y jerárquicos, no como reemplazo universal.

Limitaciones y trade-offs

  • La desnormalización es un instrumento de deuda. Los campos duplicados matan joins pero deben pagarse al actualizar — el fan-out de escrituras, las ventanas de datos viejos y el código de consistencia (transacciones o triggers de change streams) son los intereses.
  • Sin claves foráneas, sin red de seguridad. Las referencias no garantizan existencia; borrar un autor deja referencias huérfanas en los libros, en silencio, a menos que tu plataforma o tu código las limpien.
  • Los lookups no se optimizan. Sin reordenamiento de joins, sin estrategias hash — el orden del pipeline es tu plan de consulta.
  • Las migraciones entre formas son proyectos reales. Pasar de embebido a referenciado (el rescate del array que crece) significa hacer backfill de colecciones y reescribir queries — modela para la cardinalidad de mañana, no la de hoy.
  • El tope de 16MB es un precipicio, no una advertencia. Los patrones de crecimiento que se le acercan degradan la performance mucho antes de tocarlo.

Las consultas relacionales en Back4app

Back4app es una plataforma open-source de Backend as a Service (BaaS) que combina base de datos gestionada, APIs REST y GraphQL generadas automáticamente, autenticación, almacenamiento de archivos y funciones serverless con Cloud Code. Su base de datos de documentos habla un vocabulario relacional por diseño: los Pointers declaran aristas uno-a-muchos, las Relations manejan el muchos-a-muchos, e include() recorre las aristas en una sola solicitud — la salida del N+1 integrada en el SDK, como muestran las pestañas de código de arriba. La API GraphQL generada automáticamente anida objetos relacionados en una sola query sin costo extra, y los borrados pueden ponerse en cascada con triggers de Cloud Code — la red de seguridad de integridad referencial que los motores de documentos dejan fuera.

Preguntas frecuentes

¿MongoDB soporta joins?

Joins estilo SQL, no — pero sí unos relacionalmente útiles. La etapa de agregación $lookup realiza un left outer join, adjuntando los documentos coincidentes de otra colección como un array embebido en lugar de filas planas. Agregar $unwind lo convierte a semántica de inner join. Lo que difiere de SQL es la forma del resultado, el perfil de performance y el hecho de que los joins son la excepción y no el default.

¿Debería embeber o referenciar los datos relacionados?

La regla de consenso: embebe lo que lees, actualizas y archivas junto — datos pequeños, acotados y fuertemente acoplados. Referencia lo que vive por su cuenta: compartido entre padres, actualizado de forma independiente, de alta cardinalidad o sin límite de crecimiento. La guía oficial dice favorecer el embebido salvo razón de peso, y las razones de peso son precisamente esas cuatro.

¿Cómo cambia la cardinalidad la decisión de modelado?

Los niveles clásicos: uno-a-pocos (las direcciones de una persona) — embebe el array. Uno-a-muchos (cientos a miles) — un array de referencias. Uno-a-enormes (los eventos de log de una máquina) — invierte la dirección y guarda la referencia al padre en cada hijo, porque el padre no puede sostener un array que crece sin fin. Muchos-a-muchos — arrays de referencias, a veces en ambos lados.

¿$lookup es lento?

Comparado con los joins relacionales, consistentemente sí: los motores de documentos ejecutan joins de nested loop sin las estrategias de merge y hash que tienen los optimizadores relacionales. Benchmarks públicos haciendo join sobre un millón de documentos midieron decenas de segundos incluso con índices, contra medio segundo del equivalente embebido. Las reglas operativas: indexa siempre el campo foráneo, mantén $lookup en rutas por solicitud que unan un puñado de documentos, y nunca construyas fan-outs de analítica sobre él.

¿Qué es el límite de 16MB por documento y por qué importa aquí?

Un tope duro al tamaño de un documento individual — y la razón por la que "embebe todo y ya" falla. Los arrays embebidos sin límite (comentarios, logs, eventos) crecen hacia el tope y degradan la eficiencia de la caché y de los índices mucho antes de alcanzarlo. Los arreglos estándar: cambiar a referencias, embeber solo un subconjunto acotado (los N más recientes) con una colección de desborde, o agrupar los hijos en documentos-bucket.

¿Qué es el problema N+1 en bases de documentos?

Traer N padres con una consulta y luego emitir una query más por padre para sus datos relacionados — N+1 round trips que crecen con el conjunto de resultados. Los arreglos, en orden de preferencia: agrupar el segundo paso en una sola query sobre los IDs juntados, usar un lookup server-side en un solo pipeline, traer los documentos relacionados en una solicitud vía el mecanismo include del SDK, o embeber para que el "join" haya ocurrido al escribir.

¿Cómo expresan las relaciones los ODMs y los SDKs de backend?

Como referencias tipadas con un operador de eager loading. Los ODMs de documentos declaran campos de referencia y los pueblan con queries de seguimiento agrupadas en batch; los SDKs de backend usan Pointers — una referencia tipada a otro objeto — y un operador include que trae los documentos referenciados en la misma solicitud, más tipos Relation para conjuntos muchos-a-muchos grandes. La misma idea en todas partes: declara la arista y luego elige cuándo recorrerla.

¿Cuándo es una base relacional simplemente la mejor opción?

Cuando los joins son la carga de trabajo y no la excepción: dominios cargados de muchos-a-muchos, analítica ad-hoc entre entidades, integridad referencial estricta y transacciones complejas multi-fila. Los modelos de documentos ganan cuando los patrones de acceso son conocidos y jerárquicos — un documento por pantalla de datos. Si cada query necesita tres lookups, los datos te están diciendo que quieren tablas.

Términos relacionados

Compara con

Lecturas recomendadas

¿Listo para construir tu backend?

Empieza tu proyecto en Back4app en minutos — base de datos, autenticación, APIs y Cloud Code incluidos. Sin tarjeta de crédito.

Escrito y revisado por Back4app Engineering, Back4app Engineering · Publicado el 2026-09-01