Evolución del schema en MongoDB | Nicolás Garzón
Inicio Wiki MongoDB Evolución del schema Volver a MongoDBMongoDB
Evolución del schema Explica cómo evolucionar documentos y validadores sin bloquear despliegues, usando compatibilidad gradual, versionado, defaults y lectores tolerantes.
Última actualización Actualizada 25 de jul de 2026
En MongoDB el schema es flexible, pero la aplicación sigue teniendo contratos. Durante una evolución pueden coexistir documentos viejos y nuevos, varias versiones de la aplicación y jobs de migración. El objetivo no es transformar todo de una vez, sino mantener compatibilidad mientras el sistema cambia.
Nota anteriorDenormalización y datos derivados Nota siguiente Migraciones de datos Texto
Copiar lectores compatibles
→ writers nuevos
→ backfill
→ validación más estricta
→ retirada del formato antiguoCambiar un campo sin plan puede romper queries, índices, validators, serialización y procesos externos.
Añadir un campo optional:
JavaScript
Copiar {
schemaVersion : 2 ,
preferredLanguage : 'es'
} Suele ser seguro si los lectores antiguos ignoran campos desconocidos y los nuevos tienen un default para documentos previos.
De phone a phoneNumber. Requiere transición porque lectores y writers pueden usar nombres distintos.
JavaScript
Copiar { createdAt : '2026-07-24' } JavaScript
Copiar { createdAt : ISODate ( '2026-07-24T00:00:00Z' ) } Es más riesgoso porque filtros, sort e índices distinguen tipos BSON.
De campos planos a subdocumento:
JavaScript
Copiar { street, city } JavaScript
Copiar { address : { street, city } } Afecta queries, projections, updates, validators e índices.
Un campo conserva nombre y tipo, pero cambia significado. Es el más difícil de detectar. Debe versionarse o introducir un campo nuevo.
Guardar versión ayuda a identificar representación:
JavaScript
Copiar {
_id,
schemaVersion : 3 ,
} No debe incrementarse por cada cambio menor. Úsala cuando el lector necesita comportarse distinto o el backfill debe identificar pendientes.
Índice temporal para migración:
JavaScript
Copiar db. orders. createIndex ( { schemaVersion : 1 , _id : 1 } ) ; Después puede retirarse si no sirve al workload normal.
Patrón seguro para renombrar o reestructurar.
Los lectores soportan ambos campos:
TypeScript
Copiar const phone = document. phoneNumber ?? document. phone ?? null ; Los writers producen el formato nuevo y, si es necesario, escriben ambos temporalmente.
Un job transforma documentos anteriores por batches.
Cuando métricas muestran que ya no quedan lectores ni documentos viejos, se elimina soporte y se endurece validación.
No inviertas el orden. Eliminar primero el campo antiguo rompe despliegues rolling y workers viejos.
JavaScript
Copiar {
$ifNull : [ '$phoneNumber' , '$phone' ]
} Coste: lógica temporal que puede quedarse para siempre. Añade fecha o criterio de eliminación.
Escribir viejo y nuevo puede mantener aplicaciones antiguas, pero introduce divergencia:
JavaScript
Copiar {
$set : {
phone : normalized,
phoneNumber : normalized
}
} Si un writer actualiza solo uno, quedan valores diferentes. Úsalo durante una ventana corta, monitorea inconsistencias y define cuál gana.
Transforma cuando se lee o actualiza:
Texto
Copiar leer versión 1
→ convertir a versión 3
→ guardar de nuevo
distribuye coste;
migra datos activos primero;
evita job inicial enorme.
datos fríos nunca cambian;
lectura produce escritura inesperada;
carreras entre requests;
latencia variable;
difícil confirmar finalización.
Es útil como complemento, no siempre como única estrategia.
Un job procesa toda la colección.
estado final medible;
permite retirar compatibilidad;
comportamiento consistente.
carga de escritura, oplog e índices;
duración;
coordinación con tráfico;
necesidad de checkpoints y retries.
La validación debe evolucionar por fases.
Permite formato viejo y nuevo.
Despliega lectores y writers compatibles.
Migra datos.
Ejecuta auditoría.
Cambia validator para exigir formato nuevo.
JavaScript
Copiar {
$jsonSchema : {
anyOf : [
{ required : [ 'phone' ] } ,
{ required : [ 'phoneNumber' ] }
]
}
} Después se reemplaza por el requisito definitivo.
Renombrar un campo requiere crear el índice nuevo antes de retirar el antiguo:
Texto
Copiar crear índice nuevo
→ desplegar queries nuevas
→ verificar uso
→ migrar datos
→ ocultar índice viejo
→ eliminarloDos índices temporales aumentan write amplification y almacenamiento. Inclúyelos en capacidad.
Código nuevo puede leer datos viejos.
Código viejo tolera datos producidos por writers nuevos.
Durante rolling deploy suelen necesitarse ambas por un tiempo.
Un enum nuevo puede romper código viejo aunque el campo ya exista:
Texto
Copiar pending | confirmed
→ añade partially_refundedLos lectores deben manejar valores desconocidos de forma segura, no asumir un switch exhaustivo en runtime.
Un cambio de schema también afecta:
Change Stream consumers;
outbox events;
Search indexes;
caches;
analytics;
backups restaurados;
integraciones externas.
Versiona eventos independientemente del documento si tienen contratos distintos.
Un backup antiguo puede reintroducir documentos de versiones previas. El runbook de restore debe incluir:
versión de aplicación compatible;
migraciones requeridas;
validators;
índices;
secretos y configuración;
verificación posterior.
$unset masivo también genera writes y oplog. A veces puede dejarse el campo antiguo durante un periodo y retirarlo gradualmente.
Antes de eliminar confirma:
cero lecturas observadas;
cero writers antiguos;
backfill completo;
proyecciones actualizadas;
backups y exportaciones comprendidos;
rollback definido.
Un deploy nuevo puede escribir un formato que el código anterior no entiende. Si quieres rollback rápido, conserva forward compatibility o utiliza feature flags para activar el nuevo write después de desplegar lectores.
Texto
Copiar reader support
→ flag off
→ deploy writers
→ flag on gradualmente
documento sin schemaVersion;
tipo mixto en el mismo campo;
writer antiguo activo durante backfill;
restore de backup viejo;
evento fuera de orden;
validator demasiado estricto;
índice nuevo aún en construcción;
rollback después de escribir datos nuevos;
documentos corruptos que no coinciden con ninguna versión.
migrar antes de desplegar lectores compatibles;
renombrar campo en una sola release;
dual write indefinido;
cambiar tipo sin analizar índices;
asumir que TypeScript valida datos persistidos;
endurecer validator demasiado pronto;
olvidar workers e integraciones;
no planear rollback.
Prueba documentos de cada versión.
Ejecuta código viejo con datos nuevos.
Ejecuta código nuevo con datos viejos.
Simula rolling deploy.
Interrumpe el backfill.
Restaura un backup previo.
Revisa explain e índices.
Audita validators.
Busca tipos mixtos.
Practica rollback y contract.
La evolución segura usa expand, migrate y contract. Primero se amplía compatibilidad, después se cambian writers y datos, y solo al final se retira el formato anterior. Schema flexibility no elimina contratos: obliga a gestionarlos explícitamente entre aplicaciones, índices, validators, eventos y backups.
Comprueba lo aprendido
¿Qué diferencia existe entre cambio aditivo y semántico?
¿Qué etapas tiene expand and contract?
¿Qué riesgo introduce dual write?
¿Cuándo usarías lazy migration?
¿Cómo evolucionarías un validator?
¿Por qué un rollback condiciona el formato nuevo?