
SQL: Agrupamiento y Funciones Agregadas con GROUP BY
- Dacadev
- Programación
- 14 de junio de 2026
Tabla de Contenido
Cuando una tabla empieza a acumular miles o millones de filas, consultar registros individuales deja de ser suficiente. En análisis, reportes y validaciones operativas necesitamos resumir datos: cuántas rentas hizo cada cliente, cuánto pagó en total, cuál fue el monto máximo o qué categorías concentran más actividad. Para eso usamos agrupamiento en SQL con GROUP BY y funciones agregadas como COUNT, SUM, AVG, MIN y MAX.
Este es el artículo 4 de la serie SQL. Venimos de trabajar con lógica condicional con CASE, y ahora vamos a convertir filas transaccionales en resultados agregados listos para análisis.
Note
En este artículo aprenderás a usar GROUP BY, ordenar grupos con ORDER BY, filtrar agregaciones con HAVING, contar valores distintos con DISTINCT y crear agrupaciones por una columna, múltiples columnas o expresiones.
Conceptos de agrupamiento en SQL
GROUP BY toma filas que comparten el mismo valor en una o más expresiones y las compacta en un grupo lógico. Si seleccionamos únicamente customer_id desde rental, SQL puede devolver cada cliente una sola vez:
mysql> SELECT customer_id
-> FROM rental
-> GROUP BY customer_id;
+-------------+
| customer_id |
+-------------+
| 1 |
| 2 |
| 3 |
| 4 |
| 5 |
| 6 |
...
| 594 |
| 595 |
| 596 |
| 597 |
| 598 |
| 599 |
+-------------+
599 rows in set (0.00 sec)
El resultado ya no representa rentas individuales, sino el conjunto de clientes que aparecen en la tabla. El siguiente paso natural es medir cuántas filas contiene cada grupo. Para eso usamos COUNT(*):
mysql> SELECT customer_id, count(*)
-> FROM rental
-> GROUP BY customer_id;
+-------------+----------+
| customer_id | count(*) |
+-------------+----------+
| 1 | 32 |
| 2 | 27 |
| 3 | 26 |
| 4 | 22 |
| 5 | 38 |
| 6 | 28 |
...
| 594 | 27 |
| 595 | 30 |
| 596 | 28 |
| 597 | 25 |
| 598 | 22 |
| 599 | 19 |
+-------------+----------+
599 rows in set (0.01 sec)
Info
COUNT(*) le indica a SQL que cuente todas las filas contenidas dentro de cada grupo definido por GROUP BY. No cuenta clientes únicos; cuenta filas dentro del grupo actual.
Ordenar resultados agregados con ORDER BY
Una agregación suele ser más útil cuando puedes priorizarla. Por ejemplo, para identificar los clientes con más rentas, ordenamos por la segunda columna del SELECT, que en este caso es count(*):
mysql> SELECT customer_id, count(*)
-> FROM rental
-> GROUP BY customer_id
-> ORDER BY 2 DESC;
+-------------+----------+
| customer_id | count(*) |
+-------------+----------+
| 148 | 46 |
| 526 | 45 |
| 236 | 42 |
| 144 | 42 |
| 75 | 41 |
...
| 248 | 15 |
| 110 | 14 |
| 281 | 14 |
| 61 | 14 |
| 318 | 12 |
+-------------+----------+
599 rows in set (0.01 sec)
Tip
ORDER BY puede usar el nombre de una columna, un alias o la posición de la columna dentro del SELECT. Aun así, en consultas mantenibles suele ser más claro usar alias explícitos.
Filtrar grupos con HAVING
WHERE filtra filas antes de que ocurra el agrupamiento. HAVING, en cambio, filtra grupos después de calcular las agregaciones. Esta diferencia es crítica: no puedes usar WHERE count(*) >= 40 porque count(*) todavía no existe en esa fase lógica de la consulta.
Consideremos clientes con al menos 40 rentas:
mysql> SELECT customer_id, count(*)
-> FROM rental
-> GROUP BY customer_id
-> HAVING count(*) >= 40;
+-------------+----------+
| customer_id | count(*) |
+-------------+----------+
| 75 | 41 |
| 144 | 42 |
| 148 | 46 |
| 197 | 40 |
| 236 | 42 |
| 469 | 40 |
| 526 | 45 |
+-------------+----------+
7 rows in set (0.01 sec)
Analicemos el flujo conceptual: FROM obtiene las filas, WHERE descarta filas individuales, GROUP BY crea grupos, las funciones agregadas calculan métricas y HAVING descarta grupos completos. Por último, ORDER BY organiza el resultado final.
Funciones agregadas comunes
Las bases de datos implementan distintas funciones agregadas, pero estas aparecen prácticamente en cualquier motor SQL:
| Función | Qué devuelve |
|---|---|
MAX() | El valor máximo dentro de un conjunto |
MIN() | El valor mínimo dentro de un conjunto |
AVG() | El promedio de los valores del conjunto |
SUM() | La suma total de los valores del conjunto |
COUNT() | La cantidad de valores o filas del conjunto |
Sin GROUP BY, estas funciones operan sobre toda la tabla o sobre el conjunto filtrado por WHERE. Observemos un resumen global de pagos:
mysql> SELECT MAX(amount) max_amt,
-> MIN(amount) min_amt,
-> AVG(amount) avg_amt,
-> SUM(amount) tot_amt,
-> COUNT(*) num_payments
-> FROM payment;
+---------+---------+----------+----------+--------------+
| max_amt | min_amt | avg_amt | tot_amt | num_payments |
+---------+---------+----------+----------+--------------+
| 11.99 | 0.00 | 4.200667 | 67416.51 | 16049 |
+---------+---------+----------+----------+--------------+
1 row in set (0.09 sec)
Aquí SQL devuelve una sola fila porque existe un grupo implícito: todo el resultado de payment.
Grupos implícitos vs grupos explícitos
Cuando usamos una función agregada sin GROUP BY, SQL resume todo el conjunto como un único grupo. Eso es útil para métricas globales, pero no sirve si necesitamos una métrica por cliente, actor, categoría o periodo.
Para obtener métricas por cliente, definimos el grupo explícitamente:
mysql> SELECT customer_id,
-> MAX(amount) max_amt,
-> MIN(amount) min_amt,
-> AVG(amount) avg_amt,
-> SUM(amount) tot_amt,
-> COUNT(*) num_payments
-> FROM payment
-> GROUP BY customer_id;
+-------------+---------+---------+----------+---------+--------------+
| customer_id | max_amt | min_amt | avg_amt | tot_amt | num_payments |
+-------------+---------+---------+----------+---------+--------------+
| 1 | 9.99 | 0.99 | 3.708750 | 118.68 | 32 |
| 2 | 10.99 | 0.99 | 4.767778 | 128.73 | 27 |
| 3 | 10.99 | 0.99 | 5.220769 | 135.74 | 26 |
| 4 | 8.99 | 0.99 | 3.717273 | 81.78 | 22 |
| 5 | 9.99 | 0.99 | 3.805789 | 144.62 | 38 |
| 6 | 7.99 | 0.99 | 3.347143 | 93.72 | 28 |
...
| 594 | 8.99 | 0.99 | 4.841852 | 130.73 | 27 |
| 595 | 10.99 | 0.99 | 3.923333 | 117.70 | 30 |
| 596 | 6.99 | 0.99 | 3.454286 | 96.72 | 28 |
| 597 | 8.99 | 0.99 | 3.990000 | 99.75 | 25 |
| 598 | 7.99 | 0.99 | 3.808182 | 83.78 | 22 |
| 599 | 9.99 | 0.99 | 4.411053 | 83.81 | 19 |
+-------------+---------+---------+----------+---------+--------------+
599 rows in set (0.04 sec)
La regla práctica es directa: cualquier columna no agregada en el SELECT debe representar la granularidad del resultado y, por lo tanto, debe estar en el GROUP BY.
Warning
Algunos motores permiten seleccionar columnas que no están agrupadas ni agregadas, pero el resultado puede ser ambiguo o depender de configuraciones del motor. Para consultas portables y correctas, declara explícitamente la granularidad.
Contar valores distintos con COUNT(DISTINCT ...)
COUNT(*) cuenta filas. COUNT(columna) cuenta valores no nulos de esa columna. COUNT(DISTINCT columna) cuenta valores únicos, ignorando duplicados.
Esto permite diferenciar entre el número de pagos registrados y el número de clientes distintos que hicieron pagos:
mysql> SELECT COUNT(customer_id) num_rows,
-> COUNT(DISTINCT customer_id) num_customers
-> FROM payment;
+----------+---------------+
| num_rows | num_customers |
+----------+---------------+
| 16049 | 599 |
+----------+---------------+
1 row in set (0.01 sec)
Observemos que num_rows refleja eventos de pago, mientras num_customers refleja entidades únicas. Esa distinción evita errores frecuentes en reportes, especialmente cuando mezclamos métricas transaccionales con métricas de negocio.
Generar grupos según la granularidad requerida
La calidad de una consulta agregada depende de elegir la granularidad correcta. No es lo mismo agrupar por cliente, por cliente y mes, por actor y clasificación, o por una expresión derivada de una fecha.
Agrupamiento por una columna
Agrupar por una sola columna es el caso base. En este ejemplo obtenemos cuántas películas tiene asociadas cada actor:
mysql> SELECT actor_id, count(*)
-> FROM film_actor
-> GROUP BY actor_id;
+----------+----------+
| actor_id | count(*) |
+----------+----------+
| 1 | 19 |
| 2 | 25 |
| 3 | 22 |
| 4 | 22 |
...
| 197 | 33 |
| 198 | 40 |
| 199 | 15 |
| 200 | 20 |
+----------+----------+
200 rows in set (0.11 sec)
Info
En este resultado cada fila representa un actor y el conteo de películas en las que participa. La consulta ya no describe relaciones individuales actor-película, sino un resumen por actor.
Agrupamiento por múltiples columnas
En algunos casos necesitamos generar grupos que combinen más de una columna. Por ejemplo, contar cuántas películas tiene cada actor por clasificación (G, PG, PG-13, R, NC-17):
mysql> SELECT fa.actor_id, f.rating, count(*)
-> FROM film_actor fa
-> INNER JOIN film f
-> ON fa.film_id = f.film_id
-> GROUP BY fa.actor_id, f.rating
-> ORDER BY 1,2;
+----------+--------+----------+
| actor_id | rating | count(*) |
+----------+--------+----------+
| 1 | G | 4 |
| 1 | PG | 6 |
| 1 | PG-13 | 1 |
| 1 | R | 3 |
| 1 | NC-17 | 5 |
| 2 | G | 7 |
| 2 | PG | 6 |
| 2 | PG-13 | 2 |
| 2 | R | 2 |
| 2 | NC-17 | 8 |
...
| 199 | G | 3 |
| 199 | PG | 4 |
| 199 | PG-13 | 4 |
| 199 | R | 2 |
| 199 | NC-17 | 2 |
| 200 | G | 5 |
| 200 | PG | 3 |
| 200 | PG-13 | 2 |
| 200 | R | 6 |
| 200 | NC-17 | 4 |
+----------+--------+----------+
996 rows in set (0.01 sec)
Aquí la granularidad ya no es “actor”, sino “actor + clasificación”. Cada combinación única genera su propio grupo. Esta técnica es la base para reportes multidimensionales y pivotes posteriores en herramientas de BI.
Agrupamiento mediante expresiones
GROUP BY no está limitado a columnas físicas. También puede agrupar por expresiones, funciones o transformaciones calculadas. Esto es especialmente útil con fechas.
El siguiente ejemplo agrupa rentas por año:
mysql> SELECT extract(YEAR FROM rental_date) year,
-> COUNT(*) how_many
-> FROM rental
-> GROUP BY extract(YEAR FROM rental_date);
+------+----------+
| year | how_many |
+------+----------+
| 2005 | 15862 |
| 2006 | 182 |
+------+----------+
2 rows in set (0.01 sec)
En lugar de agrupar por cada rental_date exacto, reducimos la fecha al año con EXTRACT(YEAR FROM rental_date). El resultado responde una pregunta distinta: volumen anual, no eventos individuales.
Buenas prácticas al trabajar con GROUP BY
Para mantener consultas claras y confiables, considera estas reglas:
- Define primero la granularidad: una fila por cliente, por cliente y mes, por actor y rating, etc.
- Usa alias descriptivos para agregaciones:
num_payments,total_amount,avg_amount. - Aplica
WHEREpara filtrar filas antes de agrupar yHAVINGpara filtrar grupos. - Evita depender de posiciones como
ORDER BY 2en consultas de producción si un alias mejora la legibilidad. - Verifica si necesitas contar filas (
COUNT(*)), valores no nulos (COUNT(columna)) o valores únicos (COUNT(DISTINCT columna)).
Conclusión
GROUP BY cambia la forma en que lees una tabla: pasas de inspeccionar filas individuales a construir resúmenes por la granularidad que necesita tu análisis. Combinado con funciones agregadas, HAVING, DISTINCT y expresiones, se convierte en una herramienta central para reportes, dashboards y validaciones de datos.
En el siguiente artículo de la serie vamos a conectar datos distribuidos entre tablas con consultas a múltiples tablas usando JOINs.

