Agregación

Descripción

La etapa aggregate calcula los resultados agregados (p. ej., recuento, suma) a partir de los documentos que devolvió la etapa anterior.

De manera opcional, cuando se proporciona una expresión de agrupación, se agrupan los documentos según las expresiones proporcionadas y, luego, se aplican funciones de acumulador a cada grupo.

Sintaxis

Para las agregaciones sin agrupamiento, la etapa aggregate toma una o más expresiones de agregador con alias:

Node.js

const cities = await db.pipeline()
  .collection("/cities")
  .aggregate(
      countAll().as("total"),
      average("population").as("avg_population")
  )
  .execute();

Para las agregaciones con agrupamiento, se toman grupos adicionales además de los agregadores:

Node.js

const result = await db.pipeline()
  .collectionGroup('citites')
  .aggregate({
    accumulators: [
      countAll().as('cities'),
      field('population').sum().as('total_popoluation')
    ],
    groups: [field('location.state').as('state')]
  })
  .execute();

Comportamiento

Agregaciones sin agrupación

Crea una colección cities con los siguientes documentos:

Node.js

await db.collection('cities').doc('SF').set({name: 'San Francisco', state: 'CA', country: 'USA', population: 870000});
await db.collection('cities').doc('LA').set({name: 'Los Angeles', state: 'CA', country: 'USA', population: 3970000});
await db.collection('cities').doc('NY').set({name: 'New York', state: 'NY', country: 'USA', population: 8530000});
await db.collection('cities').doc('TOR').set({name: 'Toronto', state: null, country: 'Canada', population: 2930000});
await db.collection('cities').doc('MEX').set({name: 'Mexico City', state: null, country: 'Mexico', population: 9200000});

Para conocer la cantidad total de ciudades y su población promedio, sigue estos pasos:

Node.js

const cities = await db.pipeline()
  .collection("/cities")
  .aggregate(
      countAll().as("total"),
      average("population").as("avg_population")
  )
  .execute();

que produce lo siguiente:

{avg_population: 5100000, total: 5}

Realiza agregaciones en grupos

Si proporcionas un argumento groups, puedes realizar agregaciones en cada grupo distinto.

Por ejemplo, para encontrar la ciudad con la mayor población en cada país y cada estado, haz lo siguiente:

Node.js

const cities = await db.pipeline()
  .collection("/cities")
  .aggregate({
      accumulators: [
          countAll().as("number_of_cities"),
          maximum("population").as("max_population")
      ],
      groups: ["country", "state"]
  })
  .execute();

lo que da como resultado lo siguiente:

{country: "USA", state: "CA", max_population: 3970000, number_of_cities: 2},
{country: "USA", state: "NY", max_population: 8530000, number_of_cities: 1},
{country: "Canada", state: null, max_population: 2930000, number_of_cities: 1},
{country: "Mexico", state: null,  max_population: 9200000, number_of_cities: 1}

Expresiones complejas en el agrupamiento

Además de agrupar por valores de campo, la etapa aggregate admite el agrupamiento por resultados de expresiones complejas. Cualquier expresión válida en una etapa select se puede usar como clave de agrupación. Esto permite una agrupación flexible basada en valores o condiciones calculados.

Por ejemplo, para agrupar por si el campo de estado es nulo y averiguar la población total en cada grupo, usa este código:

Node.js

const cities = await db.pipeline()
  .collection("/cities")
  .aggregate({
      accumulators: [
         sum("population").as("total_population")
      ],
      groups: [equal(field("state"), null).as("state_is_null")]
  })
  .execute();

devolverá lo siguiente:

{state_is_null: true, total_population: 12130000}
{state_is_null: false, total_population: 13370000}

Comportamientos de los agregadores

El comportamiento de agregación de cada función compatible (p. ej., count, sum, avg) se puede encontrar en la página dedicada a las funciones de agregación.

Comportamientos clave del grupo

Cuando agrupa documentos, Firestore usa la semántica de igualdad para determinar si los valores pertenecen al mismo grupo.

Esto significa que los valores equivalentes, por ejemplo, los valores numéricos matemáticamente equivalentes, independientemente del tipo original (número entero de 32 bits, número entero de 64 bits, números de punto flotante, decimal128, etc.), se agrupan.

Por ejemplo, en una colección numerics con diferentes documentos que contienen valores foo de número entero de 32 bits 1, número entero de 64 bits 1L y número de punto flotante 1.0, respectivamente, todos se acumularán en el mismo grupo. Si ejecutas un recuento agrupado por foo, se mostrará lo siguiente:

{foo: 1.0, count: 3}

En estos casos, en los que hay diferentes valores equivalentes presentes en el conjunto de datos, el valor de salida del grupo puede ser cualquiera de estos valores equivalentes. En este ejemplo, foo podría ser 1, 1L o 1.0.

Incluso si parece determinístico, no debes intentar depender del comportamiento de un valor específico seleccionado.

Uso de la memoria

La forma en que se ejecutará la agregación depende de los índices disponibles. Cuando el optimizador de consultas no elige un índice adecuado, el agregado debe almacenar en búfer todos los grupos en la memoria.

En caso de que haya una gran cantidad de grupos o que cada grupo sea muy grande (p. ej., agrupar por valores enormes), es posible que esta etapa se quede sin memoria.

En esos casos, debes aplicar filtros para limitar el conjunto de datos en el que se realizará la agregación, agrupar los datos en menos campos o campos más pequeños, o bien crear índices según lo recomendado para evitar un uso excesivo de la memoria. Query Explain proporcionará información sobre el plan de ejecución de la consulta real y los datos de generación de perfiles para ayudar con la depuración.