Apex Academy
Módulo 3·06·25 min

Funciones de agregación

Contar, sumar, promediar y agrupar dentro de la base de datos: te llega el total, no las diez mil filas que lo forman.

En Admin esUn informe de resumen agrupado por etapa, o un campo roll-up

✓ En esta sub-lección

  1. 01Contar registros con COUNT() sin traerlos.
  2. 02Agrupar con GROUP BY y leer SUM, AVG, MIN y MAX desde AggregateResult.
  3. 03Distinguir WHERE (filtra filas antes de agrupar) de HAVING (filtra grupos después).

La nutria: ¿Te acuerdas? · Repaso de la lección 5

¿Cómo filtras por una variable accId en SOQL?

Muchas preguntas de negocio no piden registros sino números: cuántos casos abiertos hay, cuánto pipeline tiene cada etapa, cuál es el ticket medio. Podrías traer todas las oportunidades y sumarlas en un bucle, pero sería como exportar el informe a Excel para sumar una columna que el propio informe ya sabía sumar.

La nutria: El paralelo de Admin

Yo hacía esto con un informe de resumen: agrupaba filas (Agrupar por: Etapa) y mostraba totales por grupo (Suma de Importe, Recuento de registros). Un campo roll-up hace lo mismo en el registro padre: COUNT, SUM, MIN o MAX de sus hijos. SOQL tiene exactamente esas funciones, más AVG, y GROUP BY es tu «Agrupar filas por».

Contar: COUNT()

Apex
Integer openCases = [SELECT COUNT() FROM Case WHERE IsClosed = false];
System.debug('Casos abiertos: ' + openCases);
COUNT() sin nada dentro es especial: la consulta devuelve directamente un Integer. Es el «Recuento de registros» del informe.

Agrupar: GROUP BY y AggregateResult

Apex
List<AggregateResult> byStage = [
    SELECT StageName, COUNT(Id) deals, SUM(Amount) total, AVG(Amount) average
    FROM Opportunity
    WHERE IsClosed = false
    GROUP BY StageName
];

for (AggregateResult ar : byStage) {
    String  stage = (String)  ar.get('StageName');
    Integer deals = (Integer) ar.get('deals');
    Decimal total = (Decimal) ar.get('total');
    System.debug(stage + ': ' + deals + ' oportunidades, ' + total);
}
SELECT StageName, COUNT(Id) n, SUM(Amount) total FROM Opportunity WHERE IsClosed = false GROUP BY StageName HAVING COUNT(Id) >= 3
  • Prospecting · 30.000
  • Proposal · 90.000
  • Prospecting · 25.000
  • Negotiation · 180.000
  • Proposal · 70.000
  • Prospecting · 35.000
  • Negotiation · 130.000
  • Proposal · 80.000

Paso 1 de 4

Ocho oportunidades abiertas sueltas. Sin agregado, las ocho viajarían a Apex y tendrías que sumar tú en un bucle.

Las filas entran en la base de datos; a tu código solo sale una fila por grupo.
  • Con GROUP BY ya no recibes Opportunity, sino AggregateResult: una fila por grupo, no por registro.
  • Cada total lleva un alias (deals, total, average) escrito justo detrás de la función. Con ese alias lo lees: ar.get('total'). Sin alias, Salesforce los llama expr0, expr1… y tu código se vuelve ilegible.
  • get() devuelve Object, el tipo comodín. Para usarlo como número haces el casting del Módulo 1: (Decimal) ar.get('total').
  • Todo campo del SELECT tiene que estar agrupado o dentro de una función. Pedir Name aquí no compila: ¿el nombre de cuál de las cuarenta oportunidades del grupo?

WHERE antes, HAVING después

WHERE decide qué filas entran en los grupos. HAVING decide qué grupos salen, mirando sus totales. En un informe de resumen no hay forma sencilla de decir «enséñame solo las etapas con más de 100.000»: lo miras a ojo. HAVING lo hace por ti.

Apex
// Cuentas con más de 5 casos abiertos: candidatas a revisión de servicio
List<AggregateResult> busy = [
    SELECT AccountId, COUNT(Id) openCases
    FROM Case
    WHERE IsClosed = false            // filas: solo casos abiertos
    GROUP BY AccountId
    HAVING COUNT(Id) > 5              // grupos: solo cuentas con más de 5
    ORDER BY COUNT(Id) DESC
];
En HAVING y ORDER BY repites la función (COUNT(Id)), no el alias. El alias solo sirve para leer el resultado.
FunciónQué calculaEn el informe o roll-up
COUNT() / COUNT(Id)Cuántos registrosRecuento de registros · roll-up COUNT
COUNT_DISTINCT(campo)Cuántos valores distintosRecuento único
SUM(campo)La sumaSuma · roll-up SUM
AVG(campo)La mediaPromedio
MIN / MAX(campo)El menor / el mayorMín / Máx · roll-up MIN / MAX

La nutria: ¿Y por qué no un Flow? Porque Get Records no agrupa

El pipeline por etapa lo intenté con un flow programado. Get Records no tiene GROUP BY ni HAVING: devuelve filas, no totales por grupo. Tocaba traer todas las oportunidades abiertas al flow y repartirlas por etapa a mano, con un Loop y una Decision con una rama por etapa, que había que tocar cada vez que Ventas creaba una etapa nueva. En SOQL, GROUP BY StageName hace el reparto en la base de datos y te devuelve una fila por etapa, sea cual sea el número de etapas.