Docs Menu
Docs Home
/ /
/ / /

Optimización del pipeline de agregación

Las operaciones del pipeline de agregación tienen una fase de optimización que intenta remodelar el pipeline para mejorar el rendimiento.

Para ver cómo el optimizador transforma una canalización de agregación particular, incluya el explainOpción en el db.collection.aggregate() método.

Las optimizaciones están sujetas a cambios entre versiones.

Además de aprender sobre las optimizaciones de la canalización de agregación realizadas durante la fase de optimización, también verá cómo mejorar el rendimiento de la canalización de agregación utilizando índices y filtros de documentos.

Puedes ejecutar pipelines de agregación en la interfaz de usuario para implementaciones alojadas en MongoDB Atlas.

El pipeline de agregación puede determinar si solo necesita un subconjunto de los campos en los documentos para obtener los resultados. Si es así, el pipeline solo utiliza esos campos, lo que reduce la cantidad de datos que pasan por el pipeline.

Cuando se usa una etapa $project, normalmente debería ser la última etapa del pipeline, utilizada para especificar qué campos devolver al cliente.

Usar una etapa $project al principio o en medio de un pipeline para reducir el número de campos que se pasan a las etapas posteriores del pipeline es poco probable que mejore el rendimiento, ya que la base de datos realiza esta optimización automáticamente.

Para una canalización de agregación que contiene una etapa de proyección ($addFields, $project, $set o $unset) seguida de una etapa $match, MongoDB mueve cualquier filtro en la etapa $match que no requiera valores calculados en la etapa de proyección a una nueva etapa $match antes de la proyección.

Si una canalización de agregación contiene múltiples etapas de $match o proyección, MongoDB realiza esta optimización para cada etapa $match, moviendo cada filtro $match antes de todas las etapas de proyección de las que el filtro no depende.

Considera un pipeline con las siguientes etapas:

{
$addFields: {
maxTime: { $max: "$times" },
minTime: { $min: "$times" }
}
},
{
$project: {
_id: 1,
name: 1,
times: 1,
maxTime: 1,
minTime: 1,
avgTime: { $avg: ["$maxTime", "$minTime"] }
}
},
{
$match: {
name: "Joe Schmoe",
maxTime: { $lt: 20 },
minTime: { $gt: 5 },
avgTime: { $gt: 7 }
}
}

El optimizador descompone la etapa $match en cuatro filtros individuales, uno para cada clave en el documento de query $match. A continuación, el optimizador mueve cada filtro antes de tantas etapas de proyección como sea posible, creando nuevas etapas $match según sea necesario.

Dado este ejemplo, el optimizador produce automáticamente el siguiente pipeline optimizado:

{ $match: { name: "Joe Schmoe" } },
{ $addFields: {
maxTime: { $max: "$times" },
minTime: { $min: "$times" }
} },
{ $match: { maxTime: { $lt: 20 }, minTime: { $gt: 5 } } },
{ $project: {
_id: 1, name: 1, times: 1, maxTime: 1, minTime: 1,
avgTime: { $avg: ["$maxTime", "$minTime"] }
} },
{ $match: { avgTime: { $gt: 7 } } }

Nota

El pipeline optimizado no está destinado a ejecutarse manualmente. Los pipelines originales y optimizados devuelven los mismos resultados.

Puedes ver el pipeline optimizado en el plan de ejecución.

El filtro $match { avgTime: { $gt: 7 } } depende de la etapa $project para calcular el campo avgTime. La etapa $project es la última etapa de proyección en este pipeline, por lo que el filtro $match en avgTime no se pudo mover.

Los campos maxTime y minTime se calculan en la etapa $addFields, pero no tienen dependencia de la etapa $project. El optimizador creó una nueva etapa $match para los filtros en estos campos y la colocó antes de la etapa $project.

El filtro $match { name: "Joe Schmoe" }no utiliza ningún valor calculado ni en la etapa $project ni en la etapa $addFields, por lo que se trasladó a una nueva etapa $match antes de ambas etapas de proyección.

Después de la optimización, el filtro { name: "Joe Schmoe" } está en una etapa $match al comienzo del pipeline. Esto tiene el beneficio adicional de permitir que la agregación use un índice en el campo name al realizar la consulta inicial de la colección.

Cuando tienes una secuencia con $sort seguida de una $match, el $match se mueve antes del $sort para minimizar el número de objetos a ordenar. Por ejemplo, si el pipeline consta de las siguientes etapas:

{ $sort: { age : -1 } },
{ $match: { status: 'A' } }

Durante la fase de optimización, el optimizador transforma la secuencia a la siguiente:

{ $match: { status: 'A' } },
{ $sort: { age : -1 } }

Cuando sea posible, cuando el pipeline tenga la etapa $redact inmediatamente seguida de la etapa $match, la agregación puede a veces añadir una parte de la etapa $match antes de la etapa $redact. Si la etapa $match añadida está al inicio de un pipeline, la agregación puede utilizar un índice y aplicar la query a la colección para limitar el número de documentos que ingresan en el pipeline. Consulta Mejorar el rendimiento con índices y filtros de documentos para obtener más información.

Por ejemplo, si el pipeline consta de las siguientes etapas:

{ $redact: { $cond: { if: { $eq: [ "$level", 5 ] }, then: "$$PRUNE", else: "$$DESCEND" } } },
{ $match: { year: 2014, category: { $ne: "Z" } } }

El optimizador puede añadir la misma etapa $match antes de la etapa $redact:

{ $match: { year: 2014 } },
{ $redact: { $cond: { if: { $eq: [ "$level", 5 ] }, then: "$$PRUNE", else: "$$DESCEND" } } },
{ $match: { year: 2014, category: { $ne: "Z" } } }

Cuando tengas una secuencia con $project o $unset seguida de $skip, el $skip se mueve antes de $project. Por ejemplo, si el pipeline consta de las siguientes etapas:

{ $sort: { age : -1 } },
{ $project: { status: 1, name: 1 } },
{ $skip: 5 }

Durante la fase de optimización, el optimizador transforma la secuencia a la siguiente:

{ $sort: { age : -1 } },
{ $skip: 5 },
{ $project: { status: 1, name: 1 } }

Cuando sea posible, la fase de optimización fusiona una etapa del pipeline con su predecesora. Generalmente, la fusión ocurre después de cualquier optimización de reordenamiento de secuencias.

Cuando una $sort precede a una $limit, el optimizador puede fusionar la $limit en la $sort si ninguna etapa intermedia modifica el número de documentos (por ejemplo, $unwind, $group). MongoDB no fusionará la $limit en la $sort si hay etapas de pipeline que cambian el número de documentos entre las etapas $sort y $limit.

Por ejemplo, si el pipeline consta de las siguientes etapas:

{ $sort : { age : -1 } },
{ $project : { age : 1, status : 1, name : 1 } },
{ $limit: 5 }

Durante la fase de optimización, el optimizador fusiona la secuencia en lo siguiente:

{
"$sort" : {
"sortKey" : {
"age" : -1
},
"limit" : Long(5)
}
},
{ "$project" : {
"age" : 1,
"status" : 1,
"name" : 1
}
}

Esto permite que la operación de clasificación solo mantenga los n mejores resultados a medida que avanza, donde n es el límite especificado, y MongoDB solo necesita almacenar n elementos en memoria [1]. Consulte $sort Operador y memoria para obtener más información.

Nota

Optimización de secuencias con $skip

Si hay una etapa $skip entre las etapas $sort y $limit, MongoDB fusionará la $limit en la etapa $sort y aumentará el valor de $limit por la cantidad de $skip. Consulta $sort + $skip + $limit Secuencia para ver un ejemplo.

[1] La optimización sigue aplicándose cuando allowDiskUse es true y los elementos de n superan el límite de memoria de agregación.

Cuando una $limit sigue inmediatamente a otra $limit, las dos etapas pueden fusionarse en una sola $limit donde el monto límite es el menor de los dos montos límite iniciales. Por ejemplo, un pipeline contiene la siguiente secuencia:

{ $limit: 100 },
{ $limit: 10 }

Entonces, la segunda etapa $limit puede fusionarse con la primera etapa $limit y resultar en una única etapa $limit, donde la cantidad límite 10 es el mínimo de los dos límites iniciales 100 y 10.

{ $limit: 10 }

Cuando una $skip sigue inmediatamente a otra $skip, las dos etapas pueden fusionarse en una sola $skip donde la cantidad de omisión es la suma de las dos cantidades iniciales. Por ejemplo, un pipeline contiene la siguiente secuencia:

{ $skip: 5 },
{ $skip: 2 }

Entonces, la segunda $skip etapa puede fusionarse con la primera $skip etapa y resultar en una sola $skip etapa donde la cantidad de salto 7 es la suma de los dos límites iniciales 5 y 2.

{ $skip: 7 }

Cuando una $match sigue inmediatamente a otra $match, las dos etapas pueden fusionarse en una sola $match combinando las condiciones con un $and. Por ejemplo, un pipeline contiene la siguiente secuencia:

{ $match: { year: 2014 } },
{ $match: { status: "A" } }

Entonces, la segunda etapa $match puede fusionarse con la primera etapa $match y dar lugar a una única $match etapa

{ $match: { $and: [ { "year" : 2014 }, { "status" : "A" } ] } }

Cuando $unwind sigue inmediatamente a $lookup, y el $unwind opera en el campo as del $lookup, el optimizador fusiona el $unwind en la etapa $lookup. Esto evita la creación de grandes documentos intermedios. Además, si $unwind es seguido por un $match en cualquier subcampo as del $lookup, el optimizador también unifica el $match.

Por ejemplo, un pipeline contiene la siguiente secuencia:

{
$lookup: {
from: "otherCollection",
as: "resultingArray",
localField: "x",
foreignField: "y"
}
},
{ $unwind: "$resultingArray" },
{ $match: {
"resultingArray.foo": "bar"
}
}

El optimizador fusiona las etapas $unwind y $match en la etapa $lookup. Si ejecutas la agregación con la opción explain, la salida de explain muestra las etapas coalescidas:

{
$lookup: {
from: "otherCollection",
as: "resultingArray",
localField: "x",
foreignField: "y",
let: {},
pipeline: [
{
$match: {
"foo": {
"$eq": "bar"
}
}
}
],
unwinding: {
"preserveNullAndEmptyArrays": false
}
}
}

Puedes ver este pipeline optimizado en el plan de explicación.

El campo unwinding mostrado en el resultado explain anterior difiere de la etapa $unwind. El campo unwinding muestra cómo se optimiza internamente el pipeline. La etapa $unwind descompone un campo de arreglo de los documentos de entrada y produce un documento para cada elemento.

MongoDB puede usar el motor de ejecución de query basado en ranuras para ejecutar ciertas etapas del pipeline cuando se cumplen condiciones específicas. En la mayoría de los casos, el motor de ejecución basado en ranuras ofrece un mejor rendimiento y menores costos de CPU y memoria en comparación con el motor de query clásico.

Nota

A partir de la versión 7.0.17, el motor de ejecución de consultas basado en ranuras ya no está habilitado de forma predeterminada en las versiones de parche de 7.0. Si desea que sus consultas utilicen el motor de ejecución de consultas basado en ranuras, actualice a la versión 8.0, donde está habilitado de forma predeterminada.

Para verificar que se utiliza el motor de ejecución basado en ranuras, se debe ejecutar la agregación con la opción explain. Esta opción genera información sobre el plan del query de la agregación. Para obtener más información sobre el uso de explain con agregaciones, consulte Información sobre la operación de pipelines de agregación.

Las siguientes secciones describen:

  • Las condiciones en las que se utiliza el motor de ejecución basado en ranuras para la agregación.

  • Cómo verificar si se utilizó el motor de ejecución basado en ranuras.

Nuevo en la versión 5.2.

A partir de la versión 5.2, MongoDB usa el motor de ejecución de query basado en ranuras para ejecutar $group etapas si se cumple alguna de las siguientes condiciones:

  • $group es la primera etapa en el pipeline.

  • Todas las etapas anteriores en la pipeline también pueden ejecutarse mediante el motor de ejecución basado en ranuras.

Cuando se usa el motor de ejecución de query basado en ranuras para $group, los resultados de la explicación incluyen queryPlanner.winningPlan.queryPlan.stage: "GROUP".

La ubicación del objeto queryPlanner depende de si el pipeline contiene etapas posteriores a la etapa $group que no pueden ejecutarse usando el motor de ejecución basado en ranuras.

  • Si $group es la última etapa o todas las etapas posteriores a $group pueden ejecutarse usando el motor de ejecución basado en ranuras, el objeto queryPlanner está en el objeto de salida explain de nivel superior (explain.queryPlanner).

  • Si el pipeline contiene etapas posteriores a $group que no pueden ejecutarse usando el motor de ejecución basado en ranuras, el objeto queryPlanner está en explain.stages[0].$cursor.queryPlanner.

Novedades en la versión 6.0.

A partir de la versión 6.0, MongoDB puede utilizar el motor de ejecución de query basado en ranuras para ejecutar las etapas $lookup si todas las etapas anteriores del pipeline también pueden ejecutarse con el motor de ejecución basado en ranuras y ninguna de las siguientes condiciones se cumple:

  • La operación $lookup ejecuta un pipeline en una colección externa. Para ver un ejemplo de este tipo de operación, consulta Condiciones de unión y sub-query en una colección externa.

  • Los localField o foreignField de $lookup especifican componentes numéricos. Por ejemplo: { localField: "restaurant.0.review" }.

  • El campo from de cualquier $lookup en el pipeline especifica una vista o una colección fragmentada.

Cuando se usa el motor de ejecución de query basado en ranuras para $lookup, los resultados de la explicación incluyen queryPlanner.winningPlan.queryPlan.stage: "EQ_LOOKUP". EQ_LOOKUP significa “búsqueda de igualdad”.

La ubicación del objeto queryPlanner depende de si el pipeline contiene etapas posteriores a la etapa $lookup que no pueden ejecutarse usando el motor de ejecución basado en ranuras.

  • Si $lookup es la última etapa o todas las etapas posteriores a $lookup pueden ejecutarse usando el motor de ejecución basado en ranuras, el objeto queryPlanner está en el objeto de salida explain de nivel superior (explain.queryPlanner).

  • Si el pipeline contiene etapas posteriores a $lookup que no pueden ejecutarse usando el motor de ejecución basado en ranuras, el objeto queryPlanner está en explain.stages[0].$cursor.queryPlanner.

Las siguientes secciones muestran cómo puede mejorar el rendimiento de la agregación utilizando índices y filtros de documentos.

Un pipeline de agregación puede utilizar índices de la colección de entrada para mejorar el rendimiento. El uso de un índice limita la cantidad de documentos que una etapa procesa. Idealmente, un índice puede cubrir la query de etapa. Una query cubierta tiene un rendimiento especialmente alto, ya que el índice devuelve todos los documentos coincidentes.

Por ejemplo, un pipeline que consta de $match, $sort, $group puede beneficiarse de índices en cada etapa:

  • Un índice en el campo de query $match identifica eficientemente los datos relevantes

  • Un índice en el campo de ordenación devuelve los datos en orden ascendente para la etapa $sort.

  • Un índice en el campo de agrupación que coincide con el orden de $sort devuelve todos los valores de campo necesarios para la etapa $group, convirtiéndola en una query cubierta.

Para determinar si un pipeline utiliza índices, se debe revisar el plan del query y buscar los planes IXSCAN o DISTINCT_SCAN.

Nota

En algunos casos, el planificador de query usa un plan de índice DISTINCT_SCAN que devuelve un documento por cada valor de clave de índice. DISTINCT_SCAN se ejecuta más rápido que IXSCAN si hay múltiples documentos por cada valor de clave. Sin embargo, los parámetros de escaneo de índices podrían afectar la comparación de tiempo de DISTINCT_SCAN y IXSCAN.

Para las primeras etapas del pipeline de agregación, se debe considerar la indexación de los campos de query. Las etapas que pueden beneficiarse de los índices son las siguientes:

$match etapa
Durante la etapa $match, el servidor puede utilizar un índice si $match es la primera etapa en el pipeline, después de cualquier optimización del planificador de queries.
$sort etapa
Durante la etapa $sort, el servidor puede utilizar un índice si la etapa no está precedida por una etapa de $project, $unwind o $group.
$group etapa

Durante la etapa $group, el servidor puede usar un índice para encontrar rápidamente el documento $first o el documento $last en cada grupo si la etapa cumple con ambas condiciones:

  • El pipeline sorts y groups por el mismo campo.

  • La etapa $group solo utiliza el operador de acumulación $first o $last.

Consultar Optimizaciones de rendimiento de $group para ver un ejemplo.

$geoNear etapa
El servidor siempre usa un índice para la etapa $geoNear, ya que requiere un índice geoespacial.

Además, las etapas posteriores en el pipeline que recuperan datos de otras colecciones no modificadas pueden utilizar índices en esas colecciones para optimización. Estas etapas incluyen lo siguiente:

Si la operación de agregación requiere solo un subconjunto de los documentos en una colección, se deben filtrar primero los documentos:

  • Se deben usar las etapas $match, $limit y $skip para restringir los documentos que ingresan al pipeline.

  • Cuando sea posible, se debe colocar $match al inicio del pipeline para utilizar índices que escaneen los documentos coincidentes en una colección.

  • $match seguido de $sort al inicio del pipeline equivale a una sola query con ordenación, y puede utilizar un índice.

Un pipeline contiene una secuencia de $sort seguido de un $skip seguido de un $limit:

{ $sort: { age : -1 } },
{ $skip: 10 },
{ $limit: 5 }

El optimizador lleva a cabo $sort + $limit Coalescencia para transformar la secuencia en lo siguiente:

{
"$sort" : {
"sortKey" : {
"age" : -1
},
"limit" : Long(15)
}
},
{
"$skip" : Long(10)
}

MongoDB aumenta la $limit cantidad con el reordenamiento.

Tip

Volver

Rutas de campos

En esta página