Funciones agregadas — Referencia de SQL en Marketing Cloud
Las funciones agregadas que MC SQL soporta — COUNT, SUM, AVG, MIN, MAX — más la distinción entre COUNT(*) y COUNT(col), la trampa del SUM-devuelve-NULL-en-vacío, y el patrón MAX-por-grupo que reemplaza window functions en MC.
Las funciones agregadas reducen muchas filas a un valor (o un valor por grupo, con GROUP BY). En MC SQL el set soportado es el estándar — COUNT, SUM, AVG, MIN, MAX — y los bugs son mayormente sobre cómo cada una trata NULL y los result sets vacíos. COUNT(*) cuenta todas las filas; COUNT(col) saltea NULLs. SUM sobre cero filas devuelve NULL, no 0. AVG ignora silenciosamente las filas NULL del numerador y del denominador. La adición MC-específica: MAX-por-grupo es el patrón que reemplaza window functions cuando esas no están disponibles.
Sintaxis oficial
-- COUNT(*) vs COUNT(col) vs COUNT(DISTINCT col)
SELECT
COUNT(*) AS TotalRows, -- todas las filas, NULL o no
COUNT(LoyaltyTier) AS RowsWithTier, -- saltea LoyaltyTier NULL
COUNT(DISTINCT LoyaltyTier) AS UniqueTiers -- valores únicos no-NULL
FROM master_subscribers;
-- SUM, AVG, MIN, MAX
SELECT
EmailDomain,
SUM(TotalSpend) AS DomainSpend,
AVG(TotalSpend) AS AvgSpend,
MIN(LastPurchase) AS FirstPurchase,
MAX(LastPurchase) AS MostRecentPurchase,
COUNT(*) AS Subs
FROM master_subscribers
GROUP BY EmailDomain
HAVING COUNT(*) > 100; -- HAVING filtra agregados; WHERE filtra filas
-- Agregación condicional con CASE
SELECT
EmailDomain,
COUNT(*) AS Total,
COUNT(CASE WHEN LoyaltyTier = 'gold' THEN 1 END) AS Gold,
COUNT(CASE WHEN LoyaltyTier = 'silver' THEN 1 END) AS Silver,
SUM(CASE WHEN Status = 'Active' THEN 1 ELSE 0 END) AS ActiveCount
FROM master_subscribers
GROUP BY EmailDomain;El set soportado entre la mayoría de los tenants SFMC:
| Función | Qué hace | Comportamiento NULL |
|---|---|---|
| COUNT(*) | Cuenta todas las filas en el grupo | Siempre cuenta (NULL o no) |
| COUNT(col) | Cuenta filas donde col es no-NULL | Saltea NULLs |
| COUNT(DISTINCT col) | Cuenta valores únicos no-NULL | Saltea NULLs, deduplica |
| SUM(col) | Suma de valores no-NULL | Devuelve NULL si cero filas / todas NULL |
| AVG(col) | Promedio de valores no-NULL | Saltea NULLs en numerador Y denominador |
| MIN(col) | Valor mínimo no-NULL | Devuelve NULL si cero filas / todas NULL |
| MAX(col) | Valor máximo no-NULL | Devuelve NULL si cero filas / todas NULL |
HAVING filtra el resultado de GROUP BY (usalo para "grupos con más de N filas"). WHERE filtra filas antes del grouping. No pongas WHERE COUNT(*) > 100 — el parser lo rechaza; usá HAVING en su lugar.
Referencia:
Lo que sobrevive en producción
COUNT(*) vs COUNT(col) — la diferencia es si los NULLs cuentan
COUNT(*) cuenta cada fila del grupo, sin importar los NULLs. COUNT(LoyaltyTier) cuenta solo filas donde LoyaltyTier es no-NULL. Se usan seguido como intercambiables y no deberían.
-- "Cuántos subscribers hay en este dominio?" — cada fila, NULL OK
SELECT EmailDomain, COUNT(*) AS Subscribers
FROM master_subscribers
GROUP BY EmailDomain;
-- "Cuántos subscribers en este dominio tienen un tier de loyalty configurado?"
-- Excluye NULLs naturalmente
SELECT EmailDomain, COUNT(LoyaltyTier) AS WithTier
FROM master_subscribers
GROUP BY EmailDomain;
-- "Cuántos tiers distintos existen en este dominio?"
-- DISTINCT + COUNT
SELECT EmailDomain, COUNT(DISTINCT LoyaltyTier) AS UniqueTiers
FROM master_subscribers
GROUP BY EmailDomain;La elección equivocada produce números que "se ven correctos" pero no son lo que el negocio pidió. Siempre decí qué estás contando en un alias de columna así el próximo lector puede auditar el math.
SUM sobre cero filas devuelve NULL, no 0
Si el WHERE filtra todo en un grupo, SUM devuelve NULL en lugar de 0. Después la matemática aguas abajo (SUM(...) + 100) devuelve NULL por la lógica de tres valores. La defensa: envolvé los agregados en COALESCE cuando el resultado alimenta más math o una columna del destino que no permite NULL.
-- EN RIESGO — si ninguna fila matchea el filtro, TotalSpend es NULL,
-- que después se propaga por cualquier math siguiente
SELECT
EmailDomain,
SUM(TotalSpend) AS TotalSpend,
SUM(TotalSpend) + 100 AS TotalSpendPlus100 -- NULL + 100 = NULL
FROM master_subscribers
WHERE Status = 'Inactive' -- puede matchear cero filas
GROUP BY EmailDomain;
-- DURABLE — COALESCE garantiza un default numérico
SELECT
EmailDomain,
COALESCE(SUM(TotalSpend), 0) AS TotalSpend,
COALESCE(SUM(TotalSpend), 0) + 100 AS TotalSpendPlus100
FROM master_subscribers
WHERE Status = 'Inactive'
GROUP BY EmailDomain;MIN y MAX tienen el mismo comportamiento — devuelven NULL cuando no existen valores no-NULL. COUNT(*) es la excepción: siempre devuelve un entero (0 si no hay filas).
AVG saltea NULLs del numerador Y del denominador
Si hacés AVG(LoyaltyDelta) sobre 100 filas pero solo 70 tienen un LoyaltyDelta no-NULL, el promedio es SUM(no-null) / 70, no SUM(no-null) / 100. Esto generalmente no es lo que el negocio pidió — querían "promedio sobre todos los subscribers, tratando los faltantes como cero".
-- EN RIESGO — el promedio ignora filas NULL en AMBOS la suma y el conteo
SELECT EmailDomain, AVG(LoyaltyDelta) AS AvgDelta
FROM master_subscribers
GROUP BY EmailDomain;
-- 70 de 100 filas no-null → devuelve SUM(70) / 70
-- EXPLÍCITO — coalesce los NULLs a 0 primero si esa es la regla de negocio
SELECT EmailDomain, AVG(COALESCE(LoyaltyDelta, 0)) AS AvgDelta
FROM master_subscribers
GROUP BY EmailDomain;
-- → devuelve SUM(70 no-null + 0 por los 30 nulls) / 100
-- O numerador + denominador explícitos con lógica condicional
SELECT EmailDomain, SUM(LoyaltyDelta) * 1.0 / COUNT(*) AS AvgDelta
FROM master_subscribers
GROUP BY EmailDomain;Decidí qué math quiere el negocio y escribilo explícito. El comportamiento implícito de AVG es correcto seguido suficiente como para que confíes en él, después está mal en el reporte que va al liderazgo.
MAX-por-grupo: el patrón de dedup que reemplaza window functions
MC SQL no soporta confiablemente ROW_NUMBER() OVER (...) entre ediciones (ver gotchas — #4). El dedup-por-más-reciente estándar de SFMC usa MAX más un self-join en dos Activities stagéadas:
-- Activity 1: encontrar la fecha máxima por grupo
INSERT INTO de_stg_max_purchase_per_email
SELECT
EmailAddress,
MAX(LastPurchase) AS MaxPurchase
FROM master_subscribers
GROUP BY EmailAddress;
-- Activity 2: elegir las fila(s) que matcheen el máximo
INSERT INTO de_stg_dedup_subs
SELECT m.SubscriberKey, m.EmailAddress, m.LastPurchase
FROM master_subscribers m
INNER JOIN de_stg_max_purchase_per_email s
ON m.EmailAddress = s.EmailAddress
AND m.LastPurchase = s.MaxPurchase;Si múltiples filas empatan en MaxPurchase para el mismo email (misma fecha), todas sobreviven — lo que puede o no ser lo que querés. Agregá un desempate (ej. MIN(SubscriberKey)) en Activity 2 si necesitás una sola fila por grupo.
Decisión rápida
Usá COUNT(*) cuando:
- Estás contando cada fila sin importar NULLs.
Usá COUNT(col) cuando:
- La pregunta de negocio es "filas que tienen un valor no-NULL en esta columna".
Usá COUNT(DISTINCT col) cuando:
- Necesitás valores únicos, y el origen es chico (< ~500k filas). Para orígenes más grandes, stagéa
SELECT DISTINCTa un DE primero, despuésCOUNT(*).
Envolvé SUM / MIN / MAX en COALESCE cuando:
- El resultado alimenta más math, o la columna del destino no permite NULL.
Usá SUM(...) / COUNT(*) explícito en lugar de AVG cuando:
- Necesitás que las filas NULL se traten como cero en el promedio.
Usá el patrón de dos Activities MAX-por-grupo cuando:
- Necesitás semántica de "fila más reciente por grupo". No dependas de
ROW_NUMBER().
Relacionado
- Basics — subset de T-SQL soportado
- SELECT — funciones agregadas en proyección
- WHERE — filtrado pre-agregado (vs
HAVINGpost-agregado) - JOIN — patrón de staging para MAX-por-grupo
- CASE — agregación condicional (
SUM(CASE WHEN x THEN 1 ELSE 0 END)) - Funciones numéricas — precisión DECIMAL al sumar dinero
- MC SQL gotchas — #4 (window functions / CTEs edition-dependent), #5 (NULL de tres valores)
Una página de referencia de funciones más viene: Null Functions.
Más snippets how-to para debugging común en producción — sends de email, largo de valores, alcance de contactos, etc.