Saltar al contenido
group-by.sql · devschool

GROUP BY y HAVING en SQL

Lección 9 de 22 · 13 min de lectura · Actualizado el

En esta lección
  1. Contar por grupo
  2. Sumar por grupo
  3. Una tabla con más datos
  4. Agrupar por varias columnas
  5. La regla de oro
  6. Filtrar grupos: HAVING
  7. WHERE o HAVING
  8. El orden de ejecución
  9. Agrupar por una expresión: ventas por mes
  10. Ordenar los grupos
  11. Errores frecuentes
  12. Resumen

Con las funciones de agregación calculas un total de toda la tabla: cuántos clientes hay, cuánto se ha vendido. Pero casi siempre la pregunta real es por algo: clientes por ciudad, ventas por mes, gasto por cliente. Para eso está GROUP BY: junta las filas que tienen el mismo valor en una o varias columnas y aplica las funciones de agregación a cada grupo por separado.

En esta lección aprenderás a agrupar por una y por varias columnas, a filtrar grupos con HAVING, a agrupar por meses y a evitar el error más típico de GROUP BY en MySQL.

Contar por grupo

¿Cuántos clientes hay en cada ciudad?

SELECT ciudad, COUNT(*) AS clientes
FROM clientes
GROUP BY ciudad;
ciudadclientes
Bilbao1
Madrid2
Santander2

Lo que hace la base de datos:

  1. Separa las filas en montones según la ciudad.
  2. Cuenta las filas de cada montón.
  3. Devuelve una fila por montón.
clientes                         GROUP BY ciudad           resultado
Ana García   Santander  ─┐
Marta Ruiz   Santander  ─┴──►  [Santander] 2 filas  ──►  Santander | 2
Luis Pérez   Bilbao     ────►  [Bilbao]    1 fila   ──►  Bilbao    | 1
Pablo Díaz   Madrid     ─┐
Eva Martín   Madrid     ─┴──►  [Madrid]    2 filas  ──►  Madrid    | 2

En cada grupo puedes usar todas las funciones de agregación que quieras:

SELECT ciudad,
       COUNT(*) AS clientes,
       ROUND(AVG(edad), 1) AS edad_media,
       MIN(edad) AS menor,
       MAX(edad) AS mayor
FROM clientes
GROUP BY ciudad;
ciudadclientesedad_mediamenormayor
Bilbao122.02222
Madrid223.01729
Santander239.53445

Sumar por grupo

Total gastado por cada cliente:

SELECT cliente_id, COUNT(*) AS pedidos, SUM(importe) AS total
FROM pedidos
GROUP BY cliente_id
ORDER BY total DESC;
cliente_idpedidostotal
31120.50
2160.00
1257.99
5133.25

Ana (cliente 1) tiene dos pedidos: 45.00 + 12.99 = 57.99. Pablo (cliente 4) no aparece porque no tiene ninguna fila en pedidos: GROUP BY solo crea grupos con las filas que existen. En la lección de JOIN verás cómo mostrar el nombre en lugar del id y cómo incluir a los clientes sin pedidos.

Una tabla con más datos

Las tablas del curso son pequeñas y todos los pedidos son de septiembre. Para practicar con varias columnas y con meses, crearemos las ventas de una tienda de montaña durante el verano:

CREATE TABLE ventas (
  id INT PRIMARY KEY,
  fecha DATE NOT NULL,
  categoria VARCHAR(50) NOT NULL,
  canal VARCHAR(10) NOT NULL,
  importe DECIMAL(8, 2) NOT NULL
);

INSERT INTO ventas VALUES
  (1, '2026-07-03', 'Escalada', 'Tienda', 89.90),
  (2, '2026-07-15', 'Camping', 'Web', 120.00),
  (3, '2026-07-28', 'Escalada', 'Web', 59.00),
  (4, '2026-08-02', 'Montaña', 'Tienda', 45.50),
  (5, '2026-08-09', 'Escalada', 'Tienda', 89.90),
  (6, '2026-08-21', 'Camping', 'Web', 24.95),
  (7, '2026-08-30', 'Camping', 'Tienda', 24.95),
  (8, '2026-09-05', 'Escalada', 'Web', 59.00),
  (9, '2026-09-18', 'Montaña', 'Tienda', 45.50);

Ventas por categoría:

SELECT categoria, COUNT(*) AS ventas, SUM(importe) AS total
FROM ventas
GROUP BY categoria;
categoriaventastotal
Camping3169.90
Escalada4297.80
Montaña291.00

Agrupar por varias columnas

Si pones varias columnas en el GROUP BY, se forma un grupo por cada combinación distinta de valores. Ventas por categoría y por canal:

SELECT categoria, canal, COUNT(*) AS ventas, SUM(importe) AS total
FROM ventas
GROUP BY categoria, canal
ORDER BY categoria, canal;
categoriacanalventastotal
CampingTienda124.95
CampingWeb2144.95
EscaladaTienda2179.80
EscaladaWeb2118.00
MontañaTienda291.00

No aparece “Montaña / Web” porque no hay ninguna venta con esa combinación. El orden de las columnas en el GROUP BY no cambia los grupos (GROUP BY canal, categoria da las mismas cinco filas); lo que decide cómo se ven es el ORDER BY.

La regla de oro

En una consulta con GROUP BY, cada columna del SELECT debe ser:

  • una columna que aparece en el GROUP BY, o
  • una función de agregación (COUNT, SUM, AVG…).
SELECT ciudad, nombre, COUNT(*)   -- ✗ ¿qué nombre, si en Madrid hay dos?
FROM clientes
GROUP BY ciudad;

El grupo de Madrid es una fila del resultado, pero dentro tiene dos nombres: Pablo y Eva. No hay forma correcta de elegir uno. Lo que hace cada sistema con esta consulta lo verás en los errores frecuentes.

Filtrar grupos: HAVING

¿Qué ciudades tienen más de un cliente?

SELECT ciudad, COUNT(*) AS clientes
FROM clientes
GROUP BY ciudad
HAVING COUNT(*) > 1;
ciudadclientes
Madrid2
Santander2

HAVING es como un WHERE para grupos: se aplica después de calcular los agregados, así que puede preguntar por ellos. Otro ejemplo, las categorías que han vendido más de 150 €:

SELECT categoria, SUM(importe) AS total
FROM ventas
GROUP BY categoria
HAVING SUM(importe) > 150;
categoriatotal
Camping169.90
Escalada297.80

Nota: MySQL y SQLite te dejan usar el alias en el HAVING (HAVING total > 150), pero PostgreSQL no. Si repites la expresión, funciona en todos.

WHERE o HAVING

Los dos filtran, pero en momentos distintos:

WHEREHAVING
Qué filtraFilasGrupos
CuándoAntes de agruparDespués de agrupar
¿Puede usar COUNT, SUM…?NoSí
¿Necesita GROUP BY?NoCasi siempre se usa con él
EjemploWHERE edad >= 18HAVING COUNT(*) > 1

Muchas consultas usan los dos:

-- Ciudades con más de un cliente mayor de edad
SELECT ciudad, COUNT(*) AS adultos
FROM clientes
WHERE edad >= 18          -- 1.º quita a Pablo (17)
GROUP BY ciudad           -- 2.º agrupa
HAVING COUNT(*) > 1;      -- 3.º se queda con los grupos de más de 1
ciudadadultos
Santander2

Madrid ya no aparece: al quitar a Pablo, solo le queda Eva.

Si una condición se refiere a columnas normales, ponla en el WHERE aunque también funcionase en el HAVING: descartar filas antes de agrupar es más rápido, porque hay menos filas que repartir.

El orden de ejecución

Aunque se escriba empezando por SELECT, la base de datos lo ejecuta en este orden. Entenderlo explica muchos errores:

  1. FROM: coge la tabla.
  2. WHERE: filtra filas.
  3. GROUP BY: agrupa.
  4. HAVING: filtra grupos.
  5. SELECT: elige las columnas y calcula.
  6. ORDER BY: ordena.
  7. LIMIT: recorta.

Por ejemplo, por eso en WHERE no puedes usar un alias que has creado en el SELECT: cuando se ejecuta el WHERE, el SELECT todavía no ha ocurrido. Y por eso tampoco puedes poner COUNT(*) en el WHERE: los grupos aún no existen.

Agrupar por una expresión: ventas por mes

No tienes por qué agrupar por una columna tal cual: puedes agrupar por el resultado de una expresión. El caso más típico es agrupar por mes. La columna fecha tiene un valor distinto casi en cada fila, así que hay que quedarse solo con el año y el mes y agrupar por eso.

Aquí cambian los sistemas, porque cada uno tiene su función para formatear fechas:

-- MySQL / MariaDB
SELECT DATE_FORMAT(fecha, '%Y-%m') AS mes, COUNT(*) AS ventas, SUM(importe) AS total
FROM ventas
GROUP BY DATE_FORMAT(fecha, '%Y-%m')
ORDER BY mes;

-- SQLite
SELECT strftime('%Y-%m', fecha) AS mes, COUNT(*) AS ventas, SUM(importe) AS total
FROM ventas
GROUP BY strftime('%Y-%m', fecha)
ORDER BY mes;

Las dos dan el mismo resultado:

mesventastotal
2026-073268.90
2026-084185.30
2026-092104.50

La idea es idéntica; cambian el nombre de la función y el orden de los argumentos:

MySQLSQLitePostgreSQL
FunciónDATE_FORMAT(fecha, formato)strftime(formato, fecha)TO_CHAR(fecha, 'YYYY-MM')
Primer argumentoLa fechaEl formatoLa fecha
Año con 4 cifras%Y%YYYYY
Mes con 2 cifras%m%mMM

¿Por qué '%Y-%m' y no solo el mes? Por dos motivos. Primero, si hubiera ventas de julio de 2025 y de julio de 2026, agrupar solo por mes las mezclaría en un mismo grupo. Segundo, el formato 2026-07 se ordena bien como texto: '2026-07' va antes que '2026-08', y '2025-12' antes que '2026-01'.

Consejo: en MySQL también puedes agrupar por dos columnas, GROUP BY YEAR(fecha), MONTH(fecha), que devuelven números. Es igual de válido y útil si luego quieres operar con el mes.

Y como puedes mezclar expresiones y columnas, tienes las ventas por mes y categoría, con la más vendida primero dentro de cada mes (en SQLite):

SELECT strftime('%Y-%m', fecha) AS mes, categoria, SUM(importe) AS total
FROM ventas
GROUP BY mes, categoria
ORDER BY mes, total DESC;
mescategoriatotal
2026-07Escalada148.90
2026-07Camping120.00
2026-08Escalada89.90
2026-08Camping49.90
2026-08Montaña45.50
2026-09Escalada59.00
2026-09Montaña45.50

Aquí se agrupa por el alias mes, cosa que MySQL, PostgreSQL y SQLite permiten. Si tu sistema no lo admite, repite la expresión en el GROUP BY. También puedes agrupar por un CASE para crear tus propias categorías, como tramos de edad o de importe.

Ordenar los grupos

Sin ORDER BY, el orden de los grupos no está garantizado (aunque a menudo salgan ordenados por la columna agrupada). Para un ranking, ordena por el agregado:

SELECT categoria, SUM(importe) AS total
FROM ventas
GROUP BY categoria
ORDER BY total DESC;
categoriatotal
Escalada297.80
Camping169.90
Montaña91.00

En el ORDER BY sí puedes usar el alias, porque se ejecuta después del SELECT. Con LIMIT te quedas con el primero del ranking, por ejemplo la categoría con más ventas:

SELECT categoria, COUNT(*) AS ventas
FROM ventas
GROUP BY categoria
ORDER BY ventas DESC
LIMIT 1;   -- Escalada, 4

Errores frecuentes

Columnas que no están en el GROUP BY (ONLY_FULL_GROUP_BY)

Es el error estrella. Con la consulta de la regla de oro:

SELECT ciudad, nombre, COUNT(*)
FROM clientes
GROUP BY ciudad;
  • MySQL 5.7 y posteriores tienen activado por defecto el modo ONLY_FULL_GROUP_BY y dan el error: Expression #2 of SELECT list is not in GROUP BY clause and contains nonaggregated column ‘nombre’ … this is incompatible with sql_mode=only_full_group_by.
  • PostgreSQL da un error parecido: column “clientes.nombre” must appear in the GROUP BY clause or be used in an aggregate function.
  • SQLite y las versiones antiguas de MySQL no protestan y devuelven un nombre cualquiera de cada grupo. Parece que funciona, pero el resultado no significa nada.

Encontrarás en Internet el consejo de desactivar ONLY_FULL_GROUP_BY. No lo hagas: el modo te protege de resultados aleatorios. La solución correcta es una de estas:

  1. Añadir la columna al GROUP BY si de verdad quieres un grupo por cada combinación.
  2. Usar un agregado sobre ella: MIN(nombre), COUNT(DISTINCT nombre)…
  3. Agrupar por la clave primaria. Si agrupas por c.id, MySQL y PostgreSQL saben que cada id tiene un único nombre y te dejan mostrar c.nombre. Aun así, lo más claro es escribir GROUP BY c.id, c.nombre, como verás en JOIN.

Otros errores

  • Poner un agregado en el WHERE: WHERE COUNT(*) > 1 da error (misuse of aggregate). Va en el HAVING.
  • Usar HAVING para filtrar filas normales: funciona en algunos casos, pero es más lento y confuso. Lo que no sea un agregado, al WHERE.
  • Agrupar por mes sin el año: mezcla meses de años distintos.
  • Esperar que salgan los grupos vacíos: GROUP BY no inventa filas. Si una categoría no tiene ventas, no aparece.
  • Confiar en el orden sin ORDER BY.

Nota: si la columna por la que agrupas tiene NULL, todas esas filas forman un único grupo con valor NULL. Lo tienes explicado en valores NULL.

Resumen

Quieres…Escribe
Un resultado por cada valorGROUP BY columna
Un resultado por cada combinaciónGROUP BY col1, col2
Agrupar por mes (MySQL)GROUP BY DATE_FORMAT(fecha, '%Y-%m')
Agrupar por mes (SQLite)GROUP BY strftime('%Y-%m', fecha)
Filtrar filas antes de agruparWHERE
Filtrar gruposHAVING COUNT(*) > 1
Ranking de gruposORDER BY SUM(importe) DESC
  • En el SELECT, solo columnas del GROUP BY o funciones de agregación.
  • Orden de escritura: SELECT, FROM, WHERE, GROUP BY, HAVING, ORDER BY, LIMIT.
  • Orden de ejecución: FROM, WHERE, GROUP BY, HAVING, SELECT, ORDER BY, LIMIT.

En la siguiente lección aprenderás a combinar tablas con JOIN.

Pon a prueba lo que has aprendido

[SQL] ¿Qué consulta devuelve las ciudades cuya edad media de clientes es mayor que 30?

[SQL] Ana (cliente 1) tiene dos pedidos: 45.00 y 12.99. El resto de clientes tiene uno. ¿Qué devuelve esta consulta?
SELECT cliente_id, SUM(importe)
FROM pedidos
WHERE importe > 20
GROUP BY cliente_id
HAVING COUNT(*) > 1;

¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.