Curso de MySQL

Cláusula GROUP BY en MySQL: agrupar y calcular totales

Por Víctor Peña · Publicado el

Cláusula GROUP BY en MySQL

Hola, ¿cómo están? Continuando con el curso de MySQL, hoy veremos GROUP BY. Es lo que convierte una lista de registros en un reporte.

¡Empecemos!

Las funciones de agregación

Antes del agrupamiento, las funciones que calculan sobre un conjunto de filas:

SELECT COUNT(*) FROM citas;              -- cuántas filas
SELECT SUM(costo) FROM citas;            -- suma
SELECT AVG(costo) FROM citas;            -- promedio
SELECT MAX(costo) FROM citas;            -- el mayor
SELECT MIN(costo) FROM citas;            -- el menor

Cada una devuelve un solo valor a partir de muchas filas.

COUNT(*) y COUNT(columna)

Una diferencia sutil y muy importante:

SELECT COUNT(*) FROM pacientes;           -- todas las filas: 4
SELECT COUNT(telefono) FROM pacientes;    -- solo las que tienen teléfono: 3

COUNT(columna) ignora los NULL. Es útil cuando lo aprovechas a propósito, y una fuente de confusión cuando no lo sabes.

Y para contar valores distintos:

SELECT COUNT(DISTINCT doctor_id) FROM citas;   -- cuántos doctores tuvieron citas

GROUP BY: calcular por grupos

Sin agrupar, las funciones calculan sobre toda la tabla. Con GROUP BY, calculan por cada grupo:

SELECT
    estado,
    COUNT(*) AS cantidad,
    SUM(costo) AS total
FROM citas
GROUP BY estado;

Resultado:

estado      | cantidad | total
atendida    | 3        | 750.00
programada  | 2        | 450.00
cancelada   | 1        | 250.00

MySQL separa las filas en grupos según el valor de estado y aplica las funciones dentro de cada uno.

La regla que causa la mitad de los errores

En una consulta con GROUP BY, cada columna del SELECT debe estar en el GROUP BY o dentro de una función de agregación.

-- Incorrecto: ¿qué fecha debería mostrar de todo el grupo?
SELECT estado, fecha, COUNT(*)
FROM citas
GROUP BY estado;

-- Correcto
SELECT estado, COUNT(*), MAX(fecha) AS ultima_fecha
FROM citas
GROUP BY estado;

La lógica: si agrupas seis citas en un solo renglón, no tiene sentido pedir «la fecha» — hay seis. Tienes que decir cuál quieres: la máxima, la mínima, o agrupar también por fecha.

MySQL antiguo permitía romper esta regla y devolvía una fecha cualquiera. Las versiones modernas lo prohíben con el modo ONLY_FULL_GROUP_BY, y hacen bien: ese comportamiento daba reportes incorrectos sin avisar.

Agrupar por varias columnas

SELECT
    estado,
    doctor_id,
    COUNT(*) AS cantidad,
    SUM(costo) AS total
FROM citas
GROUP BY estado, doctor_id
ORDER BY estado, total DESC;

Ahora cada combinación de estado y doctor es un grupo distinto.

GROUP BY con JOIN

Aquí es donde se arman los reportes de verdad:

SELECT
    e.nombre AS especialidad,
    COUNT(c.id) AS total_citas,
    SUM(c.costo) AS ingresos,
    ROUND(AVG(c.costo), 2) AS costo_promedio
FROM citas c
JOIN doctores d ON c.doctor_id = d.id
JOIN especialidades e ON d.especialidad_id = e.id
WHERE c.estado = 'atendida'
GROUP BY e.id, e.nombre
ORDER BY ingresos DESC;

Fíjate en GROUP BY e.id, e.nombre: agrupar por el id garantiza que dos especialidades con el mismo nombre no se mezclen, y añadir el nombre permite mostrarlo.

HAVING: filtrar grupos

Esta es la otra confusión clásica del tema.

  • WHERE filtra filas, antes de agrupar.
  • HAVING filtra grupos, después de agrupar.
SELECT
    d.nombre,
    d.apellido,
    COUNT(c.id) AS total_citas
FROM doctores d
JOIN citas c ON d.id = c.doctor_id
WHERE c.estado = 'atendida'          -- filtra citas antes de agrupar
GROUP BY d.id, d.nombre, d.apellido
HAVING COUNT(c.id) >= 2              -- filtra doctores después de agrupar
ORDER BY total_citas DESC;

Esa consulta responde: «doctores con al menos 2 citas atendidas».

No puedes usar una función de agregación en el WHERE:

-- Error: COUNT() no existe todavía cuando se evalúa el WHERE
WHERE COUNT(c.id) >= 2

Recuerda el orden de ejecución que vimos en la lección de SELECT: WHERE se ejecuta antes que GROUP BY, así que en ese momento los grupos aún no existen.

Cuál usar

Una regla práctica: si la condición se puede aplicar a una fila suelta, va en WHERE. Si necesita el grupo completo, va en HAVING.

Y por rendimiento, prefiere WHERE siempre que puedas: filtrar antes de agrupar significa agrupar menos filas.

GROUP_CONCAT: unir valores del grupo

Una función muy útil y poco conocida:

SELECT
    e.nombre AS especialidad,
    COUNT(d.id) AS total_doctores,
    GROUP_CONCAT(d.apellido ORDER BY d.apellido SEPARATOR ', ') AS doctores
FROM especialidades e
LEFT JOIN doctores d ON e.id = d.especialidad_id
GROUP BY e.id, e.nombre;

Resultado:

especialidad   | total_doctores | doctores
Cardiología    | 1              | Mendoza
Pediatría      | 2              | Quiroga, Vargas
Traumatología  | 1              | Rojas

Reúne en una sola celda todos los valores del grupo. Perfecta para listados donde no quieres una fila por cada elemento relacionado.

Contar por rangos

Combinando CASE con agregación:

SELECT
    CASE
        WHEN costo >= 300 THEN 'Alto'
        WHEN costo >= 200 THEN 'Medio'
        ELSE 'Bajo'
    END AS rango,
    COUNT(*) AS cantidad
FROM citas
GROUP BY rango
ORDER BY cantidad DESC;

Agrupar por periodos de tiempo

Uno de los usos más frecuentes en reportes:

-- Por mes
SELECT
    DATE_FORMAT(fecha, '%Y-%m') AS mes,
    COUNT(*) AS citas,
    SUM(costo) AS ingresos
FROM citas
WHERE estado = 'atendida'
GROUP BY mes
ORDER BY mes;

-- Por día de la semana
SELECT
    DAYNAME(fecha) AS dia,
    COUNT(*) AS citas
FROM citas
GROUP BY dia;

WITH ROLLUP: totales generales

Agrega una fila con el total de todo:

SELECT
    estado,
    COUNT(*) AS cantidad,
    SUM(costo) AS total
FROM citas
GROUP BY estado WITH ROLLUP;

La última fila tiene NULL en estado y contiene la suma general. Muy cómodo para reportes que necesitan el total al pie.

Ejercicios

  1. Cuántas citas tiene cada paciente, ordenadas de mayor a menor.
  2. Ingresos totales por doctor, solo de citas atendidas.
  3. Especialidades con más de un doctor.
  4. Promedio de costo por estado de cita, redondeado a dos decimales.
  5. Pacientes con más de una cita cancelada.

Para la tercera y la quinta necesitas HAVING.

Errores comunes

  • Columnas en el SELECT que no están en el GROUP BY ni dentro de una función.
  • Usar una función de agregación en el WHERE.
  • Confundir COUNT(*) con COUNT(columna) cuando hay nulos.
  • Poner en HAVING condiciones que corresponden al WHERE, perdiendo rendimiento.
  • Olvidar LEFT JOIN cuando quieres incluir grupos vacíos: con INNER, una especialidad sin doctores desaparece.

Para cerrar

GROUP BY es la herramienta de los reportes: totales, promedios, conteos por categoría. Todo panel de control que hayas visto se apoya en consultas como estas.

Las dos ideas clave: cada columna del SELECT debe estar agrupada o agregada, y WHERE filtra filas mientras HAVING filtra grupos.

En la siguiente lección empezamos con el bloque de optimización.

Saludos y éxitos.

Norvic Software

Desarrollamos el software que tu empresa necesita

Somos una fábrica de software en Bolivia. Construimos sistemas a medida y aplicaciones móviles, y llevamos Inteligencia Artificial a las empresas que ya tienen un sistema funcionando.

Solicitar cotizaciónVer todos los servicios

Cotización sin costo · Respuesta directa por WhatsApp