UD10 · Bases de datos no relacionales (NoSQL)

Resumen del tema

Durante nueve unidades has trabajado con un único modelo de datos: el relacional. Has aprendido a analizar requisitos, dibujar un diagrama entidad/relación, normalizar, crear tablas con restricciones, consultar, modificar datos dentro de transacciones y programar el propio servidor. Esta última unidad no viene a desmontar nada de eso: viene a ampliar el mapa. Existen familias de sistemas gestores que renuncian deliberadamente a algunas garantías del modelo relacional para conseguir otras cosas (esquema flexible, escalado horizontal, latencia muy baja), y un profesional debe saber cuándo merece la pena ese intercambio.

El término NoSQL agrupa a esos sistemas. Estudiaremos las cuatro grandes familias (clave-valor, documental, columnar y de grafos) y trabajaremos en profundidad una base de datos documental, MongoDB Community Server 8.0, porque es el tipo más cercano a lo que ya sabes y el más extendido en el desarrollo de aplicaciones. Para que la comparación sea honesta usaremos los mismos datos de siempre: una versión documental de EduGest con los 32 expedientes del curso 2025-26, de modo que cada consulta de MongoDB se pueda poner al lado de su equivalente en Oracle y comparar resultado a resultado.

El hilo conductor de la unidad no es la sintaxis de mongosh, sino el criterio de elección. En el currículo, el RA7 pide caracterizar estas bases de datos, evaluar sus tipos, identificar sus elementos y gestionar la información con las herramientas del gestor; en la práctica profesional, lo que se te va a pedir es justificar una decisión de arquitectura. Por eso la unidad termina con tres casos razonados y con una conclusión que conviene adelantar: para la gestión académica de un centro, el modelo relacional sigue siendo la elección correcta. Saber por qué es, exactamente, el objetivo del tema.

Enlaza con la UD01 (modelos de datos, bases distribuidas, Big Data y protección de datos), con la UD03 y la UD04 (el modelo relacional y la normalización, que ahora servirán de contraste), con la UD07 (los informes que vamos a reescribir con el pipeline de agregación) y con la UD08 (transacciones y propiedades ACID, que aquí se discuten en un entorno distribuido).

Resultados de aprendizaje y criterios de evaluación
RA7 Gestiona la información almacenada en bases de datos no relacionales, evaluando y utilizando las posibilidades que proporciona el sistema gestor. 5 CE
  • RA7.a Se han caracterizado las bases de datos no relacionales.
  • RA7.b Se han evaluado los principales tipos de bases de datos no relacionales.
  • RA7.c Se han identificado los elementos utilizados en estas bases de datos.
  • RA7.d Se han identificado distintas formas de gestión de la información según el tipo de base de datos no relacionales.
  • RA7.e Se han utilizado las herramientas del sistema gestor para la gestión de la información almacenada.
RA1 Reconoce los elementos de las bases de datos analizando sus funciones y valorando la utilidad de los sistemas gestores. 1 CE
  • RA1.b Se han identificado los distintos tipos de bases de datos según el modelo de datos utilizado.

Objetivos de aprendizaje

Al terminar esta unidad serás capaz de:

  • Caracterizar las bases de datos no relacionales y explicar qué problemas motivaron su aparición.
  • Distinguir las propiedades ACID de las propiedades BASE y razonar el teorema CAP sobre un caso concreto.
  • Evaluar los cuatro tipos principales de bases de datos NoSQL e identificar casos de uso adecuados para cada uno.
  • Identificar los elementos de una base de datos documental: base de datos, colección, documento, campo, tipos BSON y _id.
  • Utilizar mongosh y MongoDB Compass para gestionar la información almacenada.
  • Realizar operaciones CRUD y consultas sobre subdocumentos y arrays, indicando su equivalente en SQL.
  • Construir tuberías de agregación y compararlas con la consulta SQL que resuelve el mismo informe.
  • Diseñar un modelo documental decidiendo de forma justificada qué información se incrusta y qué se referencia.
  • Declarar validadores de esquema e índices, y comprobar su efecto.
  • Elegir entre modelo relacional y NoSQL argumentando con criterios técnicos, no con modas.

Temporalización

La unidad ocupa 4 horas de aula (2 de teoría y 2 de práctica) y cierra el módulo. Es una unidad de criterio profesional: importa más saber cuándo elegir cada modelo que memorizar la sintaxis de mongosh.

UD10 · 4 horas de aula Teoría 2 h · Práctica 2 hDel 22/04/2027 al 27/04/2027
SesiónHorasTipoContenidoDónde
11TeoríaCaracterísticas de NoSQL, BASE y CAP, tipos de bases de datos y elementos de MongoDB§1 a §4 · traductor SQL ↔ MongoDB
21PrácticaPuesta en marcha de MongoDB y CRUD sobre los expedientesPrácticas 10.1 y 10.2
31TeoríaOperaciones CRUD, subdocumentos, arrays y agregaciones§5 a §7
41PrácticaModelado documental y decisión razonadaPrácticas 10.5 y 10.7 · §8 a §11
Trabajo autónomo (fuera del horario de aula): Práctica 10.3 (operadores, subdocumentos y arrays) · Práctica 10.4 (el mismo informe en SQL y en MongoDB) · Práctica 10.6 (otros modelos: clave-valor y grafos) · Proyecto EduGest · UD10 (el expediente documental)

Estudia esta unidad en paralelo: cada vez que aparezca una orden de mongosh, tapa la columna de la derecha y escribe tú la sentencia SQL equivalente. Si no sabes traducirla, es que no has entendido la orden. Y cuando una operación no tenga equivalente directo (en un sentido o en el otro), apunta por qué: ahí está la diferencia real entre los dos modelos, y ahí se decide una arquitectura.


Sesión 1 1 h Teoría · NoSQL, BASE y CAP, tipos y elementos de MongoDB

1. Qué son las bases de datos NoSQL

1.1 El problema que vinieron a resolver

El modelo relacional se diseñó a principios de los años setenta para un escenario concreto: un volumen de datos moderado, una estructura conocida y estable, un servidor potente y la necesidad absoluta de que los datos fueran correctos. En ese escenario sigue siendo imbatible.

A partir de la segunda mitad de los años 2000, algunas empresas se encontraron con escenarios distintos:

  1. Volumen y velocidad. Un portal con millones de usuarios simultáneos genera más escrituras de las que admite un único servidor, por grande que sea.
  2. Variabilidad del esquema. En un catálogo de comercio electrónico, un portátil y un ratón no comparten características. Modelarlo con tablas obliga a una tabla por tipo de producto, a una tabla entidad-atributo-valor o a decenas de columnas casi siempre nulas.
  3. Datos semiestructurados. Documentos JSON procedentes de API, registros de actividad, mensajes de dispositivos: información con forma de árbol que hay que trocear en varias tablas para guardarla y recomponer con JOIN para leerla.
  4. Escalado horizontal. Resulta más barato y más tolerante a fallos repartir los datos entre veinte máquinas modestas que comprar una máquina veinte veces más potente.
  5. Desarrollo ágil. Si el esquema cambia cada dos semanas, cada cambio implica un ALTER TABLE y una migración coordinada con el despliegue de la aplicación.

Fíjate en que ninguno de esos cinco problemas aparece en EduGest. La matrícula de un centro tiene un volumen pequeño, un esquema estable fijado por normativa, relaciones complejas y una exigencia máxima de integridad. Tenerlo claro desde el principio evita la conclusión equivocada de que NoSQL es «la versión moderna» de las bases de datos.

1.2 Qué significa realmente «NoSQL»

El nombre es desafortunado y tiene origen histórico: se usó como etiqueta de un encuentro técnico en 2009. Hoy se interpreta como «Not only SQL» («no solo SQL»), y conviene precisar qué afirma y qué no afirma:

Lo que no significaLo que sí suele implicar
Que no exista un lenguaje de consultaQue el lenguaje no es SQL estándar, sino propio del producto (y a veces se parece mucho: Cypher, CQL, el pipeline de MongoDB)
Que no haya esquemaQue el esquema no lo impone el gestor por defecto: lo impone la aplicación, o un validador que se declara aparte
Que no haya transacciones ni integridadQue las garantías son configurables y suelen alcanzar a un solo documento o agregado, no a varias colecciones
Que sustituya al modelo relacionalQue convive con él: lo normal en una empresa es tener los dos (persistencia políglota, §11.4)

Una base de datos NoSQL no es una base de datos «sin reglas». Es una base de datos en la que las reglas se trasladan de sitio: del esquema declarativo del SGBD al código de la aplicación o a un validador explícito. Ese traslado tiene un coste que hay que decidir conscientemente.

1.3 Esquema flexible frente a esquema rígido

En Oracle, la estructura se declara antes de guardar nada: Oracle 26ai

-- Hay que decidir ahora las columnas, los tipos y las restricciones
CREATE TABLE alumno (
    id_alumno         NUMBER(6)    PRIMARY KEY,
    nia               CHAR(8)      NOT NULL,
    nombre            VARCHAR2(40) NOT NULL,
    fecha_nacimiento  DATE         NOT NULL
);
-- Un INSERT sin fecha_nacimiento falla: ORA-01400

En MongoDB, la colección se crea al escribir el primer documento y cada documento puede tener campos distintos: MongoDB 8.0

// Dos documentos de la misma colección, con forma diferente
db.expedientes.insertOne({ _id: 1, nia: "10450037", nombre: "Adrián", dni: "55568986C" })
db.expedientes.insertOne({ _id: 5, nia: "10450185", nombre: "Andrea" })   // sin dni: se acepta

Esta flexibilidad resuelve el problema 2 del apartado anterior, pero tiene consecuencias inmediatas:

  • Una consulta puede devolver documentos a los que les falta el campo que esperabas. En Oracle el campo estaría presente con valor NULL; en MongoDB no está, y eso se consulta de otra forma ($exists, §5.2).
  • Nadie impide guardar nota: "siete" en un documento y nota: 7 en otro. La comparación nota: { $gte: 5 } ignorará el primero, porque MongoDB compara por tipo antes que por valor.
  • El esquema sigue existiendo: está en la cabeza de quien programa y en el código. Si no se escribe en algún sitio (§9), se degrada en cuanto el equipo cambia.

«Esquema flexible» no es «esquema inexistente». En un proyecto serio, una colección de MongoDB lleva validador $jsonSchema (§9.1), igual que una tabla lleva restricciones. La diferencia es que en MongoDB es opcional, y por eso hay que acordarlo en el equipo.

1.4 La desnormalización como decisión de diseño

En la UD04 aprendiste a eliminar la redundancia porque provoca anomalías: el mismo dato guardado en dos sitios acaba siendo distinto en los dos sitios. Esa conclusión era correcta para el modelo relacional, donde recomponer la información con JOIN es barato.

En un sistema distribuido la situación cambia: un JOIN entre tablas repartidas en máquinas diferentes exige tráfico de red y coordinación. Por eso el modelado documental incrusta (duplica) información deliberadamente, de modo que lo que se lee junto se guarde junto y cada lectura toque un solo documento y una sola máquina.

Modelo relacional normalizadoModelo documental desnormalizado
El nombre del módulo «Bases de datos»Está una vez en MODULOSe repite en cada matrícula de cada expediente
Corregir una errata en ese nombreUn UPDATE de una filaUn updateMany que recorre toda la colección
Leer el expediente completo de un alumnoJOIN de tres o cuatro tablasUna lectura de un documento
Riesgo de incoherenciaLo elimina el SGBDHay que evitarlo desde la aplicación

La pregunta que decide la duplicación es: ¿este dato cambia o es prácticamente inmutable? El nombre de un módulo publicado en el BOE no cambiará durante el curso: duplicarlo es seguro. El tutor de un grupo sí puede cambiar: duplicarlo en 30 expedientes es un problema esperando a ocurrir.

1.5 Escalado, réplica y particionado

EstrategiaEn qué consisteLímite
Escalado vertical (scale up)Poner más CPU, más memoria o discos más rápidos en el mismo servidorHay un tope físico y el precio crece más rápido que la potencia; sigue habiendo un punto único de fallo
Escalado horizontal (scale out)Repartir carga y datos entre más servidores (nodos)Exige coordinación entre nodos; aparecen los problemas del §2

Sobre el escalado horizontal se construyen dos mecanismos que ya viste en la UD01 con otros nombres:

  • Réplica (replica set en MongoDB): varios nodos guardan una copia completa de los datos. Uno es el primario y recibe las escrituras; los secundarios las replican. Mejora la disponibilidad (si cae el primario, se elige otro) y permite repartir lecturas.
  • Particionado (sharding): los datos se reparten entre varios conjuntos de réplica según una clave de partición (shard key). Es la fragmentación horizontal de la UD01. Permite crecer sin límite teórico, pero elegir mal la clave concentra la carga en un solo nodo.
flowchart LR
    APP[Aplicación] --> R[Enrutador]
    R --> S1[(Partición A<br/>NIA 10450000-10450999)]
    R --> S2[(Partición B<br/>NIA 10451000-10451999)]
    S1 --> S1a[(Réplica)]
    S1 --> S1b[(Réplica)]
    S2 --> S2a[(Réplica)]
    S2 --> S2b[(Réplica)]

Oracle también escala horizontalmente (Oracle RAC, Oracle Globally Distributed Database). La diferencia no es que «SQL no escale», sino el coste y la complejidad de hacerlo manteniendo todas las garantías transaccionales entre nodos.

1.6 Qué se gana y qué se pierde

Se ganaSe pierde
Esquema flexible: el modelo evoluciona sin migraciones costosasIntegridad referencial declarativa: no hay FOREIGN KEY entre colecciones
Lectura de un agregado completo en una sola operaciónComposiciones (JOIN) naturales y baratas entre cualquier par de entidades
Escalado horizontal sencillo y tolerancia a fallosConsultas ad hoc no previstas: el modelo está optimizado para los accesos que se diseñaron
Modelo de datos muy cercano a los objetos de la aplicaciónNormalización automática contra anomalías de actualización
Latencias muy bajas en los accesos por claveMadurez y estandarización: SQL es un estándar ISO; cada producto NoSQL tiene su propio lenguaje
Buen ajuste a datos semiestructurados o jerárquicosHerramientas de informes, auditoría y administración menos uniformes
¿Y los datos «no estructurados»?

Es frecuente leer que NoSQL sirve para «datos no estructurados». Conviene matizar el vocabulario:

  • Estructurados: encajan en filas y columnas de tipos conocidos (la matrícula de EduGest).
  • Semiestructurados: tienen estructura, pero variable y autodescriptiva (JSON, XML). Es el terreno natural del modelo documental.
  • No estructurados: imágenes, audio, vídeo, texto libre. Ninguna base de datos los interpreta: se guardan como binarios (o en un almacén de objetos) y lo que se indexa son sus metadatos, que vuelven a ser estructurados o semiestructurados.

Una base documental trabaja sobre todo con datos semiestructurados.


Curiosidad El término «NoSQL» es anterior a MongoDB

Carlo Strozzi lo usó en 1998 para una base de datos relacional ligera que no usaba SQL. El significado actual nació en 2009, en un encuentro en San Francisco organizado por Johan Oskarsson sobre bases de datos distribuidas no relacionales.

2. Transacciones distribuidas: BASE y el teorema CAP

2.1 Recordatorio: ACID

En la UD08 definiste una transacción como una unidad de trabajo que cumple cuatro propiedades: Atomicidad (todo o nada), Consistencia (se pasa de un estado válido a otro válido), Aislamiento (las transacciones concurrentes no se estorban) y Durabilidad (lo confirmado sobrevive a una caída). Oracle las garantiza en un servidor sin que haya que pedirlo.

Mantener ACID entre varias máquinas es mucho más caro: cada confirmación exige que todos los nodos implicados se pongan de acuerdo (protocolos de consenso o de confirmación en dos fases), lo que añade latencia y, si un nodo no responde, bloquea la operación.

2.2 BASE: la alternativa pragmática

Frente a ACID, parte del mundo NoSQL adoptó un conjunto de propiedades deliberadamente más débiles, resumidas en el acrónimo BASE:

PropiedadSignificadoQué implica
Basically AvailableBásicamente disponibleEl sistema responde siempre, aunque la respuesta no sea la más actual o esté incompleta
Soft stateEstado blandoEl estado de un nodo puede cambiar sin que haya escrituras nuevas, solo porque llega la replicación
Eventual consistencyConsistencia eventualSi dejan de llegar escrituras, todos los nodos acabarán coincidiendo; mientras tanto, pueden discrepar

El ejemplo clásico es un contador de «me gusta»: que durante dos segundos un usuario vea 1.034 y otro 1.035 no tiene ninguna consecuencia. El contraejemplo clásico es una nota de un acta oficial: que dos profesores vean notas distintas del mismo alumno es inadmisible.

2.3 El teorema CAP

Formulado por Eric Brewer y demostrado después formalmente, el teorema CAP afirma que un sistema distribuido no puede garantizar simultáneamente las tres propiedades siguientes:

  • Consistency (consistencia): toda lectura devuelve la escritura más reciente.
  • Availability (disponibilidad): toda petición recibe una respuesta no errónea.
  • Partition tolerance (tolerancia a particiones): el sistema sigue funcionando aunque se pierdan mensajes entre nodos.

Ejemplo concreto. Imagina EduGest replicado en dos sedes, Alicante y Elche, y que se corta la fibra entre ellas. Una profesora en Elche intenta guardar la nota de un alumno:

flowchart LR
    PA[Secretaría Alicante] --> A[(Nodo Alicante)]
    PE[Profesora en Elche] --> B[(Nodo Elche)]
    A -. red cortada .- B

El nodo de Elche solo puede hacer dos cosas:

  1. Aceptar la escritura y propagarla cuando vuelva la red. El sistema sigue disponible (A), pero durante el corte Alicante lee una nota desactualizada: se ha sacrificado la consistencia.
  2. Rechazar la escritura («no puedo garantizar que esto sea coherente, inténtalo más tarde»). Se mantiene la consistencia (C) a costa de la disponibilidad.

Como en un sistema distribuido las particiones de red ocurren y no se pueden evitar, la P no es opcional. La elección real de diseño es entre C y A durante la partición. Los sistemas que priorizan C se llaman CP (MongoDB con su configuración por defecto, HBase); los que priorizan A se llaman AP (Cassandra, DynamoDB con lecturas eventuales). Un SGBD relacional en un único servidor no está en el teorema: sin varios nodos no hay particiones.

2.4 Qué implica la consistencia eventual para una aplicación

Si el sistema es eventualmente consistente, la aplicación debe asumir que:

  • Puede leer lo que acaba de escribir… o no. Un usuario guarda su perfil, la pantalla siguiente lee de un nodo que todavía no lo tiene y parece que el cambio se ha perdido. Se mitiga con la lectura dirigida al nodo primario (read your own writes).
  • Dos escrituras simultáneas en nodos distintos pueden entrar en conflicto, y alguien tiene que resolverlo: la última gana, se guardan las dos versiones, se fusionan por reglas de negocio…
  • Las comprobaciones del tipo «no permitas dos matrículas del mismo alumno en el mismo módulo» no se pueden delegar en una restricción UNIQUE global sin coordinación entre nodos.

2.5 Matización imprescindible: NoSQL no significa «sin ACID»

La oposición «relacional = ACID, NoSQL = BASE» era razonable alrededor de 2010 y hoy es inexacta:

  • MongoDB admite transacciones ACID multidocumento desde la versión 4.0 (2018) en conjuntos de réplica, y desde la 4.2 también en clústeres particionados. En la 8.0 son una característica normal del producto.
  • Además, y esto es anterior y más importante en el día a día, toda operación de escritura sobre un único documento es atómica, incluidos los subdocumentos y arrays que contenga. Buena parte de las transacciones que en el modelo relacional abarcan varias tablas desaparecen si la información que cambia junta está en el mismo documento.
  • En el otro sentido, Oracle 23ai y 26ai incorporan tipo de datos JSON, índices sobre JSON y vistas duales JSON-relacional, con las que los mismos datos relacionales se leen y modifican como documentos. La frontera es cada vez más difusa.
// Transacción multidocumento en mongosh (MongoDB 8.0)
const s = db.getMongo().startSession()
s.startTransaction()
try {
  const ex = s.getDatabase("edugest").expedientes
  ex.updateOne({ _id: 1 }, { $push: { matriculas: { modulo: { codigo: "0486" }, curso: "2026-27" } } })
  ex.updateOne({ _id: 1 }, { $inc: { creditosMatriculados: 12 } })
  s.commitTransaction()
} catch (e) {
  s.abortTransaction()
  print("Abortada: " + e.message)
} finally {
  s.endSession()
}

Si al diseñar en MongoDB necesitas transacciones multidocumento a menudo, suele ser una señal de que el modelo documental no está bien planteado (o de que el problema es relacional). Úsalas como red de seguridad, no como herramienta de uso diario: tienen coste y límites de duración.

Autoevaluación 2 preguntas
1 Durante un corte de red entre dos nodos, un sistema AP decide…
2 ¿Es correcto decir que «MongoDB no tiene transacciones»?

Curiosidad El teorema CAP, de conjetura a teorema

Eric Brewer lo planteó como conjetura en el año 2000 y Seth Gilbert y Nancy Lynch lo demostraron en 2002: un sistema distribuido no puede garantizar a la vez consistencia, disponibilidad y tolerancia a particiones.

3. Tipos de bases de datos NoSQL

3.1 Clave-valor

Estructura. Una tabla hash gigante y distribuida: una clave única y un valor opaco para el gestor (una cadena, un número, un binario o una estructura simple). El acceso por clave es prácticamente instantáneo; sin la clave, no hay forma eficiente de buscar.

Producto de referencia: Valkey (bifurcación libre de Redis, usada en la práctica 10.6), Redis, Amazon DynamoDB, etcd.

SET  sesion:abc123 "id_alumno=1;rol=ALUMNO"
EXPIRE sesion:abc123 1800        # caducidad automática en 30 minutos
INCR visitas:portal              # incremento atómico

Casos de uso reales: sesiones de usuario de la secretaría virtual (con caducidad automática); caché de las consultas más pesadas de Oracle, para no repetirlas; contadores y limitadores de peticiones; colas de trabajos.

Límites: no se puede consultar por el contenido del valor («dame todos los alumnos de 1DAM» es imposible salvo que esa lista se haya guardado como otra clave); no hay relaciones ni informes; la mayoría de estos sistemas mantienen los datos en memoria, con persistencia opcional, de modo que no son el almacén principal de información que no se puede perder.

3.2 Documental

Estructura. Colecciones de documentos autodescriptivos con formato JSON (BSON en MongoDB), con campos anidados y arrays. Admite consultas por cualquier campo, índices secundarios y agregaciones.

Producto de referencia: MongoDB (el que usaremos), CouchDB, Amazon DocumentDB, Elasticsearch (orientado a búsqueda).

Casos de uso reales: catálogos de productos con características variables (práctica 10.5); expedientes, historiales y contenidos editoriales donde cada ficha tiene secciones distintas; perfiles de usuario y configuraciones de aplicación.

Límites: sin integridad referencial declarativa entre colecciones; las consultas que cruzan muchas colecciones son incómodas y más lentas que un JOIN bien indexado; el documento tiene un límite de 16 MB, lo que prohíbe incrustar colecciones que crecen sin fin (como las opiniones de un producto).

3.3 Columnar (familias de columnas)

Estructura. Filas identificadas por una clave de partición que se agrupan en familias de columnas; cada fila puede tener columnas distintas y el almacenamiento está orientado a la columna, lo que comprime muy bien y permite leer un rango enorme de una sola columna con muy poca entrada/salida. Las escrituras se añaden al final (append), por lo que son muy rápidas.

Producto de referencia: Apache Cassandra, HBase, ScyllaDB; en analítica, las bases de datos columnares puras como ClickHouse.

Casos de uso reales: series temporales y telemetría (sensores de CO₂ de las aulas midiendo cada 30 segundos, práctica 10.7); registros de actividad y auditoría a gran escala; sistemas de mensajería con escrituras masivas.

Límites: el modelo se diseña a partir de las consultas que se van a ejecutar, y consultar por un criterio no previsto puede ser inviable; no hay JOIN; los agregados y la unicidad se gestionan con mucho cuidado.

3.4 Grafos

Estructura. Nodos (entidades) y aristas (relaciones), ambos con propiedades. La relación es un elemento de primera clase, no una clave ajena: recorrer «amigos de mis amigos» no multiplica el coste como lo haría una cadena de JOIN.

Producto de referencia: Neo4j (con el lenguaje Cypher, práctica 10.6), ArangoDB, Amazon Neptune.

MATCH (a:Alumno {nombre:'Adrián'})-[:MATRICULADO]->(m)<-[:MATRICULADO]-(c:Alumno)
RETURN DISTINCT c.nombre

Casos de uso reales: redes sociales y recomendaciones («quien estudió lo mismo que tú eligió…»); cálculo de rutas y logística; detección de fraude por patrones de relación; análisis de dependencias y de árboles de permisos.

Límites: mal ajuste para agregaciones masivas sobre todos los datos o para informes tabulares; el escalado horizontal es más difícil, porque partir un grafo rompe precisamente las aristas que se quieren recorrer.

3.5 Tabla resumen

TipoUnidad de datosSe consulta porProductoBrilla enSufre en
Clave-valorPar clave → valorLa clave, y poco másValkey / RedisCaché, sesiones, contadoresCualquier búsqueda por contenido
DocumentalDocumento JSON/BSONCualquier campo, con índicesMongoDBAgregados autocontenidos, esquemas variablesCruces entre muchas colecciones
ColumnarFila ancha por clave de particiónClave de partición y rangos de la clave de ordenaciónCassandraEscrituras masivas, series temporalesConsultas no previstas, JOIN
GrafosNodo y aristaRecorridos desde un nodoNeo4jRelaciones profundas y variablesInformes agregados, escalado horizontal
(referencia) RelacionalFila de una tablaCualquier columna, con SQLOracle 26aiIntegridad, consultas ad hoc, informesEsquemas muy variables, escalado extremo

3.6 El mismo dato en los cinco modelos

Para ver la diferencia, tomemos un dato real de EduGest: Adrián Ferri Baeza (NIA 10450037) está matriculado en 0484 Bases de datos con un 4,75 y en 0485 Programación con un 7,25.

Relacional (lo que ya tienes en Oracle): el dato vive en tres tablas y se recompone con JOIN.

ALUMNOMATRICULA
ID_ALUMNONIANOMBREID_ALUMNOID_MODULONOTA_FINAL
110450037Adrián124.75
137.25

Documental: un único documento autocontenido.

{ _id: 1, nia: "10450037", nombre: "Adrián",
  matriculas: [ { modulo: "0484", nota: 4.75 }, { modulo: "0485", nota: 7.25 } ] }

Clave-valor: una clave por dato que se quiera recuperar.

alumno:1:nombre          -> "Adrián"
alumno:1:nota:0484       -> "4.75"
alumno:1:nota:0485       -> "7.25"

Columnar: una fila ancha, con el alumno como clave de partición y el módulo como clave de ordenación.

clave de partición04840485
alumno#1nota=4.75nota=7.25

Grafos: dos nodos y una arista con propiedades por cada matrícula.

(Alumno {nia:'10450037'})-[:MATRICULADO {nota:4.75}]->(Modulo {codigo:'0484'})
(Alumno {nia:'10450037'})-[:MATRICULADO {nota:7.25}]->(Modulo {codigo:'0485'})

El dato es el mismo; lo que cambia es qué pregunta resulta barata. El modelo relacional contesta bien a casi cualquier pregunta. El documental contesta instantáneamente a «dame el expediente de este alumno». El de clave-valor solo a «dame este valor concreto». El columnar a «dame todas las notas de este alumno por orden de módulo». El de grafos a «qué alumnos comparten módulos con este». Elegir el modelo es elegir qué preguntas quieres que sean baratas.


Curiosidad ¿Por qué «Mongo»?

El nombre de MongoDB viene de humongous («enorme»), en referencia a su pensamiento para manejar grandes volúmenes de datos.

4. MongoDB: elementos y herramientas

4.1 El servidor y sus herramientas cliente

ComponenteQué esEquivalente aproximado en Oracle
mongodEl proceso servidor; escucha en el puerto 27017 por defectoLa instancia de base de datos
mongoshEl shell oficial: un intérprete de JavaScript con acceso a la base de datosSQL*Plus / SQLcl
MongoDB CompassHerramienta gráfica: explorar colecciones, analizar el esquema real, construir agregaciones paso a paso, ver planes de ejecuciónSQL Developer
Database ToolsUtilidades de línea de órdenes: mongodump, mongorestore, mongoimport, mongoexportData Pump, SQL*Loader
DriversBibliotecas oficiales para Java, Python, C#, Node.js…JDBC / OCI

Como mongosh es JavaScript, en él son válidas las variables, los bucles y las funciones, lo que resulta muy cómodo para automatizar:

// Scripting en mongosh: ejemplo de uso del lenguaje del cliente
for (const g of ["1DAM", "1DAW", "1ASIR"]) {
  print(g + ": " + db.expedientes.countDocuments({ "grupo.codigo": g }))
}

mongosh sustituyó al antiguo shell mongo (retirado en la versión 6.0). Si encuentras apuntes o respuestas en foros que usan mongo como orden, son anteriores a 2021 y es probable que también usen métodos obsoletos (§5).

4.2 Base de datos, colección, documento y campo

La jerarquía de elementos tiene cuatro niveles, y se corresponde casi término a término con la del modelo relacional:

flowchart LR
    S[Servidor mongod] --> D[(Base de datos<br/>edugest)]
    D --> C1[Colección<br/>expedientes]
    D --> C2[Colección<br/>profesores]
    D --> C3[Colección<br/>ciclos]
    C1 --> DOC[Documento<br/>un expediente]
    DOC --> F[Campos<br/>nia, nombre, matriculas…]
Modelo relacionalMongoDBMatiz importante
Base de datos / esquemaBase de datosEn MongoDB la base de datos se crea al escribir el primer documento
TablaColecciónLa colección no impone columnas ni tipos
Fila (tupla)DocumentoUn documento puede contener arrays y otros documentos: no es plano
Columna (atributo)CampoCada documento tiene los campos que tiene; no hay «columnas de la colección»
Clave primariaCampo _idObligatorio, único, indexado y no modificable; si no lo pones, lo genera el servidor
Clave ajenaReferencia manual (_id de otro documento)No existe la integridad referencial declarativa
JOIN$lookup o documento embebidoLo normal es evitar el cruce incrustando los datos
ÍndiceÍndiceMismo concepto y utilidad que en la UD05 y la UD07
VistaVista (db.createView)De solo lectura, definida con una tubería de agregación

4.3 BSON y los tipos de datos

Los documentos se escriben como JSON, pero MongoDB los almacena en BSON (Binary JSON), una codificación binaria que añade tipos que JSON no tiene y que permite recorrer el documento sin analizarlo entero.

Tipo BSONCómo se escribe en mongoshEquivalente en OracleCuidado
Double7.25NUMBER / BINARY_DOUBLEEs el tipo por defecto de todo número con decimales: coma flotante
Int32 / Int64NumberInt(3) / NumberLong(…)NUMBER(p)Un literal entero como 3 se guarda como Int32 desde mongosh
Decimal128NumberDecimal("7.25")NUMBER(p,s)El tipo correcto para importes y notas oficiales: decimal exacto
String"Adrián"VARCHAR2Siempre UTF-8
Booleantrue / falseBOOLEAN (desde 23ai) o CHAR(1)
DateISODate("2026-05-20")DATE / TIMESTAMPMilisegundos desde 1970 en UTC; no guardes fechas como texto
ObjectIdObjectId("6710f1…")—12 bytes: marca de tiempo + aleatorio + contador. Valor por defecto de _id
Array[1, 2, 3]Sin equivalente directo (tabla hija)El operador de consulta se aplica a cada elemento
Object{ email: "…", telefono: "…" }Sin equivalente directo (columnas o tabla hija)Se consulta con notación de punto
NullnullNULLnull y «campo ausente» no son lo mismo

{ nota: 7.25 } se guarda como coma flotante de doble precisión, igual que un BINARY_DOUBLE de Oracle: 0.1 + 0.2 no da exactamente 0.3. Para notas, importes o cualquier valor que se sume y se compare por igualdad, usa NumberDecimal("7.25"), que es el equivalente del NUMBER(4,2) de EduGest. Con 32 expedientes no lo notarás; en una contabilidad, sí.

El campo _id

  • Es la clave primaria del documento. Si no se indica, el servidor genera un ObjectId.
  • Tiene siempre un índice único que no se puede eliminar.
  • Es inmutable: no se puede cambiar con $set; habría que borrar el documento e insertarlo de nuevo.
  • Puede ser de cualquier tipo escalar. En la versión documental de EduGest usamos _id: 1 … 32, los mismos valores que id_alumno en Oracle, para poder comparar los dos sistemas fila a documento.

4.4 Primeras órdenes

show dbs                          // bases de datos con datos (las vacías no aparecen)
use edugest                       // selecciona la base de datos; la crea si no existe
db                                // muestra la base de datos activa
show collections                  // colecciones de la base activa
db.getCollectionNames()           // lo mismo, como array de JavaScript
db.stats()                        // tamaño, número de colecciones, de objetos e índices
db.expedientes.countDocuments()   // 32
db.expedientes.getIndexes()       // solo el índice de _id, creado automáticamente
edugest> show collections
ciclos
expedientes
profesores
edugest> db.expedientes.countDocuments()
32

use edugest no crea nada: la base de datos y la colección aparecen con la primera escritura. Por eso un error de tecleo (use edugets) no da error y los datos acaban en una base de datos nueva. Comprueba siempre con db antes de insertar, y con show dbs después.

En MongoDB 8.0, db.collection.stats() sigue disponible en mongosh, pero la forma recomendada de obtener estadísticas de una colección es la etapa de agregación $collStats:

db.expedientes.aggregate([ { $collStats: { storageStats: {} } } ])

4.5 Convención de nombres

No hay una norma oficial, pero el ecosistema sigue convenios muy estables que conviene respetar:

ElementoConvenioEjemploObservaciones
Base de datosminúsculas, sin espaciosedugestMáximo 63 caracteres; distingue mayúsculas
Colecciónplural, minúsculasexpedientesEvita $ y nombres que empiecen por system.
CampocamelCasefechaNacimientoEvita el punto y el $ inicial: complican las rutas de consulta
Referencia a otro documentonombre + IdcicloIdAsí se ve que es una referencia, no un dato embebido

En Oracle los identificadores no distinguen mayúsculas (ALUMNO y alumno son la misma tabla); en MongoDB sí: Expedientes y expedientes serían dos colecciones distintas, y db.expedientes.find({ Nombre: "Adrián" }) no devuelve nada si el campo se llama nombre. Es una de las causas más frecuentes de «mi consulta no devuelve nada».

4.6 La colección expedientes de EduGest

Toda la unidad trabaja sobre la versión documental de EduGest que carga el script edugest_mongo.js: tres colecciones (expedientes, profesores y ciclos) obtenidas de las mismas tablas de Oracle. Este es el documento del alumno 1, completo: MongoDB 8.0

db.expedientes.findOne({ _id: 1 })
{
  _id: 1,
  nia: '10450037',
  dni: '55568986C',
  nombre: 'Adrián',
  apellidos: 'Ferri Baeza',
  fechaNacimiento: ISODate('2006-10-18T00:00:00.000Z'),
  localidad: 'Alicante',
  contacto: { email: 'adrianferri1@alu.edugest.es', telefono: '610608088' },
  grupo: { codigo: '1DAM', ciclo: 'DAM', curso: 1, turno: 'M' },
  matriculas: [
    { curso: '2025-26', convocatoria: 1, nota: 8.75,
      modulo: { codigo: '0483', nombre: 'Sistemas informáticos', ciclo: 'DAM', cursoCiclo: 1, horas: 160 } },
    { curso: '2025-26', convocatoria: 1, nota: 4.75,
      modulo: { codigo: '0484', nombre: 'Bases de datos', ciclo: 'DAM', cursoCiclo: 1, horas: 160 } },
    { curso: '2025-26', convocatoria: 1, nota: 7.25,
      modulo: { codigo: '0485', nombre: 'Programación', ciclo: 'DAM', cursoCiclo: 1, horas: 256 },
      faltas: [ { fecha: ISODate('2026-02-04'), horas: 3, justificada: true },
                { fecha: ISODate('2026-03-19'), horas: 3, justificada: false } ] },
    { curso: '2025-26', convocatoria: 1, nota: 4.75,
      modulo: { codigo: '0487', nombre: 'Entornos de desarrollo', ciclo: 'DAM', cursoCiclo: 1, horas: 96 },
      faltas: [ { fecha: ISODate('2026-05-08'), horas: 2, justificada: true } ] },
    { curso: '2025-26', convocatoria: 1, nota: 6.75,
      modulo: { codigo: '0373', nombre: 'Lenguajes de marcas y sistemas de gestión de información',
                ciclo: 'DAM', cursoCiclo: 1, horas: 128 } }
  ]
}

Tres decisiones de esta carga conviene entenderlas desde el principio, porque explican muchos resultados:

  1. Los valores ausentes no se guardan. La exportación usa ABSENT ON NULL, así que un alumno sin DNI no tiene campo dni (ocurre en 4 de los 32 expedientes), un alumno sin grupo no tiene grupo (3 expedientes) y una matrícula sin calificar no tiene nota (6 matrículas). No hay null: hay ausencia.
  2. El array matriculas está siempre, aunque esté vacío ([]): los alumnos 30, 31 y 32 no tienen matrícula, igual que en Oracle no tienen filas en MATRICULA.
  3. Hay dos campos de nombre parecido y significado distinto: matriculas[].curso es el curso académico ('2025-26', el CURSO_ACADEMICO de la tabla MATRICULA) y matriculas[].modulo.cursoCiclo es el curso del ciclo al que pertenece el módulo (1 o 2). Como en un documento no hay cabecera de tabla que aclare el significado, los nombres de campo deben ser más explícitos que los de columna.

Traductor SQL ↔ MongoDB

Antes de escribir una sola orden, usa el laboratorio siguiente. Trabaja sobre una colección reducida de 6 expedientes (los alumnos 1, 2, 9, 14, 21 y 25, con sus matrículas reales simplificadas) y, para cada operación que elijas, muestra tres cosas a la vez: la orden de mongosh, la consulta SQL de Oracle que haría lo mismo y el resultado. El botón del final conmuta entre ver el dato de Adrián como dos tablas relacionales o como un documento.

Fíjate especialmente en dos operaciones: «La trampa del array» y «Reemplazar el documento completo». Las dos muestran comportamientos que no tienen paralelo en SQL y que son fuente habitual de errores.

Interactivo Traductor SQL ↔ MongoDB MongoDB 8.0 · colección expedientes con 6 documentos · simulación didáctica, no es un SGBD

Localidades del $in

1 · Orden de mongosh

2 · SQL equivalente (Oracle)

3 · Resultado

El laboratorio es una simulación didáctica escrita en JavaScript: reproduce el comportamiento y los mensajes de MongoDB 8.0 sobre seis documentos, pero no es un servidor. Los resultados de la unidad y de las prácticas se obtienen ejecutando las órdenes en el MongoDB que instalaste en la práctica 10.1.

Autoevaluación 2 preguntas
1 ¿Cuál de estas afirmaciones sobre _id es correcta?
2 En la colección expedientes, 4 documentos no tienen el campo dni. ¿Qué habría en Oracle?

Sesión 3 1 h Teoría · Operaciones CRUD, subdocumentos, arrays y agregaciones

5. Operaciones CRUD

CRUD son las cuatro operaciones básicas sobre datos: Create, Read, Update, Delete. En el modelo relacional son INSERT, SELECT, UPDATE y DELETE; en MongoDB, métodos de la colección. La tabla general de equivalencias es esta:

OperaciónMongoDB 8.0Oracle 26ai
CrearinsertOne, insertManyINSERT
Leerfind, findOne, countDocuments, aggregateSELECT
ActualizarupdateOne, updateMany, replaceOne, findOneAndUpdateUPDATE, MERGE
BorrardeleteOne, deleteManyDELETE
Vaciar / eliminar la estructuradb.col.drop()TRUNCATE TABLE, DROP TABLE
Confirmar— (atómico por documento)COMMIT / ROLLBACK

Los métodos insert(), update(), remove(), save() y count() que verás en tutoriales antiguos están obsoletos; save() ya no existe en mongosh. No los uses: no distinguen entre afectar a uno o a muchos documentos, que es justamente el error más caro. Usa siempre los métodos con sufijo One o Many, de modo que la propia orden diga cuántos documentos puede tocar.

5.1 Crear: insertOne e insertMany

Finalidad. Añadir documentos a una colección (creándola si no existe).

Sintaxis.

db.<coleccion>.insertOne( <documento> )
db.<coleccion>.insertMany( [ <documento>, <documento>, … ], { ordered: true } )

Componentes. El documento es un objeto JavaScript; si no lleva _id, el servidor añade un ObjectId. En insertMany, la opción ordered: true (valor por defecto) detiene la inserción en el primer error; con ordered: false continúa con el resto y comunica al final los que han fallado.

Ejemplo sencillo. Matriculamos a una alumna nueva en 1DAM: MongoDB 8.0

db.expedientes.insertOne({
  _id: 33,
  nia: "10451221",
  dni: "48219376P",
  nombre: "Marina",
  apellidos: "López Server",
  fechaNacimiento: ISODate("2006-03-14"),
  localidad: "Alicante",
  contacto: { email: "marinalopez33@alu.edugest.es", telefono: "612004455" },
  grupo: { codigo: "1DAM", ciclo: "DAM", curso: 1, turno: "M" },
  matriculas: []
})
{ acknowledged: true, insertedId: 33 }

Equivalente en Oracle. Hacen falta dos sentencias y una COMMIT, porque la información está repartida en dos tablas y el contacto en columnas: Oracle 26ai

INSERT INTO alumno (id_alumno, nia, dni, nombre, apellidos, fecha_nacimiento,
                    email, telefono, localidad, cod_grupo)
VALUES (33, '10451221', '48219376P', 'Marina', 'López Server', DATE '2006-03-14',
        'marinalopez33@alu.edugest.es', '612004455', 'Alicante', '1DAM');
COMMIT;

Ejemplo aplicado. Dos alumnas más, una de ellas sin _id:

db.expedientes.insertMany([
  { _id: 34, nia: "10451258", nombre: "Nadia", apellidos: "Ruiz Pastor",
    fechaNacimiento: ISODate("2005-09-30"), localidad: "Elche", matriculas: [] },
  {        nia: "10451295", nombre: "Omar",  apellidos: "Sellés Giner",
    fechaNacimiento: ISODate("2006-01-22"), localidad: "Alicante", matriculas: [] }
])
{
  acknowledged: true,
  insertedIds: { '0': 34, '1': ObjectId('6714a3c9f1b2e45d7c0a9e01') }
}

Resultado esperado. db.expedientes.countDocuments() devuelve ahora 35. Observa que el segundo documento recibió un ObjectId generado por el servidor: una clave primaria artificial, como la columna identidad de la UD05, pero generada en el cliente o en el servidor sin necesidad de secuencia.

Errores habituales.

MensajeCausaEquivalente en Oracle
E11000 duplicate key error collection: edugest.expedientes index: _id_ dup key: { _id: 33 }Ese _id ya existeORA-00001: restricción única violada
MongoBulkWriteError con insertedCount menor que el esperadoUn documento del array ha fallado y ordered: true ha parado ahíEquivale a parar el INSERT ... SELECT en la primera violación
Ningún error, pero el documento no está donde esperabasOlvidaste use edugest y lo has escrito en la base testNo ocurre: en Oracle el esquema está fijado por la conexión

MongoDB ha aceptado a Nadia y a Omar sin DNI, sin grupo y sin contacto. Oracle habría rechazado la inserción si faltara una columna NOT NULL y la habría rechazado también si cod_grupo apuntara a un grupo inexistente (ORA-02291). Esa comprobación no desaparece: la tiene que hacer la aplicación, o un validador (§9).

5.2 Leer: find y findOne

Finalidad. Recuperar documentos que cumplan un filtro, devolviendo solo los campos que interesan.

Sintaxis.

db.<coleccion>.find( <filtro>, <proyección> )      // devuelve un cursor
db.<coleccion>.findOne( <filtro>, <proyección> )   // devuelve un documento o null

El filtro es el WHERE y la proyección es la lista de columnas del SELECT: 1 incluye el campo, 0 lo excluye, y _id se devuelve siempre salvo que se pida _id: 0.

// Las dos partes de un SELECT, en el mismo orden conceptual
db.expedientes.find(
  { localidad: "Mutxamel" },                           // WHERE
  { _id: 0, nombre: 1, apellidos: 1, localidad: 1 }    // SELECT
).sort({ apellidos: 1 })                               // ORDER BY
SELECT nombre, apellidos, localidad
FROM   alumno
WHERE  localidad = 'Mutxamel'
ORDER  BY apellidos;
NOMBREAPELLIDOSLOCALIDAD
MartinaAlemany VidalMutxamel
SaraAmorós GuillemMutxamel
IreneBelda IborraMutxamel
HugoBrotons IborraMutxamel
ÁlexTomás VidalMutxamel
AlbaTorregrosa PlanellesMutxamel

6 filas (6 documentos)

Operadores de comparación

MongoDBSignificadoOracleEjemplo sobre EduGestDocumentos
$eqIgual (implícito al escribir campo: valor)={ localidad: "Alicante" }13
$neDistinto<>{ localidad: { $ne: "Alicante" } }19
$gt, $gteMayor, mayor o igual>, >={ "matriculas.nota": { $gte: 9 } }6
$lt, $lteMenor, menor o igual<, <={ fechaNacimiento: { $lt: ISODate("2005-01-01") } }10
$inEstá en la listaIN{ localidad: { $in: ["Elche", "El Campello"] } }6
$ninNo está en la listaNOT IN{ localidad: { $nin: ["Elche", "El Campello"] } }26

$ne y $nin sí devuelven los documentos en los que el campo no existe, mientras que en Oracle WHERE localidad <> 'Alicante' nunca devuelve las filas con localidad a NULL (lógica de tres valores, UD06). Es una diferencia de criterio, no un error de ninguno de los dos: en MongoDB «no es Alicante» incluye «no se sabe».

Operadores lógicos

MongoDBOracleNota
$andANDImplícito: las condiciones de un mismo objeto filtro se combinan con AND
$orORNecesita un array: { $or: [ {…}, {…} ] }
$notNOTSe aplica a un operador, no a una condición completa
$norNOT (… OR …)Ninguna de las condiciones se cumple
// Alumnado de 1DAM o de 1DAW nacido en 2006 o después
db.expedientes.find({
  $and: [
    { "grupo.codigo": { $in: ["1DAM", "1DAW"] } },
    { fechaNacimiento: { $gte: ISODate("2006-01-01") } }
  ]
}, { _id: 1, nombre: 1, "grupo.codigo": 1 })
SELECT id_alumno, nombre, cod_grupo
FROM   alumno
WHERE  cod_grupo IN ('1DAM', '1DAW')
AND    fecha_nacimiento >= DATE '2006-01-01';

Existencia y tipo

Con esquema flexible aparecen dos preguntas que en SQL no tienen sentido: ¿existe el campo? y ¿de qué tipo es?

db.expedientes.find({ "contacto.telefono": { $exists: false } }, { _id: 1 })   // 4, 10, 16, 22, 28
db.expedientes.find({ dni: { $exists: false } }, { _id: 1 })                   // 5, 14, 23, 32
db.expedientes.countDocuments({ "matriculas.nota": { $type: "double" } })      // 29
-- El equivalente relacional de "el campo no existe" es "la columna es nula"
SELECT id_alumno FROM alumno WHERE telefono IS NULL;   -- 4, 10, 16, 22, 28

5 documentos y 5 filas

$type comprueba el tipo BSON real de cada valor: es la herramienta para auditar una colección en la que alguien ha podido guardar nota: "7" en lugar de nota: 7. En Oracle esa comprobación es innecesaria porque el tipo lo garantiza la columna.

Expresiones regulares

db.expedientes.find({ apellidos: /^Iborra/ }, { _id: 1, apellidos: 1 })
// equivalente explícito: { apellidos: { $regex: "^Iborra" } }
_idapellidos
2Iborra Ferri
17Iborra Alemany
25Iborra Torregrosa
30Iborra Valero

4 documentos

SELECT id_alumno, apellidos FROM alumno WHERE apellidos LIKE 'Iborra%';

Una expresión regular anclada al principio (/^Iborra/) puede aprovechar un índice, igual que LIKE 'Iborra%' en Oracle. Si empieza por comodín (/Iborra/ o LIKE '%Iborra%'), los dos sistemas tienen que recorrer toda la colección o tabla. El criterio de la UD07 sobre índices y comodines se aplica aquí sin cambios.

Ordenar, limitar, saltar y contar

MongoDBOracleSignificado
.sort({ campo: 1 }) / -1ORDER BY campo ASC / DESCOrdenación
.limit(n)FETCH FIRST n ROWS ONLYPrimeras n
.skip(n).limit(m)OFFSET n ROWS FETCH NEXT m ROWS ONLYPaginación
.countDocuments(filtro)SELECT COUNT(*) … WHERERecuento exacto
.estimatedDocumentCount()— (se parece a leer las estadísticas)Recuento aproximado e inmediato
// Los tres alumnos más jóvenes
db.expedientes.find({}, { _id: 0, nombre: 1, apellidos: 1, fechaNacimiento: 1 })
  .sort({ fechaNacimiento: -1 }).limit(3)
SELECT nombre, apellidos, fecha_nacimiento
FROM   alumno
ORDER  BY fecha_nacimiento DESC
FETCH FIRST 3 ROWS ONLY;
NOMBREAPELLIDOSFECHA_NACIMIENTO
ManuelSoriano Domènech25/12/2006
AlbaTorregrosa Planelles21/12/2006
VíctorTomás Guillem06/12/2006

3 filas (3 documentos)

Sin sort() el orden de los documentos no está garantizado, exactamente igual que sin ORDER BY en SQL (UD06). Y una paginación con skip() grande es cara en los dos sistemas: el servidor tiene que recorrer y descartar todo lo que salta.

5.3 Actualizar: updateOne, updateMany y $set

Finalidad. Modificar campos de documentos existentes.

Sintaxis.

db.<coleccion>.updateOne ( <filtro>, { <operador>: { <campo>: <valor> } }, <opciones> )
db.<coleccion>.updateMany( <filtro>, { <operador>: { <campo>: <valor> } }, <opciones> )

Componentes. El segundo argumento debe empezar por un operador de actualización. Los más usados:

OperadorQué haceEquivalente en Oracle
$setAsigna un valor (crea el campo si no existe)SET columna = valor
$unsetElimina el campo del documentoSET columna = NULL (parecido, no igual)
$incSuma (o resta, con negativo)SET columna = columna + n
$mulMultiplicaSET columna = columna * n
$min, $maxAsigna solo si el nuevo valor es menor / mayorSET columna = LEAST(columna, n)
$renameCambia el nombre del campoALTER TABLE … RENAME COLUMN (¡es DDL!)
$currentDatePone la fecha y hora actualesSET columna = SYSDATE
$setOnInsertAsigna solo si el upsert acaba insertandoRama WHEN NOT MATCHED de MERGE

Ejemplo sencillo. Añadir el teléfono que falta a Paula (_id: 4): MongoDB 8.0

db.expedientes.updateOne(
  { _id: 4 },
  { $set: { "contacto.telefono": "612345678" } }
)
{ acknowledged: true, insertedId: null, matchedCount: 1,
  modifiedCount: 1, upsertedCount: 0 }
UPDATE alumno SET telefono = '612345678' WHERE id_alumno = 4;
COMMIT;

matchedCount dice cuántos documentos ha encontrado el filtro y modifiedCount cuántos ha cambiado. Si asignas el valor que ya estaba, verás matchedCount: 1, modifiedCount: 0. Oracle informa de una sola cifra («1 fila actualizada»), aunque el valor no cambie.

Ejemplo aplicado. Varias operaciones encadenadas sobre el curso:

// a) Un dato común a todo un grupo: aula de referencia
db.expedientes.updateMany(
  { "grupo.codigo": "1DAM" },
  { $set: { aulaReferencia: "I-12" }, $currentDate: { actualizado: true } }
)
// { matchedCount: 7, modifiedCount: 7 }

// b) Subir una convocatoria a una matrícula concreta y dejar constancia
db.expedientes.updateOne(
  { _id: 1, "matriculas.modulo.codigo": "0484" },
  { $inc: { "matriculas.$.convocatoria": 1 } }
)

// c) Retirar un campo que ya no se usa, de toda la colección
db.expedientes.updateMany({}, { $unset: { aulaReferencia: "" } })
// { matchedCount: 32, modifiedCount: 7 }   <- solo los 7 que lo tenían

$unset elimina el campo; no lo deja a null. Es la diferencia con UPDATE … SET telefono = NULL de Oracle, donde la columna sigue existiendo. Después de un $unset, ese documento responde true a { campo: { $exists: false } }.

upsert: actualizar o insertar

db.expedientes.updateOne(
  { nia: "10451332" },
  { $set: { nombre: "Lara", apellidos: "Ortuño Mas" },
    $setOnInsert: { matriculas: [] } },
  { upsert: true }
)
{ acknowledged: true, insertedId: ObjectId('6714a4...'), matchedCount: 0,
  modifiedCount: 0, upsertedCount: 1 }

Es el equivalente del MERGE de Oracle: si el filtro encuentra un documento lo actualiza, y si no, inserta uno nuevo con los campos del filtro y de la actualización.

MERGE INTO alumno a
USING (SELECT '10451332' AS nia FROM dual) s
ON (a.nia = s.nia)
WHEN MATCHED THEN UPDATE SET a.nombre = 'Lara', a.apellidos = 'Ortuño Mas'
WHEN NOT MATCHED THEN INSERT (nia, nombre, apellidos) VALUES (s.nia, 'Lara', 'Ortuño Mas');

replaceOne: la diferencia conceptual más peligrosa

replaceOne sustituye el documento entero por el que se le pasa, conservando únicamente _id:

db.expedientes.replaceOne({ _id: 1 }, { nombre: "Adrián", apellidos: "Ferri Baeza" })
// Lo que queda en la colección:
{ _id: 1, nombre: 'Adrián', apellidos: 'Ferri Baeza' }
// Han desaparecido nia, dni, fechaNacimiento, localidad, contacto, grupo y las 5 matrículas

No hay nada equivalente en SQL: UPDATE solo toca las columnas que se nombran y las demás se quedan como estaban. Lo más parecido sería DELETE seguido de INSERT, con el agravante de que en EduGest el DELETE del alumno arrastraría sus matrículas (ON DELETE CASCADE). Usa siempre $set; reserva replaceOne para cuando quieras reescribir un documento completo a propósito.

Afortunadamente, el controlador protege del olvido más común:

db.expedientes.updateOne({ _id: 1 }, { nombre: "Adrián" })
// MongoInvalidArgumentError: Update document requires atomic operators

Ese mensaje significa «falta $set». El antiguo método update(), retirado de mongosh, sí reemplazaba el documento silenciosamente en esta situación: es la razón de que en código viejo se encuentren documentos mutilados.

5.4 Borrar: deleteOne y deleteMany

db.expedientes.deleteOne({ _id: 33 })                  // { acknowledged: true, deletedCount: 1 }
db.expedientes.deleteMany({ nia: { $in: ["10451258", "10451295"] } })
db.expedientes.deleteMany({ grupo: { $exists: false } })   // los 3 alumnos sin grupo
DELETE FROM alumno WHERE id_alumno = 33;
DELETE FROM alumno WHERE nia IN ('10451258', '10451295');
DELETE FROM alumno WHERE cod_grupo IS NULL;
COMMIT;
OperaciónMongoDBOracleDiferencia clave
Borrar documentos/filas que cumplen un filtrodeleteMany(filtro)DELETE … WHEREEn Oracle se puede deshacer con ROLLBACK mientras no haya COMMIT
Vaciar la colección/tabladeleteMany({})TRUNCATE TABLE o DELETE sin WHERETRUNCATE es DDL, no se deshace y libera el espacio
Eliminar la estructuradb.expedientes.drop()DROP TABLEdrop() elimina también los índices

deleteMany({}) borra toda la colección sin preguntar y sin ROLLBACK posible: en MongoDB cada operación se confirma sola. Antes de borrar, ejecuta el mismo filtro con countDocuments() y comprueba el número. En Oracle un DELETE mal escrito se puede deshacer; aquí, no.

Y en el modelo documental no hay ON DELETE CASCADE: si borras el documento de un ciclo al que apuntan 24 módulos por referencia, las referencias quedan huérfanas y nadie avisa. En Oracle, ORA-02292 lo habría impedido.

¿Qué devuelve db.expedientes.deleteMany({ "grupo.codigo": "1DAM" }) si antes has hecho un $unset del grupo?

{ acknowledged: true, deletedCount: 0 }. El filtro no encuentra nada porque el campo ya no existe, así que no borra nada: es un resultado correcto, no un error. Esta es la razón por la que conviene comprobar primero con countDocuments() que el filtro selecciona lo que crees. Un filtro mal escrito en un deleteMany puede borrar cero documentos… o todos.


6. Consultas sobre subdocumentos y arrays

Aquí está la diferencia real entre consultar una tabla y consultar un documento. Un documento tiene estructura interna, y los filtros se aplican dentro de ella.

6.1 Documentos embebidos y notación de punto

Para llegar a un campo anidado se usa la notación de punto, siempre entre comillas (porque el punto no es válido en un nombre de propiedad de JavaScript):

db.expedientes.find({ "grupo.codigo": "1DAW" }, { _id: 0, nombre: 1, "grupo.turno": 1 })
db.expedientes.countDocuments({ "grupo.ciclo": "DAM" })                  // 13
db.expedientes.countDocuments({ "contacto.email": { $exists: true } })   // 29
SELECT COUNT(*) FROM alumno a JOIN grupo g ON g.cod_grupo = a.cod_grupo
WHERE  g.cod_ciclo = 'DAM';     -- 13

Buscar el subdocumento completo no es lo mismo que buscar un campo suyo:

db.expedientes.find({ grupo: { codigo: "1DAM" } })        // 0 documentos
db.expedientes.find({ "grupo.codigo": "1DAM" })           // 7 documentos

La primera forma exige que el subdocumento sea exactamente igual, con los mismos campos, los mismos valores y en el mismo orden. Como los grupo de EduGest tienen cuatro campos, no coincide ninguno. Es un error muy frecuente al empezar.

6.2 Modificar arrays

OperadorQué haceEquivalente relacional
$pushAñade un elemento al final (admite duplicados)INSERT en la tabla hija
$addToSetAñade solo si no existe yaINSERT precedido de una comprobación, o una restricción UNIQUE
$pullElimina los elementos que cumplan una condiciónDELETE … WHERE en la tabla hija
$popElimina el último (1) o el primero (-1) elementoSin equivalente: en SQL las filas no tienen orden
$each, $slice, $sortModificadores de $push: varios elementos, recortar y ordenar el array—
// Registrar una falta de asistencia en la matrícula de 0484 de Adrián
db.expedientes.updateOne(
  { _id: 1, "matriculas.modulo.codigo": "0484" },
  { $push: { "matriculas.$.faltas": { fecha: ISODate("2026-05-20"), horas: 2, justificada: false } } }
)

// Mantener solo las 5 faltas más recientes de esa matrícula
db.expedientes.updateOne(
  { _id: 1, "matriculas.modulo.codigo": "0484" },
  { $push: { "matriculas.$.faltas": { $each: [], $sort: { fecha: -1 }, $slice: 5 } } }
)

// Quitar las faltas justificadas de todas las matrículas de un alumno
db.expedientes.updateOne({ _id: 1 }, { $pull: { "matriculas.$[].faltas": { justificada: true } } })
-- En Oracle, cada falta es una fila de otra tabla
INSERT INTO falta_asistencia (id_matricula, fecha, horas, justificada)
VALUES (10002, DATE '2026-05-20', 2, 'N');

DELETE FROM falta_asistencia
WHERE  justificada = 'S'
AND    id_matricula IN (SELECT id_matricula FROM matricula WHERE id_alumno = 1);
COMMIT;

Observa lo que ha ocurrido: en el modelo documental, añadir una falta y leer el expediente completo son operaciones sobre un solo documento, atómicas y sin JOIN. En el relacional son dos tablas y una clave ajena, pero a cambio la falta tiene identidad propia, se puede consultar directamente («todas las faltas del 20 de mayo de todo el centro») y el SGBD garantiza que no apunte a una matrícula inexistente.

6.3 Consultar arrays

Al aplicar un filtro a un campo que es un array, MongoDB lo compara con cada elemento: el documento coincide si algún elemento cumple la condición.

// Alumnado con alguna matrícula del módulo 0484
db.expedientes.countDocuments({ "matriculas.modulo.codigo": "0484" })    // 13
// Alumnado con alguna nota de 9 o más
db.expedientes.countDocuments({ "matriculas.nota": { $gte: 9 } })        // 6
SELECT COUNT(DISTINCT m.id_alumno)
FROM   matricula m JOIN modulo mo ON mo.id_modulo = m.id_modulo
WHERE  mo.codigo = '0484';      -- 13
OperadorQué compruebaEjemplo
(implícito)Algún elemento cumple la condición{ "matriculas.nota": { $gte: 9 } }
$allEl array contiene todos los valores dados{ "matriculas.modulo.codigo": { $all: ["0484", "0485"] } } → 13
$sizeEl array tiene exactamente n elementos{ matriculas: { $size: 6 } } → 3 (los _id 10, 11 y 12)
$elemMatchUn mismo elemento cumple todas las condicionesver abajo
$expr + $sizeComparaciones sobre el tamaño ($size solo admite igualdad){ $expr: { $gt: [ { $size: "$matriculas" }, 5 ] } } → 3

Coincidencia exacta frente a coincidencia parcial

db.expedientes.find({ "matriculas.modulo.codigo": ["0484", "0485"] })   // 0 documentos
db.expedientes.find({ "matriculas.modulo.codigo": { $all: ["0484", "0485"] } })  // 13

La primera forma busca un array exactamente igual a ["0484", "0485"], con esos dos elementos y en ese orden. La segunda busca arrays que contengan los dos valores. La misma distinción que vimos con los subdocumentos.

$elemMatch y la trampa del array

Esta es, probablemente, la diferencia conceptual que más errores produce al venir de SQL:

// (A) Parece: "alguna matrícula con nota entre 5 y 6"
db.expedientes.countDocuments({ "matriculas.nota": { $gte: 5, $lt: 6 } })        // 26

// (B) Realmente queríamos esto:
db.expedientes.countDocuments({ matriculas: { $elemMatch: { nota: { $gte: 5, $lt: 6 } } } })   // 18

26 documentos frente a 18

La consulta (A) devuelve 26 documentos porque las dos condiciones las puede cumplir un elemento distinto del array: le basta con que el alumno tenga alguna nota mayor o igual que 5 y alguna (otra) menor que 6. La consulta (B) exige que sea la misma matrícula.

-- En SQL el problema no existe: cada fila se evalúa por separado
SELECT COUNT(DISTINCT id_alumno) FROM matricula
WHERE  nota_final >= 5 AND nota_final < 6;      -- 18, igual que (B)

Regla práctica: si pones dos o más condiciones sobre el mismo array, usa $elemMatch. Con una sola condición, no hace falta. La consulta (A) no es un error de MongoDB, es su semántica documentada; el error es escribirla pensando en filas.

Ejemplo aplicado: quién ha suspendido Bases de datos.

db.expedientes.find(
  { matriculas: { $elemMatch: { "modulo.codigo": "0484", nota: { $lt: 5 } } } },
  { _id: 1, nombre: 1, apellidos: 1 }
)
_idnombreapellidos
1AdriánFerri Baeza
2RubénIborra Ferri

2 documentos

SELECT a.id_alumno, a.nombre, a.apellidos
FROM   alumno a
WHERE  EXISTS (SELECT 1
               FROM   matricula m JOIN modulo mo ON mo.id_modulo = m.id_modulo
               WHERE  m.id_alumno = a.id_alumno
               AND    mo.codigo = '0484' AND m.nota_final < 5);

2 filas

$elemMatch es el EXISTS correlacionado de la UD07, y la coincidencia implícita sobre el array es el IN con subconsulta. Si dominas esa pareja de la UD07, esta sección es una traducción.

El operador posicional $

Para actualizar el elemento que ha coincidido con el filtro sin saber su posición:

db.expedientes.updateOne(
  { _id: 1, "matriculas.modulo.codigo": "0484" },   // el filtro localiza el elemento
  { $set: { "matriculas.$.nota": 5 } }              // $ = ese elemento
)
UPDATE matricula SET nota_final = 5
WHERE  id_alumno = 1
AND    id_modulo = (SELECT id_modulo FROM modulo WHERE codigo = '0484' AND cod_ciclo = 'DAM');
COMMIT;
OperadorAfecta aEjemplo
$El primer elemento que coincide con el filtro{ $set: { "matriculas.$.nota": 5 } }
$[]Todos los elementos del array{ $inc: { "matriculas.$[].convocatoria": 1 } }
$[<id>]Los elementos que cumplan un arrayFiltersver abajo
// Subir 0,25 a todas las matrículas suspendidas de un alumno
db.expedientes.updateOne(
  { _id: 14 },
  { $inc: { "matriculas.$[susp].nota": 0.25 } },
  { arrayFilters: [ { "susp.nota": { $lt: 5 } } ] }
)

$ necesita que el filtro incluya una condición sobre el array; si no, el error es The positional operator did not find the match needed from the query. Y actualiza solo el primero que coincide: si Adrián tuviera dos matrículas de 0484 (de dos convocatorias), solo cambiaría una. Para las dos, arrayFilters.

Autoevaluación 2 preguntas
1 db.expedientes.find({ grupo: { codigo: '1DAM' } }) devuelve 0 documentos. ¿Por qué?
2 ¿Qué consulta responde a «alumnado con alguna matrícula suspendida de 0485»?

7. Agregaciones

7.1 El concepto de tubería

Los GROUP BY, HAVING y funciones de grupo de la UD07 se resuelven en MongoDB con el marco de agregación (aggregation framework): una tubería (pipeline) de etapas por las que van pasando los documentos, transformándose en cada paso.

flowchart LR
    C[(expedientes<br/>32 documentos)] --> U["$unwind<br/>una fila por matrícula"] --> M["$match<br/>filtra"] --> G["$group<br/>agrupa y calcula"] --> P["$project<br/>da forma"] --> S["$sort<br/>ordena"] --> R[Resultado]
db.<coleccion>.aggregate([ <etapa1>, <etapa2>, … ])

La idea es la misma que el orden de evaluación de un SELECT (UD06), con una diferencia importante: en SQL el orden lo decide el lenguaje y tú no lo controlas; en MongoDB tú escribes el orden, y eso tiene consecuencias de corrección y de rendimiento.

7.2 Etapas principales y su equivalente en SQL

EtapaQué haceEquivalente en SQL
$matchFiltra documentosWHERE (o HAVING, si va después de $group)
$groupAgrupa por una clave y calcula acumuladoresGROUP BY + funciones de grupo
$projectElige, renombra y calcula camposLista de columnas y expresiones del SELECT
$addFields / $setAñade campos calculados conservando el restoColumna calculada adicional
$sortOrdenaORDER BY
$limit / $skipLimita y saltaFETCH FIRST / OFFSET
$unwindConvierte cada elemento de un array en un documentoDesnormalizar: el JOIN con la tabla hija
$lookupBusca documentos relacionados en otra colecciónLEFT OUTER JOIN
$countCuenta documentosCOUNT(*)
$facetEjecuta varias tuberías sobre la misma entradaVarias consultas con UNION ALL o WITH

Acumuladores de $group más usados: $sum, $avg, $min, $max, $count, $first, $last, $push (recoge todos los valores en un array) y $addToSet (sin repetidos). Los cuatro primeros se corresponden con SUM, AVG, MIN y MAX, y como ellos ignoran los valores ausentes o no numéricos.

El orden de las etapas importa. $match al principio reduce los documentos que recorren el resto de la tubería y puede usar índices; detrás de un $group ya no hay índices que usar y actúa como un HAVING. Pon siempre $match y $project lo antes posible: es el mismo criterio de la UD07 sobre filtrar antes de agrupar.

7.3 El mismo informe en Oracle y en MongoDB

Resolvamos un informe real de jefatura de estudios: número de matrículas, matrículas calificadas y nota media de cada módulo de primer curso del ciclo DAM.

Oracle 26ai
SELECT mo.codigo,
       mo.nombre,
       COUNT(*)                    AS matriculas,
       COUNT(m.nota_final)         AS calificadas,
       ROUND(AVG(m.nota_final), 2) AS media
FROM   matricula m
       JOIN modulo mo ON mo.id_modulo = m.id_modulo
WHERE  mo.cod_ciclo = 'DAM'
AND    mo.curso = 1
GROUP  BY mo.codigo, mo.nombre
ORDER  BY mo.codigo;
CODIGONOMBREMATRICULASCALIFICADASMEDIA
0373Lenguajes de marcas y sistemas de gestión de información877.21
0483Sistemas informáticos886.38
0484Bases de datos775.93
0485Programación885.56
0487Entornos de desarrollo776.57

5 filas

MongoDB 8.0
db.expedientes.aggregate([
  { $unwind: "$matriculas" },
  { $match: { "matriculas.modulo.ciclo": "DAM", "matriculas.modulo.cursoCiclo": 1 } },
  { $group: {
      _id:         "$matriculas.modulo.codigo",
      nombre:      { $first: "$matriculas.modulo.nombre" },
      matriculas:  { $sum: 1 },
      calificadas: { $sum: { $cond: [ { $eq: [ { $type: "$matriculas.nota" }, "missing" ] }, 0, 1 ] } },
      media:       { $avg: "$matriculas.nota" } } },
  { $project: { nombre: 1, matriculas: 1, calificadas: 1, media: { $round: ["$media", 2] } } },
  { $sort: { _id: 1 } }
])
[
  { _id: '0373', nombre: 'Lenguajes de marcas y sistemas de gestión de información',
    matriculas: 8, calificadas: 7, media: 7.21 },
  { _id: '0483', nombre: 'Sistemas informáticos',  matriculas: 8, calificadas: 8, media: 6.38 },
  { _id: '0484', nombre: 'Bases de datos',         matriculas: 7, calificadas: 7, media: 5.93 },
  { _id: '0485', nombre: 'Programación',           matriculas: 8, calificadas: 8, media: 5.56 },
  { _id: '0487', nombre: 'Entornos de desarrollo', matriculas: 7, calificadas: 7, media: 6.57 }
]

5 grupos: los mismos cinco módulos y las mismas cinco medias

Lectura comparada, etapa por etapa:

Etapa de la tuberíaLínea equivalente en SQLComentario
$unwind: "$matriculas"JOIN matricula mGenera 143 documentos a partir de 32: uno por matrícula
$match: { … ciclo: "DAM" … }WHERE mo.cod_ciclo = 'DAM' AND mo.curso = 1Va después del $unwind, porque filtra por un campo del elemento
$group: { _id: "$…codigo" }GROUP BY mo.codigoLa clave de agrupación se llama siempre _id
$sum: 1COUNT(*)Suma uno por documento del grupo
$avg: "$matriculas.nota"AVG(m.nota_final)Las dos ignoran las notas que faltan: de ahí la columna calificadas
$round: ["$media", 2]ROUND(…, 2)Ojo: las reglas de desempate no son idénticas (ver aviso)
$sort: { _id: 1 }ORDER BY mo.codigo

Dos avisos sobre este informe.

  1. $match va después de $unwind porque la condición es sobre el módulo de cada matrícula. Si se pusiera antes, seleccionaría a los alumnos que tienen alguna matrícula de DAM y luego sumaría todas sus matrículas, incluidas las de otros ciclos. Es el mismo razonamiento que decidir entre WHERE y HAVING.
  2. AVG de Oracle redondea los empates hacia arriba y $round de MongoDB los redondea al dígito par. En la media de 0483 (exactamente 6,375) los dos dan 6,38, pero no siempre coincidirán; en la práctica 10.4 se analiza el caso.
¿Por qué nombre: { $first: ... } y no nombre: 1 en el $group?

Porque después de $group solo existen los campos que se hayan declarado en esa etapa: la clave _id y los acumuladores. El nombre del módulo es constante dentro de cada grupo, pero $group no lo sabe, así que hay que recogerlo con un acumulador ($first, $last o $max).

Es exactamente el motivo por el que en SQL mo.nombre tiene que aparecer en el GROUP BY: una columna que no está agrupada ni agregada no puede salir en el resultado (en Oracle, ORA-00979: not a GROUP BY expression).

7.4 $lookup: el JOIN que casi nunca querías necesitar

Cuando la información no está embebida, hay que cruzar colecciones. $lookup hace una composición externa por la izquierda: para cada documento de entrada añade un array con los documentos relacionados de la otra colección.

db.expedientes.aggregate([
  { $match: { _id: 1 } },
  { $lookup: {
      from: "ciclos",                 // colección a cruzar
      localField: "grupo.ciclo",      // campo de esta colección
      foreignField: "_id",            // campo de la otra
      as: "cicloInfo" } },            // nombre del array resultante
  { $unwind: "$cicloInfo" },          // de array de 1 elemento a subdocumento
  { $project: { _id: 1, nombre: 1, ciclo: "$cicloInfo.nombre", grado: "$cicloInfo.grado" } }
])
[ { _id: 1, nombre: 'Adrián',
    ciclo: 'Desarrollo de Aplicaciones Multiplataforma', grado: 'SUPERIOR' } ]

1 documento

SELECT a.id_alumno, a.nombre, c.nombre AS ciclo, c.grado
FROM   alumno a
       JOIN grupo g ON g.cod_grupo = a.cod_grupo
       LEFT JOIN ciclo c ON c.cod_ciclo = g.cod_ciclo
WHERE  a.id_alumno = 1;

1 fila

DiferenciaOracleMongoDB
SintaxisDeclarativa: el optimizador elige el algoritmoImperativa: tú escribes la etapa y su orden
ResultadoColumnas de las dos tablas en la misma filaUn array anidado, que normalmente hay que $unwind
Tipo de composiciónINNER, LEFT, RIGHT, FULLSiempre externa por la izquierda (se puede simular la interna con $match)
RendimientoÍndices, varios algoritmos de JOIN, estadísticasConviene índice en foreignField; no admite particionado en el mismo grado

Si en tu diseño documental necesitas $lookup en la mayoría de las consultas, estás usando MongoDB como una base de datos relacional sin sus ventajas. O se incrusta la información (§8), o el problema era relacional desde el principio.

7.5 $facet: varios informes de una sola pasada

db.expedientes.aggregate([
  { $facet: {
      porLocalidad: [ { $group: { _id: "$localidad", n: { $sum: 1 } } }, { $sort: { n: -1 } } ],
      porCiclo:     [ { $group: { _id: "$grupo.ciclo", n: { $sum: 1 } } } ],
      total:        [ { $count: "documentos" } ] } }
])

Devuelve un único documento con tres arrays, uno por cada subtubería. En SQL equivaldría a tres consultas o a un WITH con varias ramas. Es muy útil para los paneles de las aplicaciones web, donde una sola llamada alimenta varios indicadores.


Sesión 4 1 h Práctica en el aula · Modelado documental y decisión razonadaPráctica 10.5 en la página de prácticas

1. El modelo relacional

Un pedido se guarda en PEDIDO y sus líneas en LINEA_PEDIDO; para ver un pedido completo hace falta un join.

2. Embeber

En un documento, las líneas pueden ir dentro del pedido: { _id: 1, cliente: 'Ana', lineas: [ { producto: 'P1', cantidad: 2 } ] }. Se lee todo en una sola operación.

3. Cuándo embeber

Cuando los datos hijos se leen siempre junto al padre, son pocos y no tienen vida propia.

4. Cuándo referenciar

Cuando el dato hijo es compartido por muchos documentos (el catálogo de productos) o puede crecer sin límite. Se guarda su _id y se consulta aparte o con $lookup.

5. La regla

Se diseña según cómo se consulta, no según cómo se normaliza. En NoSQL es habitual duplicar algo de información a cambio de lecturas más rápidas.

8. Modelado de información en una base documental

8.1 La pregunta central: ¿embebido o referenciado?

En el modelo relacional, el diseño lo decide la normalización: una entidad, una tabla; una relación, una clave ajena o una tabla intermedia. El resultado es independiente de las consultas que se vayan a hacer.

En el modelo documental no hay normalización que aplicar: la decisión es qué se guarda junto, y depende de cómo se va a leer y escribir la información. Para cada relación entre dos entidades hay dos opciones:

// EMBEBIDO: los datos del otro lado viven dentro del documento
{ _id: 1, nombre: "Adrián",
  matriculas: [ { modulo: { codigo: "0484", nombre: "Bases de datos" }, nota: 4.75 } ] }

// REFERENCIADO: se guarda solo el identificador, como una clave ajena manual
{ _id: 1, nombre: "Adrián", matriculaIds: [ 10001, 10002, 10003 ] }
// … y en otra colección
{ _id: 10002, alumnoId: 1, moduloId: 2, nota: 4.75 }

8.2 Criterios de decisión

CriterioFavorece embebirFavorece referenciar
CardinalidadUno a pocos (un alumno, 4-6 matrículas)Uno a muchos o a muchísimos (un módulo, miles de matrículas históricas)
TamañoEl documento se queda muy por debajo de los 16 MBEl array crecería sin límite
Frecuencia de lectura conjuntaCasi siempre se leen juntosSe consultan por separado
Volatilidad del dato duplicadoEs estable (el nombre oficial de un módulo)Cambia a menudo (el tutor de un grupo)
Acceso independienteEl dato no se consulta por sí mismoSe consulta por sí mismo («todas las faltas de hoy»)
AtomicidadHay que modificar el conjunto de una vezCada parte se modifica por separado
Duplicación aceptableEl coste de actualizar en varios sitios es asumibleLa duplicación sería inmanejable

La regla de oro del modelado documental es «lo que se lee junto se guarda junto», limitada por tres topes: los 16 MB por documento, los arrays que crecen sin fin y los datos volátiles duplicados muchas veces.

8.3 Patrones habituales

PatrónCuándoCómo
Uno a pocos embebidoUn alumno y sus matrículas del curso; una direcciónArray de subdocumentos dentro del documento padre
Uno a muchos referenciadoUn módulo y todas sus matrículas históricasColección aparte con el _id del padre en cada hijo
Muchos a muchos con referencias en los dos ladosAlumnos y módulos, si se consulta en ambos sentidosmoduloIds en el alumno y alumnoIds en el módulo (con el riesgo de desincronización)
Subconjunto (subset)Un producto con miles de opiniones, de las que solo se muestran 5Todas en una colección aparte y las 5 últimas embebidas (práctica 10.5)
Dato duplicado por rendimiento (extended reference)Mostrar el nombre del módulo sin cruzar coleccionesCopiar los 2-3 campos que se muestran, no el documento entero
Campo calculado (computed)Nota media del expediente, usada en cada listadoGuardar el valor calculado y actualizarlo en cada escritura

8.4 El expediente de EduGest, modelado y comparado

En nuestra versión documental se tomaron estas decisiones:

InformaciónDecisiónPor qué
Matrículas del alumnoEmbebidasSon 4-6 por alumno, se leen siempre con el expediente y se escriben una vez al matricular
Datos del módulo dentro de cada matrículaDuplicados (referencia extendida)El nombre y las horas no cambian durante el curso y evitan un $lookup en cada lectura
Faltas de asistenciaEmbebidas en la matrículaSon pocas por matrícula (46 en todo el centro) y siempre se consultan en el contexto de la matrícula
Grupo del alumnoDuplicado el código, el ciclo, el curso y el turnoSon estables dentro del curso académico; el tutor, que sí puede cambiar, no se duplicó: está en profesores
Ciclos y profesoradoColecciones aparteTienen vida propia, se consultan por sí mismos y los referencian muchos expedientes

Qué consultas se vuelven triviales:

ConsultaMongoDBOracle
Expediente completo de un alumnofindOne({ _id: 1 }): una lecturaJOIN de alumno, matricula, modulo y falta_asistencia
Boletín de notas para imprimirEl documento ya tiene la forma del boletínHay que recomponer la jerarquía en la aplicación
Añadir una matrícula con sus datosUn updateOne atómicoINSERT en matricula (y comprobaciones de clave ajena)

Qué consultas se complican:

ConsultaOracleMongoDB
Nota media por módulo de todo el centroGROUP BY directo$unwind de todos los expedientes y después $group
Todas las faltas del 20 de mayoWHERE fecha = DATE '2026-05-20' sobre una tabla indexadaDoble $unwind de 32 documentos y filtrado posterior
Renombrar un móduloUPDATE modulo SET nombre = … (una fila)updateMany con arrayFilters sobre toda la colección
«¿Qué alumnos comparten módulo con Adrián?»JOIN de matricula con ella mismaDos consultas, o $lookup sobre la propia colección

8.5 La integridad que ahora es tu responsabilidad

Al pasar del esquema de la UD03 al modelo documental, estas restricciones dejan de estar garantizadas por el gestor:

Restricción de EduGestCómo la garantiza OracleQué hay que hacer en MongoDB
nota_final BETWEEN 0 AND 10CHECKValidador $jsonSchema con minimum y maximum (§9)
NIA únicoUNIQUE (nia)Índice único: createIndex({ nia: 1 }, { unique: true })
cod_grupo debe existir en GRUPOFOREIGN KEYCódigo de la aplicación: comprobarlo antes de escribir
No matricular dos veces del mismo módulo en el mismo cursoUNIQUE (id_alumno, id_modulo, curso_academico)Lógica de aplicación, o índice único sobre un campo calculado
Al borrar un alumno, borrar sus matrículasON DELETE CASCADEAutomático si están embebidas; manual si están referenciadas
El nombre duplicado del módulo coincide con el oficialImposible que no coincida: está una sola vezProceso de actualización que propague el cambio a toda la colección

Esta tabla es el verdadero coste del modelo documental, y casi nunca aparece en los tutoriales. Cada fila de la derecha es código que hay que escribir, probar y mantener, y que en Oracle son tres palabras de DDL. Antes de elegir NoSQL para un sistema con muchas reglas de integridad, estima ese trabajo.


9. Validación de esquema e índices

9.1 Validadores con $jsonSchema

Que MongoDB no exija un esquema no significa que no se pueda declarar uno. Un validador se asocia a la colección y el servidor lo comprueba en cada escritura, igual que las restricciones de la UD05. MongoDB 8.0

db.createCollection("expedientes", {
  validator: {
    $jsonSchema: {
      bsonType: "object",
      title: "Expediente académico",
      required: ["nia", "nombre", "apellidos", "fechaNacimiento", "matriculas"],
      properties: {
        nia:             { bsonType: "string", pattern: "^[0-9]{8}$",
                           description: "8 dígitos, como el CHAR(8) de Oracle" },
        nombre:          { bsonType: "string", maxLength: 40 },
        apellidos:       { bsonType: "string", maxLength: 80 },
        fechaNacimiento: { bsonType: "date" },
        matriculas: {
          bsonType: "array",
          items: {
            bsonType: "object",
            required: ["curso", "modulo", "convocatoria"],
            properties: {
              curso:        { bsonType: "string", pattern: "^[0-9]{4}-[0-9]{2}$" },
              convocatoria: { bsonType: "int", minimum: 1, maximum: 4 },
              nota:         { bsonType: ["double", "int"], minimum: 0, maximum: 10 }
            }
          }
        }
      },
      additionalProperties: true
    }
  },
  validationLevel: "moderate",
  validationAction: "error"
})

Comparación con el DDL que ya conoces:

Restricción de OracleEquivalente en $jsonSchema
NOT NULLEl campo en la lista required
CHAR(8) + REGEXP_LIKEbsonType: "string" + pattern
VARCHAR2(40)maxLength: 40
CHECK (convocatoria BETWEEN 1 AND 4)minimum: 1, maximum: 4
NUMBER(4,2) CHECK (nota BETWEEN 0 AND 10)bsonType, minimum, maximum
FOREIGN KEYNo tiene equivalente

Si una escritura no cumple el validador:

MongoServerError: Document failed validation
Additional information: { failingDocumentId: 40,
  details: { operatorName: '$jsonSchema',
             schemaRulesNotSatisfied: [ { operatorName: 'properties',
               propertiesNotSatisfied: [ { propertyName: 'nia', description: '8 dígitos…' } ] } ] } }

Dos parámetros controlan el rigor:

ParámetroValoresEfecto
validationLeveloffNo valida nada
strict (por defecto)Valida todas las inserciones y actualizaciones
moderateValida las inserciones y las actualizaciones de documentos que ya eran válidos: permite convivir con datos históricos incorrectos
validationActionerror (por defecto)Rechaza la operación
warnLa acepta y lo anota en el registro del servidor

validationLevel: "moderate" con validationAction: "warn" es la configuración para introducir un esquema en una colección que ya tiene datos: se detecta lo que incumple sin romper la aplicación, se corrige y después se endurece a strict y error. Es la misma estrategia que ENABLE NOVALIDATE en una restricción de Oracle (UD05).

9.2 Índices

Los índices de MongoDB son la misma idea de la UD05 y la UD07: una estructura auxiliar (árbol B) que evita recorrer todos los documentos, a cambio de ocupar espacio y encarecer las escrituras.

db.expedientes.createIndex({ nia: 1 }, { unique: true, name: "ux_nia" })  // único, como UNIQUE
db.expedientes.createIndex({ "grupo.codigo": 1 })                         // simple
db.expedientes.createIndex({ localidad: 1, apellidos: 1 })                // compuesto
db.expedientes.createIndex({ "matriculas.modulo.codigo": 1 })             // multiclave (array)
db.expedientes.createIndex({ nombre: "text", apellidos: "text" })         // de texto
db.expedientes.getIndexes()
db.expedientes.dropIndex("ux_nia")
TipoEquivalente en OraclePara qué
SimpleCREATE INDEXFiltros de igualdad y rango por un campo
CompuestoÍndice sobre varias columnasFiltros por varios campos; el orden importa y sirve también para los prefijos
ÚnicoUNIQUE / CREATE UNIQUE INDEXGarantiza unicidad (NIA, DNI)
MulticlaveSin equivalente directoSe crea automáticamente al indexar un campo array: una entrada por elemento
De textoCONTEXT de Oracle TextBúsqueda por palabras con $text
TTL (expireAfterSeconds)Tarea programada que borra filas antiguasCaducidad automática de documentos (sesiones, registros)
Parcial / dispersoÍndice basado en función con CASE, índice sobre columna nulaIndexar solo los documentos que cumplen una condición

El índice _id_ existe siempre y no se puede eliminar, como el índice que Oracle crea al declarar una clave primaria.

9.3 Comprobar que se usa: explain

db.expedientes.find({ "grupo.codigo": "1DAM" }).explain("executionStats")

En la salida interesan tres datos, que se leen igual que un plan de ejecución de Oracle (UD07):

Campo de la salidaQué significaEquivalente en Oracle
stage: "COLLSCAN"Ha recorrido toda la colecciónTABLE ACCESS FULL
stage: "IXSCAN"Ha usado un índiceINDEX RANGE SCAN
totalDocsExaminedDocumentos leídosFilas accedidas
nReturnedDocumentos devueltosFilas devueltas
executionTimeMillisTiempoTiempo de ejecución

El criterio profesional es el mismo que en Oracle: la señal de alarma no es que aparezca COLLSCAN, sino que totalDocsExamined sea mucho mayor que nReturned. Si para devolver 7 documentos ha examinado 32, con 32 documentos no pasa nada; con 32 millones, es un problema grave. Mide, no supongas.


10. Seguridad y usuarios

10.1 Autenticación: el problema real

Por defecto, un mongod recién instalado no exige autenticación: cualquiera que alcance el puerto 27017 puede leer, modificar y borrar todo. Entre 2015 y 2017 decenas de miles de servidores MongoDB quedaron expuestos en internet por esta razón, con filtraciones masivas de datos personales y extorsiones. Desde la versión 3.6, mongod escucha solo en 127.0.0.1 (parámetro bindIp), lo que reduce el riesgo, pero muchas guías de instalación siguen recomendando bindIp: 0.0.0.0 «para que funcione».

Nunca pongas bindIp: 0.0.0.0 sin activar antes la autenticación (--auth o security.authorization: enabled en mongod.conf) y sin un cortafuegos delante. Un MongoDB sin autenticación accesible desde internet es una brecha de seguridad, y si contiene datos personales de alumnado es además una infracción del RGPD con obligación de notificación (UD01).

// 1) Con el servidor aún sin --auth, crear el administrador de usuarios
use admin
db.createUser({
  user: "adminEdu",
  pwd: passwordPrompt(),                         // nunca la contraseña en el script
  roles: [ { role: "userAdminAnyDatabase", db: "admin" } ]
})

// 2) Reiniciar mongod con autenticación y conectarse
//    mongosh -u adminEdu -p --authenticationDatabase admin

// 3) Crear los usuarios de la aplicación con el mínimo privilegio necesario
use edugest
db.createUser({ user: "appEdugest", pwd: passwordPrompt(),
                roles: [ { role: "readWrite", db: "edugest" } ] })
db.createUser({ user: "informes",   pwd: passwordPrompt(),
                roles: [ { role: "read",      db: "edugest" } ] })
db.getUsers()

10.2 Roles integrados

RolPermiteEquivalente aproximado en Oracle (UD05)
readLeer todas las colecciones de la base de datosGRANT SELECT sobre las tablas
readWriteLeer y escribir, crear y borrar coleccionesSELECT, INSERT, UPDATE, DELETE + CREATE TABLE
dbAdminÍndices, estadísticas y validadores; no lee los datosPrivilegios de administración del esquema
userAdminCrear usuarios y asignar roles en esa base de datosCREATE USER, GRANT ANY ROLE (limitado)
dbOwnerreadWrite + dbAdmin + userAdminPropietario del esquema
clusterAdmin, rootAdministración del clúster; todoSYSDBA

El principio de mínimo privilegio es idéntico al de la UD05: la aplicación web se conecta con readWrite sobre edugest y nada más; el panel de informes, con read; nadie trabaja a diario con root. Y, como allí, los roles personalizados (db.createRole) permiten ajustar permisos por colección y por acción.

10.3 Cifrado, copias y datos personales

MedidaEn MongoDBComentario
Cifrado en tránsitoTLS (net.tls.mode: requireTLS)Sin TLS, las credenciales y los datos viajan en claro por la red
Cifrado en reposoCifrado del almacenamiento (edición Enterprise) o del sistema de ficherosEn Community, se cifra el volumen o el disco
Cifrado por campoClient-Side Field Level EncryptionEl servidor nunca ve el valor en claro: útil para datos de salud
Copias de seguridadmongodump / mongorestoreEquivalen a Data Pump; comprueba siempre la restauración, no solo la copia
AuditoríaRegistro de operaciones (Enterprise)En Community, el registro del servidor y la lógica de la aplicación
# Copia completa de la base de datos y restauración en otro servidor
mongodump   --uri="mongodb://appEdugest@localhost:27017/edugest" --out=/backup/2026-06-08
mongorestore --uri="mongodb://appEdugest@localhost:27017" /backup/2026-06-08

El RGPD y la LOPDGDD (UD01) se aplican igual sea el almacén relacional o documental: minimización de datos, limitación de la finalidad, derecho de supresión y seguridad desde el diseño. El modelo documental añade dos dificultades prácticas: al duplicar datos personales en varios documentos, el borrado (artículo 17) debe recorrer todas las copias; y al no haber esquema obligatorio, es más fácil que alguien añada un campo con datos personales que nadie ha inventariado en el registro de actividades de tratamiento.


11. SQL frente a NoSQL: cómo elegir

11.1 La tabla de decisión, matizada

NecesidadSolución habitualMatiz imprescindible
Datos muy estructurados y establesRelacionalSi además son pocos, no hay ni discusión
Relaciones complejas entre entidadesRelacionalSalvo que lo que importe sea recorrer las relaciones en profundidad: entonces, grafos
Integridad referencial garantizadaRelacionalEs el criterio más decisivo de todos: ningún producto NoSQL ofrece claves ajenas declarativas
Esquema flexible o cambianteNoSQL documentalPero declara un validador: flexible no es inexistente
Documentos con estructura variableDocumentalAlternativa real: una columna JSON en Oracle, PostgreSQL o MySQL
Grandes volúmenes distribuidosDependerá del caso¿Grandes de verdad? Oracle gestiona terabytes en un servidor. Mide antes de distribuir
Relaciones entre nodos, caminosGrafosTambién se resuelve en SQL recursivo (CONNECT BY, WITH RECURSIVE), peor pero suficiente si es ocasional
Caché, sesiones, contadoresClave-valorCasi siempre junto a una base de datos principal, nunca en lugar de ella
Series temporales, telemetríaColumnar o base de datos de series temporalesMongoDB tiene colecciones de series temporales desde la 5.0; Oracle, particionado por rango de fechas
Informes, cuadros de mando, BIRelacional o almacén analíticoLas herramientas de informes hablan SQL

Esta tabla es un punto de partida para el análisis, no un algoritmo. Dos proyectos con la misma fila de la tabla pueden acabar en soluciones distintas según el equipo, el presupuesto, la normativa aplicable y la infraestructura existente.

11.2 Tres escenarios resueltos

Escenario 1 · Gestión académica de un centro (EduGest).

  • Datos: estructura fijada por normativa, estable durante años, con relaciones N:M y restricciones legales (convocatorias, notas, actas).
  • Volumen: 32 alumnos en nuestro ejemplo; 2.000 en un centro grande; algún millón si fuese toda la Comunitat. Nada que no quepa en un servidor.
  • Consultas: informes imprevisibles (jefatura de estudios, inspección, memoria anual), casi siempre agregados sobre varias entidades.
  • Consistencia: una nota de un acta oficial no admite consistencia eventual.
  • Decisión: relacional. La integridad declarativa y las consultas ad hoc son exactamente lo que pide el problema. Qué perderíamos con MongoDB: claves ajenas, unicidad de combinaciones, JOIN cómodos y la capacidad de contestar a una pregunta que nadie había previsto. Qué ganaríamos: la lectura del expediente completo en un acceso, que es útil pero no determinante.

Escenario 2 · Catálogo de productos con atributos variables (práctica 10.5).

  • Datos: cada familia de producto tiene características distintas, y la lista cambia con cada proveedor.
  • Consultas: ficha del producto, listado por categoría con filtros, puntuación media.
  • Consistencia: que el stock mostrado sea de hace dos segundos es aceptable; el stock real se comprueba al confirmar el pedido.
  • Decisión: documental. El modelo relacional obligaría a una tabla por tipo de producto, a decenas de columnas nulas o al patrón entidad-atributo-valor, que es incómodo y lento. Qué perderíamos: integridad declarativa entre producto y categoría. Matiz honesto: el pedido y el pago de esa misma tienda deberían ser relacionales y transaccionales. Dos modelos en la misma aplicación.

Escenario 3 · Telemetría de los sensores de las aulas (práctica 10.7).

  • Datos: 40 aulas × 3 magnitudes × 2 medidas por minuto ≈ 126 millones de filas al año, inmutables y siempre con la misma forma.
  • Consultas: «media de CO₂ del aula I-12 entre las 9:00 y las 14:00 del jueves»: rangos de tiempo por dispositivo.
  • Consistencia: perder una medida aislada no es grave.
  • Decisión: columnar, base de datos de series temporales o colección de series temporales de MongoDB. El patrón de acceso (clave de partición = sensor, clave de ordenación = instante) encaja con el modelo. Alternativa razonable: Oracle con particionado por rangos de fecha y compresión, que para un solo centro probablemente sea más que suficiente y evita añadir una tecnología más al sistema.

11.3 El coste que no aparece en los benchmarks

Al comparar dos tecnologías, además del rendimiento hay que poner en la balanza:

  • Aprendizaje del equipo. SQL lo conoce todo el mundo; el pipeline de agregación, mucha menos gente. Un sistema que nadie sabe mantener es un riesgo.
  • Herramientas. Informes, cuadros de mando, migraciones, auditoría y copias están mucho más estandarizados en el mundo relacional.
  • Operación. Un conjunto de réplica con particionado tiene más piezas que fallar que un servidor único.
  • Reversibilidad. Pasar de relacional a documental es relativamente fácil (hay JSON en el propio Oracle). Volver atrás después de dos años de documentos sin esquema es mucho más caro.
  • Normativa. Si hay datos personales o actas oficiales, la trazabilidad y la retención pesan tanto como la latencia.

11.4 Persistencia políglota

Lo habitual en un sistema real no es elegir una base de datos, sino usar cada una para lo que es buena:

flowchart LR
    APP[Aplicación de secretaría] --> O[(Oracle<br/>matrícula, actas, notas)]
    APP --> M[(MongoDB<br/>documentos, contenidos, histórico)]
    APP --> V[(Valkey<br/>sesiones y caché)]
    O -. proceso de sincronización .-> M

Esa es la persistencia políglota. Tiene una condición y un peligro: la condición es que haya una única fuente de verdad para cada dato (en el diagrama, Oracle para la matrícula); el peligro es que cada almacén añade copias de seguridad, monitorización, permisos y conocimiento que mantener.

11.5 Conclusión honesta sobre EduGest

Después de toda la unidad, la respuesta para nuestro proyecto es clara: la gestión académica de EduGest debe seguir en Oracle. Los datos son estructurados y estables, las reglas de integridad son muchas y legalmente exigibles, las consultas son impredecibles y el volumen es pequeño. Elegir MongoDB aquí sería cambiar garantías que necesitamos por ventajas que no nos hacen falta.

Y a la vez hay partes de un centro educativo donde el modelo documental encaja muy bien: el repositorio de materiales didácticos y programaciones, el histórico de expedientes cerrados (inmutable y consultado como un todo), los contenidos de la web, los registros de actividad de la plataforma. Saber dónde poner cada cosa —y saber justificarlo— es el aprendizaje de esta unidad y el objetivo del RA7.

La pregunta profesional nunca es «¿SQL o NoSQL?». Es: «¿cómo son mis datos, cómo los voy a consultar, qué garantías necesito, cuánto van a crecer y con qué equipo lo voy a mantener?». Las cinco respuestas juntas determinan la tecnología. Cualquier recomendación que no venga precedida de esas cinco respuestas es una moda, no una decisión técnica.


12. Errores frecuentes

SíntomaCausaSolución
MongoInvalidArgumentError: Update document requires atomic operatorsupdateOne/updateMany sin operador: { nombre: "X" } en lugar de { $set: { nombre: "X" } }Añadir $set. Si de verdad quieres sustituir el documento, usar replaceOne a conciencia
MongoServerError: E11000 duplicate key error … index: _id_Insertas un _id que ya existeUsar otro _id, un upsert o dejar que el servidor genere el ObjectId. Equivale a ORA-00001
matchedCount: 0 y «no hace nada»El filtro no coincide: mayúsculas (Nombre ≠ nombre), tipos ("1" ≠ 1) o notación de punto mal escritaProbar el mismo filtro con countDocuments() antes de actualizar
Una consulta sobre un array devuelve más documentos de los esperadosDos condiciones sobre el mismo array sin $elemMatch: cada una la cumple un elemento distintoEnvolver las condiciones en $elemMatch (§6.3)
{ grupo: { codigo: "1DAM" } } devuelve 0 documentosCompara el subdocumento completo, con todos sus campos y en ordenUsar notación de punto: { "grupo.codigo": "1DAM" }
Una agregación da totales inflados o filtra mal$match colocado antes de $unwind cuando la condición es sobre el elemento del arrayFiltrar después del $unwind, o antes y después (§7.3)
The positional operator did not find the match needed from the query$ en la actualización sin condición sobre el array en el filtroIncluir la condición en el filtro, o usar arrayFilters / $[]
Las fechas no se ordenan ni se filtran bienSe guardaron como texto ("20/05/2026") en lugar de ISODate(...)Convertir a Date. Es el mismo error que usar VARCHAR2 para fechas en Oracle (UD01)
Los datos aparecen en una base de datos llamada testOlvidaste use edugest; MongoDB creó la base al primer insertComprobar con db, y mover los datos con mongodump/mongorestore
Un documento válido ayer es rechazado hoySe ha añadido un validador $jsonSchema con validationLevel: "strict"Corregir el documento, o pasar temporalmente a moderate / warn (§9.1)
La consulta funciona pero tarda mucho con datos realesFalta el índice: explain muestra COLLSCAN y totalDocsExamined enormecreateIndex sobre el campo del filtro y comprobar IXSCAN (§9.3)

13. Buenas prácticas

  • Diseña a partir de las consultas, no de las entidades: en el modelo documental, el patrón de acceso es el requisito principal.
  • Declara el esquema aunque no sea obligatorio: validador $jsonSchema desde el primer día e índices únicos donde el modelo relacional tendría UNIQUE.
  • Usa los métodos actuales (insertOne, updateMany, deleteOne…) y evita los obsoletos: la propia orden debe decir a cuántos documentos afecta.
  • Elige el tipo BSON correcto: ISODate para fechas y NumberDecimal para notas e importes; nunca texto para ninguno de los dos.
  • Antes de un updateMany o un deleteMany, ejecuta el filtro con countDocuments(). No hay ROLLBACK que te salve.
  • Duplica solo datos estables y documenta en el repositorio qué campos están duplicados y qué proceso los mantiene coherentes.
  • No incrustes arrays que crecen sin límite: aplica el patrón de subconjunto antes de acercarte a los 16 MB por documento.
  • Mide con explain("executionStats") y compara nReturned con totalDocsExamined, igual que lees un plan de ejecución en Oracle.
  • Activa la autenticación y aplica el mínimo privilegio antes de exponer el servicio, y nunca escribas contraseñas en los scripts (passwordPrompt()).
  • Prueba la restauración de las copias de mongodump, no solo su creación.
  • Justifica por escrito la elección de tecnología con los cinco criterios del §11.5: es lo que te pedirán en una entrevista y en la práctica 10.7.

Repasa los términos de la UD10 Pulsa una tarjeta para girarla

14. Resumen

Idea claveDetalle
NoSQL no sustituye al modelo relacionalResuelve otros problemas: esquema variable, escalado horizontal, agregados autocontenidos. Lo normal es la convivencia
Esquema flexible ≠ sin esquemaEl esquema se traslada del SGBD a la aplicación o a un validador $jsonSchema
La desnormalización es una decisiónSe duplica lo estable para leer junto lo que se usa junto; lo volátil se referencia
ACID frente a BASEBASE cambia consistencia inmediata por disponibilidad; el teorema CAP obliga a elegir entre C y A cuando la red se parte
«NoSQL = sin ACID» es falsoMongoDB es atómico por documento desde siempre y admite transacciones multidocumento desde la 4.0
Cuatro familiasClave-valor (caché, sesiones), documental (agregados, catálogos), columnar (series temporales), grafos (relaciones profundas)
Elementos de MongoDBBase de datos → colección → documento → campo, con _id obligatorio, único e inmutable, y tipos BSON
CRUDinsertOne/insertMany, find/findOne, updateOne/updateMany con $set, deleteOne/deleteMany. Nunca los métodos obsoletos
Arrays y subdocumentosNotación de punto; el filtro se aplica a cada elemento; dos condiciones sobre el mismo array exigen $elemMatch
AgregaciónTubería de etapas: $match≈WHERE, $group≈GROUP BY, $unwind≈JOIN con la tabla hija, $lookup≈LEFT JOIN
ModeladoEmbebido o referenciado según cardinalidad, tamaño (16 MB), lectura conjunta y volatilidad
La integridad se trasladaClaves ajenas, unicidad de combinaciones y borrado en cascada pasan a ser código de la aplicación
SeguridadAutenticación desactivada por defecto: --auth, roles integrados, mínimo privilegio, TLS y mongodump
Criterio de elecciónEstructura de los datos, consultas previstas, garantías necesarias, crecimiento y equipo. Para EduGest: Oracle

15. Autoevaluación

Autoevaluación 10 preguntas
1 ¿Cuál de estas afirmaciones describe mejor qué es una base de datos NoSQL?
2 En un sistema distribuido con una partición de red, ¿qué dice el teorema CAP que hay que elegir?
3 Quieres guardar las opiniones de un producto del que hay miles, y en la ficha solo se muestran las 5 más recientes. ¿Qué modelado es correcto?
4 db.expedientes.find({ "matriculas.nota": { $gte: 5, $lt: 6 } }) devuelve 26 de los 32 expedientes, cuando solo 18 alumnos tienen una nota entre 5 y 6. ¿Qué ocurre?
5 ¿Qué diferencia hay entre $unset: { telefono: "" } en MongoDB y UPDATE … SET telefono = NULL en Oracle?
6 En la tubería de agregación, ¿dónde conviene colocar $match y por qué?
7 Vas a migrar la tabla MATRICULA de EduGest a documentos embebidos en el expediente. ¿Qué restricción deja de estar garantizada por el gestor?
8 ¿Qué hace db.expedientes.replaceOne({ _id: 1 }, { nombre: "Adrián" })?
9 Un mongod recién instalado, con la configuración por defecto, …
10 Para la gestión académica de un centro (matrícula, notas y actas), la elección técnica razonada es…

Referencias