$match, $project y $set en MongoDB | Nicolás Garzón
$match
$project
$set
$match
$project
$set
Texto
Copiar $match
→ qué documentos continúan
$set
→ qué valores se calculan o corrigen
$project
→ qué forma sale de la etapaAunque parecen sencillos, su ubicación determina índices, cantidad de trabajo, tipos disponibles y legibilidad.
Utiliza sintaxis de query para filtrar:
JavaScript
Copiar {
$match : {
businessId,
status : 'confirmed' ,
confirmedAt : { $gte : start, $lt : end }
}
} Cuando aparece al inicio, puede aprovechar un índice compatible. Debe incluir tenant y restricciones de seguridad igual que una query normal.
Texto
Copiar 1.000.000 documentos
→ $match reduce a 5.000
→ stages posteriores procesan 5.000Moverlo después de $unwind, $lookup o $group puede multiplicar trabajo.
Si filtras un campo creado por $set, el $match debe ir después:
JavaScript
Copiar [
{
$set : {
lineTotal : { $multiply : [ '$quantity' , '$price' ] }
}
} ,
{
$match : {
lineTotal : { $gte : Decimal128 ( '100000.00' ) }
}
}
] Ese match no puede usar un índice original sobre lineTotal porque el campo se calcula durante la pipeline.
Define qué campos permanecen, se renombran o se calculan.
JavaScript
Copiar {
$project : {
_id : 0 ,
id : { $toString : '$_id' } ,
customerName : '$customer.nameAtPurchase' ,
total : 1 ,
confirmedAt : 1
}
}
incluir o excluir;
renombrar;
crear expresiones;
construir subdocumentos;
retirar campos sensibles;
reducir payload interno y final.
No proyectes fuera un campo que un stage posterior necesita.
$set conserva campos actuales y añade o reemplaza valores:
JavaScript
Copiar {
$set : {
subtotal : {
$sum : {
$map : {
input : '$items' ,
as : 'item' ,
in : {
$multiply : [ '$$item.quantity' , '$$item.unitPrice' ]
}
}
}
} ,
processedAt : '$$NOW'
}
} Es equivalente a $addFields en propósito general. Elegir uno es principalmente cuestión de claridad y estilo del equipo.
JavaScript
Copiar {
_id,
name : 'Café' ,
price : 10000 ,
cost : 7000 ,
internalCode : 'X'
} JavaScript
Copiar {
$set : {
margin : { $subtract : [ '$price' , '$cost' ] }
}
} Salida conserva todos los campos y añade margin.
JavaScript
Copiar {
$project : {
name : 1 ,
margin : { $subtract : [ '$price' , '$cost' ] }
}
} Salida contiene _id, name y margin, salvo exclusión de _id.
Para expresiones complejas, usa $set en pasos:
JavaScript
Copiar [
{
$set : {
subtotal : { $sum : '$items.subtotal' }
}
} ,
{
$set : {
tax : { $multiply : [ '$subtotal' , Decimal128 ( '0.19' ) ] }
}
} ,
{
$set : {
total : { $add : [ '$subtotal' , '$tax' ] }
}
}
] Es más fácil verificar que una expresión profundamente anidada.
Dentro de projections condicionales, $$REMOVE puede omitir un campo:
JavaScript
Copiar {
$project : {
name : 1 ,
discount : {
$cond : [
{ $gt : [ '$discount' , 0 ] } ,
'$discount' ,
'$$REMOVE'
]
}
}
} Esto distingue omitir de devolver null.
JavaScript
Copiar { price : '10000' }
{ price : 10000 }
{ price : Decimal128 ( '10000.00' ) } JavaScript
Copiar {
$set : {
normalizedPrice : {
$convert : {
input : '$price' ,
to : 'decimal' ,
onError : null ,
onNull : null
}
}
}
} Después puedes separar inválidos:
JavaScript
Copiar {
$match : {
normalizedPrice : { $ne : null }
}
} No conviertas silenciosamente datos inválidos en cero, porque altera métricas.
JavaScript
Copiar {
$set : {
displayName : {
$ifNull : [ '$name' , 'Sin nombre' ]
}
}
} Pero null y missing pueden representar situaciones diferentes. Para auditoría usa $type antes de decidir un default.
JavaScript
Copiar {
$set : {
day : {
$dateTrunc : {
date : '$confirmedAt' ,
unit : 'day' ,
timezone : 'America/Bogota'
}
}
}
} La zona horaria forma parte del cálculo. Agrupar UTC sin considerar el negocio puede asignar ventas a un día local incorrecto.
Filtrar elementos sin $unwind:
JavaScript
Copiar {
$set : {
expensiveItems : {
$filter : {
input : '$items' ,
as : 'item' ,
cond : {
$gte : [ '$$item.unitPrice' , Decimal128 ( '50000.00' ) ]
}
}
}
}
} Esto conserva un documento por orden, a diferencia de $unwind.
Reducir campos temprano puede ayudar, pero el optimizador ya puede eliminar campos innecesarios en algunos casos. No añadas $project inicial únicamente por superstición. Úsalo para claridad, seguridad o una transformación necesaria y verifica el plan.
No permitas que el cliente decida expressions o paths arbitrarios. Una projection dinámica podría exponer:
hashes;
tokens;
notas internas;
campos de otro tenant;
metadata operacional.
Construye vistas permitidas en código.
$set puede reemplazar accidentalmente un campo original necesario.
Mezclar inclusión y exclusión genera error salvo _id y casos específicos.
Sin onError, $convert puede detener la pipeline.
Una expression puede devolver null, missing o error según operador.
Las rutas usan dot notation; schemas con nombres problemáticos dificultan acceso.
Calcular antes de filtrar puede impedir index use.
Añade ruido y puede ocultar dependencias.
Un campo decimal se reemplaza por double accidentalmente.
Comparten nombre, pero uno es stage de aggregation y otro operador de escritura.
Permite acceso a campos no autorizados.
Documenta input y output de cada stage.
Prueba tipos válidos e inválidos.
Prueba null y missing.
Revisa que campos posteriores existan.
Ejecuta explain() para el $match inicial.
Compara cardinalidad antes y después.
Verifica zona horaria.
Revisa precisión numérica.
Audita campos sensibles.
Divide expressions densas.
$match controla cardinalidad, $set construye valores y $project define forma. El orden debe reflejar dependencias y rendimiento. Filtrar temprano y convertir tipos explícitamente suele ser correcto, pero toda optimización debe verificarse con el plan y datos reales.
Comprueba lo aprendido
¿Cuándo un $match puede usar índice?
¿Qué diferencia existe entre $set y $project?
¿Por qué un match sobre campo calculado no usa el índice original?
¿Cómo evitarías que una conversión inválida detenga la pipeline?
¿Cuándo usarías $filter en vez de $unwind?
¿Por qué no conviene permitir projections arbitrarias?