Curso SQL · Northwind
Tema 18 de 64
Bloque 4 · Agregar PostgreSQL 17 northwind

GROUP BY

1. Qué es

GROUP BY parte las filas en montones y hace que las agregaciones se calculen una vez por montón, en vez de una vez para toda la tabla.

830 pedidos  →  GROUP BY ship_country  →  21 grupos  →  21 filas de resultado

Es el paso 3 del orden-logico-de-ejecucion, y de ahí sale la regla que gobierna todo el tema:

Después del GROUP BY, la fila individual ya no existe. Solo existen los grupos. Por eso el SELECT únicamente puede pedir dos cosas: columnas por las que agrupaste, o agregaciones. Nada más.

Entender eso quita de golpe el error más frecuente de SQL.

2. Sintaxis

SELECT columna_agrupada, agregación(...)
FROM tabla
WHERE   filtro_de_filas       -- antes de agrupar
GROUP BY columna_agrupada
HAVING  filtro_de_grupos      -- después de agrupar
ORDER BY ...;

Reglas:

  • Se puede agrupar por una o varias columnas, por una expresión, por el alias o por el número de posición.
  • El número de filas del resultado = el número de combinaciones distintas de las columnas agrupadas.
  • Los NULL forman su propio grupo. Uno solo, aunque NULL <> NULL.
  • Sin GROUP BY pero con agregaciones, toda la tabla es un grupo y sale una fila.

3. Ejemplos sobre Northwind

Lo básico:

SELECT ship_country,
       count(*)              AS pedidos,
       round(sum(freight),2) AS flete
FROM orders
GROUP BY ship_country
ORDER BY pedidos DESC
LIMIT 5;
 ship_country | pedidos |  flete
--------------+---------+----------
 Germany      |     122 | 11283.28
 USA          |     122 | 13771.29
 Brazil       |      83 |  4880.19
 France       |      77 |  4237.84
 UK           |      56 |  2954.27

Alemania y Estados Unidos empatan a 122 pedidos, pero el flete de EE. UU. es 2.500 mayor. Ese contraste solo aparece porque pediste dos agregaciones a la vez; con count(*) solo, los dos países parecerían iguales.

3.1 El error que todo el mundo comete

SELECT ship_country, order_id, count(*)
FROM orders
GROUP BY ship_country;
ERROR:  column "orders.order_id" must appear in the GROUP BY clause
        or be used in an aggregate function

El mensaje es literalmente la regla. Y la pregunta que hay detrás es razonable: si Alemania tiene 122 pedidos, ¿cuál de los 122 order_id debería enseñarte? No hay respuesta, así que SQL se niega en vez de inventar.

Las tres salidas, según lo que querías de verdad:

Lo que querías Qué escribir
Un pedido cualquiera del grupo min(order_id) o max(order_id)
Todos los pedidos del grupo string_agg(order_id::text, ', ')
Un desglose más fino Añadir order_id al GROUP BY — pero entonces cada grupo es una fila y el count(*) vale 1

MySQL en modo permisivo acepta esa consulta y devuelve un order_id arbitrario sin avisar. Es una de las razones por las que informes migrados desde MySQL traen números que nadie sabe explicar.

3.2 Agrupar por varias columnas

SELECT ship_country, ship_city, count(*)
FROM orders
GROUP BY ship_country, ship_city
ORDER BY 3 DESC
LIMIT 4;
 ship_country |   ship_city    | count
--------------+----------------+-------
 Brazil       | Rio de Janeiro |    34
 UK           | London         |    33
 Brazil       | Sao Paulo      |    31
 USA          | Boise          |    31

70 grupos, uno por pareja país-ciudad. Cuantas más columnas agrupas, más grupos y menos filas por grupo — hasta el extremo de agrupar por la clave primaria, donde cada grupo es una fila y agrupar deja de servir de nada.

3.3 Agrupar por una expresión

No hace falta que la columna exista:

SELECT extract(year FROM order_date)::int AS anio, count(*)
FROM orders
GROUP BY 1
ORDER BY 1;
 anio | count
------+-------
 1996 |   152
 1997 |   408
 1998 |   270

Tres formas equivalentes de escribir ese GROUP BY: GROUP BY 1 (posición), GROUP BY anio (alias), o repetir extract(...) entero. Las tres funcionan — es la comodidad que GROUP BY concede y HAVING no, y el porqué está en la sección 4 de este mismo tema.

Y el mismo patrón, por mes:

SELECT to_char(order_date, 'YYYY-MM') AS mes, count(*)
FROM orders
WHERE order_date >= '1997-01-01' AND order_date < '1997-04-01'
GROUP BY 1 ORDER BY 1;
   mes   | count
---------+-------
 1997-01 |    33
 1997-02 |    29
 1997-03 |    30

Fíjate en el orden WHERE → GROUP BY: el filtro se aplica a las filas antes de agrupar, así que los meses de 1996 ni siquiera llegan a formar grupo.

3.4 Los NULL forman un grupo

SELECT region, count(*)
FROM customers
GROUP BY region
ORDER BY count(*) DESC
LIMIT 3;
 region | count
--------+-------
        |    60
 SP     |     6
 OR     |     4

La primera fila, con la región vacía, es el grupo de los 60 NULL. GROUP BY los junta todos en uno, aunque NULL = NULL no sea cierto (null-y-la-logica-de-tres-valores). Es una excepción deliberada del estándar: para agrupar, los nulos se consideran "el mismo desconocido".

En un informe conviene etiquetarlos, porque una fila con la primera celda vacía parece un error:

SELECT coalesce(region, '(sin región)') AS region, count(*) FROM customers GROUP BY 1;

3.5 Lo que GROUP BY no puede hacer: inventar grupos vacíos

SELECT category_id, count(*) FROM products GROUP BY category_id ORDER BY category_id;
 category_id | count
-------------+-------
           1 |    12
           2 |    12
           3 |    13
           4 |    10
           5 |     7
           6 |     6
           7 |     5
           8 |    12

Ocho categorías, todas presentes. Pero solo porque todas tienen productos. Si mañana se crea la categoría 9 sin productos, no aparecerá en este resultado: GROUP BY agrupa las filas que hay, no las que podría haber.

Es el mismo problema que con los meses sin pedidos: una serie mensual construida con GROUP BY se salta los meses vacíos y el gráfico pinta una línea continua donde había un agujero. La solución es generar el eje aparte y hacer LEFT JOIN — fechas-sin-actividad.

3.6 Dos niveles a la vez

SELECT employee_id,
       extract(year FROM order_date)::int AS anio,
       count(*)
FROM orders
GROUP BY employee_id, anio
ORDER BY employee_id, anio
LIMIT 6;
 employee_id | anio | count
-------------+------+-------
           1 | 1996 |    26
           1 | 1997 |    55
           1 | 1998 |    42
           2 | 1996 |    16
           2 | 1997 |    41

Esta forma —una fila por combinación— es la larga. La misma información en formato ancho, con los años como columnas, es el pivote con CASE de case-condicionales-en-sql. La larga es la que se puede seguir procesando; la ancha es la que se enseña.

4. Dónde encaja en el orden de ejecución

GROUP BY es el paso 3 del orden-logico-de-ejecucion: después de filtrar filas (WHERE, paso 2) y antes de que se ejecute el SELECT (paso 5). Tres consecuencias.

4.1 El WHERE filtra antes de agrupar; no hay forma de que sea al revés

Todo lo que descartes en el paso 2 no llega a formar grupos. Por eso filtrar en el WHERE es más barato que filtrar después: el GROUP BY trabaja con menos filas.

4.2 Después del paso 3, la fila individual ya no existe

Es la regla de la sección 1, ahora con su porqué: el SELECT se ejecuta en el paso 5, cuando lo único que queda son grupos. No puede pedir una columna suelta porque, sencillamente, ya no hay ninguna que pedir — hay 21 montones, no 830 filas.

4.3 GROUP BY sí acepta un nombre puesto con AS, aunque el orden diga que no debería

SELECT ship_country AS pais, count(*) AS pedidos
FROM orders
GROUP BY pais;          -- ✅ funciona

Si el orden lógico manda, el paso 3 no debería ver un nombre que nace en el paso 5. Y en teoría no lo ve: es una comodidad que el estándar SQL concede solo a GROUP BY y a ORDER BY, no a WHERE ni a HAVING. La regla práctica, que conviene memorizar tal cual:

En GROUP BY y ORDER BY puedes usar el nombre del alias. En WHERE y HAVING, nunca.

5. Errores comunes

  1. Poner en el SELECT una columna que no está agrupada ni agregada. Da error, y el error tiene razón.
  2. Agrupar por demasiadas columnas. Si añades la clave primaria al GROUP BY, cada grupo es una fila y todos los count(*) valen 1. Suele pasar al añadir columnas "para verlas".
  3. Esperar que aparezcan los grupos vacíos. No aparecen.
  4. Olvidar que los NULL hacen grupo. Aparece una fila con la celda en blanco que parece un fallo.
  5. Filtrar en HAVING lo que debería ir en WHERE. Funciona pero es más lento y más confuso (having-vs-where).
  6. Agrupar por una columna descriptiva en vez de por su id. Si dos categorías se llaman igual, se fusionan en un grupo. Agrupa por la clave, muestra el nombre.
  7. Creer que GROUP BY ordena. No lo garantiza. Si quieres orden, ORDER BY.

6. Diferencias con SQL Server

GROUP BY es igual en ambos: mismas reglas, mismo error cuando falta una columna. Las diferencias son de alrededor:

PostgreSQL SQL Server
Agrupar por alias ✅ GROUP BY anio ❌ Hay que repetir la expresión
Agrupar por posición ✅ GROUP BY 1 ❌ No se permite
Columnas dependientes de la PK ✅ Si agrupas por la PK, puedes seleccionar el resto sin agregarlos ❌ Hay que listarlas todas
GROUPING SETS / ROLLUP / CUBE ✅ ✅

La tercera es la más cómoda de PostgreSQL y conviene conocerla:

SELECT c.customer_id, c.company_name, c.country, count(*)
FROM customers c JOIN orders o ON o.customer_id = c.customer_id
GROUP BY c.customer_id;      -- funciona: customer_id es la PK de customers

PostgreSQL sabe que si agrupaste por la clave primaria, company_name y country están funcionalmente determinados y no pueden ser ambiguos. SQL Server exige listar las tres. Es más cómodo, y menos portable: si el material tiene que valer para los dos motores, lista las columnas.

7. Ejercicios

  1. Cuántos productos hay por categoría, de la categoría con más a la que menos.
  2. Los 5 clientes con más pedidos.
  3. Unidades vendidas por producto, y los 3 más vendidos. ¿Cuántos productos distintos se han vendido alguna vez?
  4. Pedidos por empleado y por año, solo de 1997.
  5. Explica por qué esta consulta devuelve 830 filas y no 21: sql SELECT ship_country, order_id, count(*) FROM orders GROUP BY ship_country, order_id;

8. Pregunta de negocio

Ventas quiere saber "qué comercial vende más". Te dan libertad para elegir la métrica. Entrega la respuesta y explica por qué elegiste esa métrica y no otra.

Con lo que sabes hasta aquí puedes hacerlo sin JOIN, aunque salgan ids en vez de nombres.

? RespuestaRespóndela antes de abrir

La métrica honesta es facturación neta, no número de pedidos:

SELECT o.employee_id,
       count(DISTINCT o.order_id)                                   AS pedidos,
       round(sum(od.unit_price * od.quantity * (1 - od.discount)),2) AS facturacion,
       round(sum(od.unit_price * od.quantity * (1 - od.discount))
             / count(DISTINCT o.order_id), 2)                        AS ticket_medio
FROM orders o
JOIN order_details od ON od.order_id = o.order_id
GROUP BY o.employee_id
ORDER BY facturacion DESC;
 employee_id | pedidos | facturacion | ticket_medio
-------------+---------+-------------+--------------
           4 |     156 |   232890.85 |      1492.89
           3 |     127 |   202812.84 |      1596.95
           1 |     123 |   192107.60 |      1561.85
           2 |      96 |   166537.76 |      1734.77
           8 |     104 |   126862.28 |      1219.83
           7 |      72 |   124568.24 |      1730.11
           9 |      43 |    77308.07 |      1797.86
           6 |      67 |    73913.13 |      1103.18
           5 |      42 |    68792.28 |      1637.91

Por qué esa métrica y no otra, que era la mitad del encargo:

  1. count(*) de pedidos premia el volumen, no el valor. El empleado 4 gana en las dos, pero mirar solo el volumen esconde el resto de la tabla: el 9 hace 43 pedidos —los menos de todos— y tiene el mejor ticket medio, 1.797,86. Y el 8 hace 104 pedidos con el peor ticket después del 6. Ordenar por número de pedidos pondría al 8 por delante del 7, que factura casi lo mismo con 32 pedidos menos. Son perfiles de comercial distintos y las dos lecturas son legítimas — pero hay que elegir una y decirlo.
  2. Neto, no bruto. Un comercial que cierra ventas regalando descuento factura menos de lo que parece. Usar sum(unit_price * quantity) sin el (1 - discount) premia exactamente el comportamiento que la empresa no quiere.
  3. count(DISTINCT o.order_id), no count(*). Al unir con order_details cada pedido se multiplica por sus líneas (distinct-y-duplicados). Sin el DISTINCT, "pedidos" contaría líneas y el ticket medio saldría dividido entre 2,6.

Y lo que hay que decir al entregar: esto mide facturación atribuida al empleado que registró el pedido, que no es necesariamente quien lo vendió. Antes de usarlo para calcular comisiones, hay que confirmar que orders.employee_id significa lo que Ventas cree que significa.

✓ SolucionesResuélvelos antes de abrir

1.

SELECT category_id, count(*) AS productos
FROM products
GROUP BY category_id
ORDER BY productos DESC;
 category_id | productos
-------------+-----------
           3 |        13
           1 |        12
           8 |        12
           2 |        12
           4 |        10
           5 |         7
           6 |         6
           7 |         5

77 productos repartidos entre 8 categorías, bastante equilibrados. Dos cosas:

  • Comprueba siempre que los grupos suman el total: 13+12+12+12+10+7+6+5 = 77. Si no suman, o hay nulos formando grupo o el WHERE se comió filas.
  • Las categorías 1, 8 y 2 empatan a 12 y salen en un orden arbitrario. Es el empate de ordenar-y-limitar: si esta lista se compara entre ejecuciones, hace falta desempatar con ORDER BY productos DESC, category_id.

2.

SELECT customer_id, count(*) AS pedidos
FROM orders
GROUP BY customer_id
ORDER BY pedidos DESC
LIMIT 5;
 customer_id | pedidos
-------------+---------
 SAVEA       |      31
 ERNSH       |      30
 QUICK       |      28

89 grupos, no 91. GROUP BY solo ve los clientes que aparecen en orders; los dos que no han comprado nunca no existen para esta consulta. Es el mismo hueco de los grupos vacíos, y se resuelve con LEFT JOIN (left-right-y-full-join).

3.

SELECT product_id, count(*) AS lineas, sum(quantity) AS unidades
FROM order_details
GROUP BY product_id
ORDER BY unidades DESC
LIMIT 3;
 product_id | lineas | unidades
------------+--------+----------
         60 |     51 |     1577
         59 |     54 |     1496
         31 |     51 |     1397
SELECT count(DISTINCT product_id) FROM order_details;   -- 77

Los 77 productos se han vendido alguna vez, ninguno se ha quedado sin salir. Y fíjate en el detalle: el producto 59 aparece en más líneas (54) que el 60 (51), pero vende menos unidades. "Más vendido" significa cosas distintas según cuentes pedidos o unidades — y una tercera si cuentas dinero.

4.

SELECT employee_id, count(*) AS pedidos
FROM orders
WHERE order_date >= '1997-01-01' AND order_date < '1998-01-01'
GROUP BY employee_id
ORDER BY pedidos DESC;
 employee_id | pedidos
-------------+---------
           4 |      81
           3 |      71
           1 |      55

Como el año ya está fijado por el WHERE, no hace falta agruparlo. Agrupar por una columna que el WHERE dejó en un solo valor añade una columna constante y ningún grupo nuevo.

5. Porque order_id es la clave primaria de orders: cada valor es único, así que cada combinación (ship_country, order_id) aparece exactamente una vez. 830 grupos de una fila cada uno, y todos los count(*) valen 1.

Es el error del punto 2 de la sección 4 en estado puro: agrupar por la clave primaria destruye la agrupación sin dar ningún error. La consulta devuelve lo mismo que un SELECT sin GROUP BY, con un 1 inútil al lado.