Saltar al contenido
IntermedioArturo Lorenzo·22 de agosto de 2026·5 min

GROUP BY, HAVING y funciones de ventana: la guía que te falta

Tres niveles de SQL analítico. GROUP BY colapsa filas; window functions no. Esa es toda la diferencia.

Sabes hacer un SELECT, un WHERE, un JOIN. Perfecto, eso es el 40% del SQL que usa un analista. El otro 60% es saber agrupar, filtrar grupos y — sobre todo — comparar filas sin perder el detalle. Ahí es donde entra el SQL analítico, y donde la mayoría de tutoriales se ponen innecesariamente abstractos.

Vamos a ir de menos a más, con una sola tabla de ventas como ejemplo en todo el artículo. Nada de cambiar de dataset cada sección. Así ves la progresión real.

### La tabla: ventas de una tienda

1-- Nuestra tabla para todo el artículo
2CREATE TABLE ventas (
3 id SERIAL PRIMARY KEY,
4 fecha DATE NOT NULL,
5 producto VARCHAR(50),
6 tienda VARCHAR(50),
7 cantidad INT,
8 importe DECIMAL(10, 2)
9);
10
11-- Datos de ejemplo (imagina 10.000 filas)
12-- fecha | producto | tienda | cantidad | importe
13-- 2024-01-15 | Laptop | Madrid | 2 | 1799.98
14-- 2024-01-15 | Monitor | Madrid | 3 | 1048.50
15-- 2024-01-15 | Laptop | Barcelona | 1 | 899.99
16-- 2024-01-16 | Teclado | Madrid | 5 | 149.95
17-- ...

Una tabla, muchas preguntas. Toda la magia de SQL analítico es saber qué pregunta contestar con qué herramienta.

### Nivel 1: GROUP BY y HAVING

GROUP BY responde a "¿cuánto en total?" agrupado por algo. HAVING es el WHERE de los grupos — filtra después de agrupar. La diferencia clave: WHERE filtra filas ANTES de agrupar; HAVING filtra grupos DESPUÉS.

1-- Total vendido por tienda
2SELECT
3 tienda,
4 SUM(importe) AS total_vendido,
5 COUNT(*) AS num_transacciones
6FROM ventas
7GROUP BY tienda;
8
9-- Solo tiendas con más de 100k€ en ventas
10SELECT
11 tienda,
12 SUM(importe) AS total_vendido
13FROM ventas
14GROUP BY tienda
15HAVING SUM(importe) > 100000;
16
17-- Ventas por producto y mes (agregación multinivel)
18SELECT
19 DATE_TRUNC('month', fecha) AS mes,
20 producto,
21 SUM(importe) AS total,
22 AVG(importe) AS ticket_medio
23FROM ventas
24GROUP BY DATE_TRUNC('month', fecha), producto
25ORDER BY mes, total DESC;

WHERE filtra filas. HAVING filtra grupos. No los mezcles — son dos momentos distintos de la query.

Truco para no equivocarte: si el filtro usa una función de agregación (SUM, COUNT, AVG), va en HAVING. Si no la usa, va en WHERE. Siempre.

### El problema de GROUP BY: colapsa filas

GROUP BY te da un resumen, pero pierdes el detalle. Si quieres saber "el total por tienda Y además ver cada venta individual con su porcentaje del total", GROUP BY no puede. Necesitas ver el bosque y los árboles al mismo tiempo. Aquí entran las funciones de ventana.

GROUP BY = resumen. OVER() = detalle + cálculo. Las dos son útiles; sirven para cosas distintas.

### Nivel 2: ROW_NUMBER, RANK y DENSE_RANK

La primera función de ventana que todo el mundo necesita: numerar filas dentro de un grupo. "Dame el producto más vendido de cada tienda", "la última venta de cada cliente", "el top 3 por mes". Todo eso es ROW_NUMBER.

1-- Top 1 producto por tienda (el más vendido)
2WITH ranking AS (
3 SELECT
4 tienda,
5 producto,
6 SUM(importe) AS total,
7 ROW_NUMBER() OVER (
8 PARTITION BY tienda
9 ORDER BY SUM(importe) DESC
10 ) AS rn
11 FROM ventas
12 GROUP BY tienda, producto
13)
14SELECT tienda, producto, total
15FROM ranking
16WHERE rn = 1;
17
18-- Diferencia entre ROW_NUMBER, RANK y DENSE_RANK:
19-- Si dos productos empatados en ventas:
20-- ROW_NUMBER: 1, 2, 3 (siempre consecutivo, desempata arbitrariamente)
21-- RANK: 1, 1, 3 (empate real, se salta el 2)
22-- DENSE_RANK: 1, 1, 2 (empate real, NO se salta)

PARTITION BY es el GROUP BY de las window functions: define los grupos. ORDER BY define el criterio dentro de cada grupo.

### Nivel 3: LAG, LEAD y acumulados con SUM OVER

Aquí es donde el SQL analítico brilla de verdad. LAG te da la fila anterior, LEAD la siguiente. SUM OVER con un frame te da acumulados. Con estas tres puedes responder "¿cuánto crecimos respecto al mes pasado?" y "¿cuál es el acumulado del año?" sin salir de SQL.

1-- Ventas mensuales con variación respecto al mes anterior
2WITH mensual AS (
3 SELECT
4 DATE_TRUNC('month', fecha) AS mes,
5 SUM(importe) AS total
6 FROM ventas
7 GROUP BY DATE_TRUNC('month', fecha)
8)
9SELECT
10 mes,
11 total,
12 LAG(total) OVER (ORDER BY mes) AS mes_anterior,
13 total - LAG(total) OVER (ORDER BY mes) AS diferencia,
14 ROUND(
15 (total - LAG(total) OVER (ORDER BY mes))
16 / LAG(total) OVER (ORDER BY mes) * 100, 1
17 ) AS pct_cambio
18FROM mensual
19ORDER BY mes;

LAG(columna) OVER (ORDER BY ...) = "dame el valor de la fila anterior". Así de simple.

1-- Acumulado de ventas por tienda a lo largo del año
2SELECT
3 fecha,
4 tienda,
5 importe,
6 SUM(importe) OVER (
7 PARTITION BY tienda
8 ORDER BY fecha
9 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
10 ) AS acumulado
11FROM ventas
12ORDER BY tienda, fecha;
13
14-- Media móvil de 7 días (suavizar picos)
15SELECT
16 fecha,
17 importe,
18 AVG(importe) OVER (
19 ORDER BY fecha
20 ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
21 ) AS media_7d
22FROM ventas;

El frame (ROWS BETWEEN) define la "ventana" de filas que usa el cálculo. Sin él, usa todo el partition.

Cuidado con ROWS vs RANGE. ROWS cuenta filas literales; RANGE agrupa valores iguales. Si tienes varias filas con la misma fecha y usas RANGE, las trata como una sola posición. Para acumulados financieros, normalmente quieres ROWS.

### Cuándo usar cada herramienta

  • Quieres un resumen (total, media, conteo) → GROUP BY
  • Quieres filtrar esos resúmenes → GROUP BY + HAVING
  • Quieres el top N por categoría → ROW_NUMBER() + PARTITION BY
  • Quieres comparar con el periodo anterior → LAG()
  • Quieres un acumulado progresivo → SUM() OVER(ORDER BY ... ROWS ...)
  • Quieres un porcentaje del total del grupo → valor / SUM() OVER(PARTITION BY ...)

La clave para dominar SQL analítico es practicar con datos reales y preguntas de negocio reales. No te estudies la sintaxis de memoria — plantéate preguntas ("¿cuál fue el mejor mes de cada tienda?") e intenta responderlas. Si quieres practicar esto con ejercicios progresivos y un editor SQL integrado, lo trabajamos en profundidad en la plataforma.

Consejo de entrevista: si te preguntan por window functions, explica primero QUÉ PROBLEMA resuelven (ver detalle + cálculo al mismo tiempo) antes de escribir la sintaxis. Demuestra que entiendes el porqué, no solo el cómo.

$ whoami

Regístrate gratis para dar like, guardar posts en carpetas y recibir nuevos artículos por email.

Leer está bien. Ejecutar está mejor.

## comentarios

¿Te ha servido? ¿Añadirías algo? ¿Lo has vivido de otra forma en tu trabajo? Cuéntalo.

Inicia sesión para comentar y responder.

cargando comentarios...