Skip to content

Agregación y GROUP BY ​

Los dueños de negocio no preguntan "¿qué filas hay?", preguntan "¿cuánto vendí este mes?", "¿cuántos clientes tengo por ciudad?", "¿cuál es el ticket promedio?". Para eso existen las funciones de agregación y GROUP BY. Aquí aprenderás a resumir datos, agruparlos, filtrar grupos con HAVING y entender el orden en que SQL ejecuta realmente una consulta.

Concepto ​

Una función de agregación toma muchas filas y devuelve un solo valor. Es como pasar el ticket del súper por la calculadora: muchos productos entran, un total sale.

GROUP BY hace lo mismo pero por grupos: es como separar los tickets en montones por sucursal y sumar cada montón.

txt
productos                      GROUP BY categoria_id         resultado
┌───────────┬────────┐        ┌───────────────────────┐     ┌──────────┬──────────┐
│ categoria │ precio │        │ cat 1: 899,1250,3499  │ ──► │ cat 1    │ 3 prod.  │
│ 1         │ 899    │        │ cat 2: 349,289        │ ──► │ cat 2    │ 2 prod.  │
│ 1         │ 1250   │  ───►  │ cat 3: 459,199        │ ──► │ cat 3    │ 2 prod.  │
│ 2         │ 349    │        │ NULL : 99             │ ──► │ NULL     │ 1 prod.  │
│ ...       │ ...    │        └───────────────────────┘     └──────────┴──────────┘

Funciones de agregación ​

FunciónQué calculaIgnora NULL
COUNT(*)Número de filasNo (cuenta todas)
COUNT(columna)Filas donde la columna no es NULLSí
COUNT(DISTINCT columna)Valores distintosSí
SUM(columna)SumaSí
AVG(columna)PromedioSí
MIN(columna) / MAX(columna)Mínimo / máximo (también textos y fechas)Sí
sql
SELECT
  COUNT(*)              AS total_productos,
  COUNT(categoria_id)   AS con_categoria,
  SUM(stock)            AS piezas_en_almacen,
  ROUND(AVG(precio), 2) AS precio_promedio,
  MIN(precio)           AS mas_barato,
  MAX(precio)           AS mas_caro
FROM productos;
total_productoscon_categoriapiezas_en_almacenprecio_promediomas_baratomas_caro
87275880.3899.003499.00

Fíjate: COUNT(*) da 8 y COUNT(categoria_id) da 7 porque el Cable USB-C tiene NULL.

sql
-- ¿En cuántas ciudades distintas tenemos clientes?
SELECT COUNT(DISTINCT ciudad) AS ciudades FROM clientes;   -- 4

-- Primer y último registro de clientes
SELECT MIN(fecha_registro) AS primero, MAX(fecha_registro) AS ultimo FROM clientes;
-- primero: 2024-01-15 | ultimo: 2024-04-02

GROUP BY: agregar por grupos ​

sql
-- Productos, precio promedio y stock por categoría
SELECT
  categoria_id,
  COUNT(*)              AS productos,
  ROUND(AVG(precio), 2) AS precio_promedio,
  SUM(stock)            AS stock_total
FROM productos
GROUP BY categoria_id
ORDER BY categoria_id;
categoria_idproductosprecio_promediostock_total
131882.6740
22319.0070
32329.0065
NULL199.00100

(En PostgreSQL la fila NULL aparece al final; en MySQL al principio.) Los NULL forman su propio grupo.

sql
-- Pedidos por estado
SELECT estado, COUNT(*) AS cantidad
FROM pedidos
GROUP BY estado
ORDER BY cantidad DESC, estado;
estadocantidad
entregado2
cancelado1
enviado1
pendiente1

El total de cada pedido ​

Esta es una de las consultas más importantes de cualquier tienda:

sql
SELECT
  pedido_id,
  SUM(cantidad)                   AS piezas,
  SUM(cantidad * precio_unitario) AS total
FROM detalle_pedido
GROUP BY pedido_id
ORDER BY pedido_id;
pedido_idpiezastotal
131597.00
241847.00
313499.00
441146.00
51899.00

Agrupar por varias columnas ​

sql
-- Pedidos por cliente y estado
SELECT cliente_id, estado, COUNT(*) AS pedidos
FROM pedidos
GROUP BY cliente_id, estado
ORDER BY cliente_id;
cliente_idestadopedidos
1entregado1
1enviado1
2entregado1
3pendiente1
4cancelado1

Agrupar por una expresión: ventas por mes ​

sql
-- PostgreSQL y MySQL
SELECT
  EXTRACT(YEAR FROM p.fecha)  AS anio,
  EXTRACT(MONTH FROM p.fecha) AS mes,
  SUM(d.cantidad * d.precio_unitario) AS ventas
FROM pedidos p, detalle_pedido d
WHERE p.id = d.pedido_id
  AND p.estado <> 'cancelado'
GROUP BY EXTRACT(YEAR FROM p.fecha), EXTRACT(MONTH FROM p.fecha)
ORDER BY anio, mes;
aniomesventas
202453444.00
202464645.00

(Aquí unimos dos tablas con una coma y una condición en WHERE; en la siguiente lección aprenderás la forma moderna: JOINs.)

Error común: columnas fuera del GROUP BY

sql
-- ERROR en PostgreSQL y en MySQL 8 (ONLY_FULL_GROUP_BY)
SELECT categoria_id, nombre, COUNT(*)
FROM productos
GROUP BY categoria_id;

¿Qué nombre debería mostrar el grupo de Electrónica, si tiene 3 productos? No hay respuesta, por eso falla. Regla: toda columna del SELECT debe estar en el GROUP BY o dentro de una función de agregación.

HAVING: filtrar grupos ​

WHERE filtra filas antes de agrupar. HAVING filtra grupos después de agregar.

sql
-- Pedidos cuyo total supera 1,500
SELECT pedido_id, SUM(cantidad * precio_unitario) AS total
FROM detalle_pedido
GROUP BY pedido_id
HAVING SUM(cantidad * precio_unitario) > 1500
ORDER BY total DESC;
pedido_idtotal
33499.00
21847.00
11597.00
sql
-- Clientes con más de un pedido
SELECT cliente_id, COUNT(*) AS pedidos
FROM pedidos
GROUP BY cliente_id
HAVING COUNT(*) > 1;
cliente_idpedidos
12

Combinando ambos:

sql
-- Categorías con stock total mayor a 50, contando solo productos de más de 150 pesos
SELECT categoria_id, SUM(stock) AS stock_total
FROM productos
WHERE precio > 150                 -- filtra filas
GROUP BY categoria_id
HAVING SUM(stock) > 50             -- filtra grupos
ORDER BY categoria_id;
categoria_idstock_total
270
365
WHEREHAVING
FiltraFilas individualesGrupos
CuándoAntes de agruparDespués de agrupar
Puede usar SUM, COUNT...NoSí

Buena práctica

Si una condición no usa funciones de agregación, ponla en WHERE, no en HAVING: la BD descarta filas antes y trabaja menos.

El orden lógico de ejecución ​

Escribes la consulta en un orden, pero la BD la ejecuta en otro. Entenderlo resuelve el 80 % de las dudas:

txt
Orden en que lo escribes        Orden en que se ejecuta
────────────────────────        ───────────────────────
1. SELECT                        1. FROM / JOIN    ← ¿de dónde salen los datos?
2. FROM                          2. WHERE          ← filtra filas
3. WHERE                         3. GROUP BY       ← forma grupos
4. GROUP BY                      4. HAVING         ← filtra grupos
5. HAVING                        5. SELECT         ← calcula columnas y alias
6. ORDER BY                      6. DISTINCT
7. LIMIT                         7. ORDER BY       ← ordena
                                 8. LIMIT / OFFSET ← recorta

Consecuencias prácticas:

  • No puedes usar un alias del SELECT en el WHERE (el WHERE se ejecuta antes). WHERE total > 100 falla si total es un alias.
  • Sí puedes usar el alias en ORDER BY (se ejecuta después).
  • MySQL permite alias en HAVING como extensión; PostgreSQL no. Repetir la expresión funciona en ambos.

Caso real: tablero de la tienda ​

sql
-- Resumen general de ventas (sin cancelados)
SELECT
  COUNT(DISTINCT p.id)                                   AS pedidos,
  SUM(d.cantidad * d.precio_unitario)                    AS ventas_totales,
  ROUND(SUM(d.cantidad * d.precio_unitario)
        / COUNT(DISTINCT p.id), 2)                       AS ticket_promedio,
  SUM(d.cantidad)                                        AS piezas_vendidas
FROM pedidos p, detalle_pedido d
WHERE p.id = d.pedido_id
  AND p.estado <> 'cancelado';
pedidosventas_totalesticket_promediopiezas_vendidas
48089.002022.2512

Ejercicios ​

  1. ¿Cuántos clientes hay por ciudad? Ordena de mayor a menor.
  2. ¿Cuántas piezas se han vendido de cada producto (todos los pedidos)? Muestra producto_id y piezas, ordenado por piezas descendente.
  3. ¿Qué productos han generado más de 1,000 pesos en ventas (suma de cantidad × precio_unitario)?
  4. ¿Cuál es el valor total del inventario (precio × stock) por categoría?
  5. ¿Por qué falla SELECT ciudad, COUNT(*) AS total FROM clientes WHERE total > 1 GROUP BY ciudad;? Corrígela.
Ver solución
sql
-- 1
SELECT ciudad, COUNT(*) AS clientes
FROM clientes
GROUP BY ciudad
ORDER BY clientes DESC, ciudad;
ciudadclientes
Ciudad de México2
Guadalajara1
Monterrey1
Puebla1
sql
-- 2
SELECT producto_id, SUM(cantidad) AS piezas
FROM detalle_pedido
GROUP BY producto_id
ORDER BY piezas DESC, producto_id;
producto_idpiezas
75
12
42
21
31
51
61
sql
-- 3
SELECT producto_id, SUM(cantidad * precio_unitario) AS ventas
FROM detalle_pedido
GROUP BY producto_id
HAVING SUM(cantidad * precio_unitario) > 1000
ORDER BY ventas DESC;
producto_idventas
33499.00
11798.00
21250.00
sql
-- 4
SELECT categoria_id, SUM(precio * stock) AS valor_inventario
FROM productos
GROUP BY categoria_id
ORDER BY categoria_id;
categoria_idvalor_inventario
152470.00
222630.00
316835.00
NULL9900.00
  1. Porque WHERE se ejecuta antes de agrupar: ni el alias ni el conteo existen todavía. Usa HAVING:
sql
SELECT ciudad, COUNT(*) AS total
FROM clientes
GROUP BY ciudad
HAVING COUNT(*) > 1;
ciudadtotal
Ciudad de México2

Resumen ​

  • COUNT, SUM, AVG, MIN, MAX resumen muchas filas en un valor; ignoran NULL (salvo COUNT(*)).
  • GROUP BY calcula esas funciones por grupo. Toda columna del SELECT debe estar agrupada o agregada.
  • WHERE filtra filas antes de agrupar; HAVING filtra grupos después.
  • Orden de ejecución: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT.
  • El total de un pedido es SUM(cantidad * precio_unitario) agrupado por pedido.

Academia Web — aprende, copia y construye.