Saltar al contenido

lección 11

CASE y pivotar en SQL: la tabla dinámica sin hoja de cálculo

Clasificar filas con CASE y girar una tabla larga a ancha sin salir de SQL.

45 min

Tu query devuelve 12 filas, una por categoría de producto con las ventas mensuales apiladas en formato largo. Pero el informe que tienes que entregar necesita las categorías como filas y los meses como columnas: una tabla de 5×12 que quepa en una diapositiva. En la hoja de cálculo lo resolverías con una tabla dinámica en tres clics, pero tus datos tienen 800.000 filas y el Excel se congela. Necesitas hacer ese giro dentro de SQL, donde el motor lo resuelve en milisegundos. Las dos piezas que lo hacen posible son CASE — el «si esto, entonces aquello» de SQL, que clasifica cada fila según una condición — y el truco de meter ese CASE dentro de una agregación para girar la tabla de larga a ancha sin salir de la consulta.

La tabla dinámica (en inglés, pivot table) es probablemente la función más usada de las hojas de cálculo en toda la historia del negocio. La inventó Pito Salas en Lotus para un programa llamado Improv que salió en 1991. Tres años después Excel la incorporó con el nombre de «tabla dinámica», y desde entonces cualquier persona que trabaje con datos ha arrastrado un campo a «filas», otro a «columnas» y un tercero a «valores» para ver un resumen cruzado. Lo que casi nadie sabe es que ese mismo giro se puede hacer en SQL, y que a menudo conviene hacerlo ahí: cuando los datos no caben en la hoja, cuando el informe hay que rehacerlo cada semana, o cuando quieres que el número salga siempre del mismo sitio y no de la versión que cada uno tenga guardada en su ordenador (computadora).

### CASE WHEN: el "si esto, entonces aquello" de SQL

Antes de girar nada, necesitas una pieza previa: CASE. Es la forma que tiene SQL de tomar decisiones fila a fila. Funciona exactamente como el «si… entonces… si no…» que usarías al hablar: si el importe es menor de 20, entonces la etiqueta es "bajo"; si no, si es menor de 100, entonces "medio"; en cualquier otro caso, "alto". Cada fila se evalúa por separado y recibe su etiqueta.

La analogía más cercana es la del cajero de un supermercado que separa la compra en bolsas según lo que va saliendo por la cinta: lo congelado a una bolsa, lo frágil a otra, lo pesado al fondo. No mira la cesta entera de golpe; decide producto a producto según una regla. CASE hace eso mismo con las filas de una tabla: recorre una a una y le pega a cada una la etiqueta que le corresponde.

1-- Clasificar cada pedido segun su importe
2SELECT
3 pedido_id,
4 importe,
5 CASE
6 WHEN importe < 20 THEN 'bajo'
7 WHEN importe < 100 THEN 'medio'
8 ELSE 'alto'
9 END AS tramo
10FROM pedidos
11ORDER BY pedido_id;

CASE en el SELECT: crea una columna nueva con la categoría de cada fila

El orden de los WHEN importa, y es la trampa número uno con CASE. Las ramas se evalúan de arriba abajo y gana la PRIMERA que se cumple. Si pones "WHEN importe < 100 THEN 'medio'" antes que "WHEN importe < 20 THEN 'bajo'", ningún pedido saldrá jamás como "bajo": un importe de 8 cumple "< 100" antes de llegar a "< 20", así que se queda en "medio". Ordena siempre de la condición más estrecha a la más amplia.

### Los dos sabores de CASE

CASE tiene dos formas. La que acabas de ver es el CASE "buscado" (searched), con una condición completa en cada WHEN, y es la más flexible: cada rama puede comparar lo que quiera. La otra es el CASE "simple", que compara una sola columna contra valores concretos. Sirve para traducir códigos a etiquetas legibles, algo que harás constantemente cuando el sistema guarde "ES" y el informe tenga que decir "España".

1-- CASE simple: traducir un codigo a etiqueta legible
2SELECT
3 cliente_id,
4 CASE pais
5 WHEN 'ES' THEN 'Espana'
6 WHEN 'PT' THEN 'Portugal'
7 WHEN 'FR' THEN 'Francia'
8 ELSE 'Otro'
9 END AS pais_legible
10FROM clientes;
11
12-- El mismo resultado en forma "buscada" (mas verbosa, mas flexible)
13SELECT
14 cliente_id,
15 CASE
16 WHEN pais = 'ES' THEN 'Espana'
17 WHEN pais = 'PT' THEN 'Portugal'
18 WHEN pais = 'FR' THEN 'Francia'
19 ELSE 'Otro'
20 END AS pais_legible
21FROM clientes;

CASE simple (compara una columna) frente a CASE buscado (condición por rama)

Consejo de senior: cuando clasifiques en tramos numéricos (importe, edad, antigüedad), documenta en un comentario dónde caen los límites exactos. "< 20" deja el 20 fuera del tramo bajo; "<= 20" lo mete. Ese detalle de si el límite es "menor que" o "menor o igual" es el origen de la mitad de las discusiones de "a mí no me cuadra tu número" en las reuniones. Escríbelo al lado del CASE y te ahorras la conversación.

### CASE dentro de una agregación: contar subconjuntos

CASE en el SELECT crea una columna nueva. Pero el CASE despliega toda su potencia cuando lo metes DENTRO de una función de agregación como COUNT o SUM. La idea es sencilla y poderosa: CASE decide qué filas cuentan y cuáles se ignoran dentro de cada grupo. Así puedes tener, en una sola consulta, el total de pedidos de cada región y, al lado, cuántos de esos pedidos eran "grandes".

El truco está en que COUNT ignora los NULL. Cuando escribes COUNT(CASE WHEN importe >= 100 THEN 1 END), el CASE devuelve 1 para los pedidos grandes y NULL para el resto (porque no hay ELSE). COUNT cuenta los unos y pasa de los NULL, así que acabas contando solo el subconjunto que te interesa, sin dejar de tener el total con un COUNT(*) normal en la columna de al lado.

1-- Total de pedidos y cuantos son "grandes", por region
2SELECT
3 region,
4 COUNT(*) AS total_pedidos,
5 COUNT(CASE WHEN importe >= 100 THEN 1 END) AS pedidos_grandes,
6 ROUND(100.0 * COUNT(CASE WHEN importe >= 100 THEN 1 END) / COUNT(*), 1) AS pct_grandes
7FROM pedidos
8GROUP BY region
9ORDER BY region;

COUNT(CASE WHEN...) cuenta solo el subconjunto que cumple la condición, dentro de cada grupo

Con SUM el patrón es idéntico pero para sumar importes en lugar de contar filas. SUM(CASE WHEN estado = 'completado' THEN importe END) te da la facturación real de cada grupo, dejando fuera lo cancelado, sin tocar el total. Y aquí, casi sin darte cuenta, ya tienes la llave del pivotado.

### Formato largo y formato ancho: el mismo dato, dos formas

Antes de pivotar hay que entender qué estamos girando. Los mismos datos se pueden guardar de dos maneras. En formato largo hay una fila por cada combinación: región Norte en enero, región Norte en febrero, región Sur en enero… Es como te llegan los datos casi siempre, porque es como los guarda una base de datos: compacto, fácil de ampliar, una fila por hecho. En formato ancho hay una fila por región y una columna por mes, con el importe en el cruce. Es como quiere verlo un humano en un informe, porque puede comparar los meses de un vistazo, de izquierda a derecha.

Pivotar es girar la tabla: los valores de la columna "mes" se convierten en cabeceras de columna

### El truco del pivotado: SUM(CASE WHEN...)

Ahora junta las dos ideas. Ya sabes agrupar por región con GROUP BY, y ya sabes que SUM(CASE WHEN condición THEN importe END) suma solo las filas que cumplen la condición. Pues pivotar no es más que escribir una columna de esas por cada mes: una que sume solo enero, otra que sume solo febrero, otra solo marzo. Al agrupar por región, cada región se queda en una fila y cada SUM rellena su celda del mes correspondiente.

1-- Pivotar: de formato largo (una fila por mes)
2-- a formato ancho (una columna por mes)
3SELECT
4 region,
5 SUM(CASE WHEN mes = 'ene' THEN importe END) AS ene,
6 SUM(CASE WHEN mes = 'feb' THEN importe END) AS feb,
7 SUM(CASE WHEN mes = 'mar' THEN importe END) AS mar
8FROM ventas
9GROUP BY region
10ORDER BY region;

Una columna SUM(CASE...) por cada mes: eso es una tabla dinámica escrita en SQL

Dentro de cada grupo, el CASE deja pasar solo el mes de su columna; SUM hace el resto

### Pivotar para contar, no solo para sumar

El pivotado no es solo para importes. Cambiando SUM por COUNT giras una tabla de conteos: cuántos pedidos hay en cada estado, por ciudad. Esto es la clásica tabla cruzada (en inglés, cross-tab) que responde de un vistazo a preguntas como "¿en qué ciudad se cancelan más pedidos?". Es el mismo patrón, con COUNT(CASE WHEN estado = 'x' THEN 1 END) por cada estado que quieras como columna.

1-- Tabla cruzada: pedidos por estado, con las ciudades en filas
2SELECT
3 ciudad,
4 COUNT(CASE WHEN estado = 'completado' THEN 1 END) AS completados,
5 COUNT(CASE WHEN estado = 'pendiente' THEN 1 END) AS pendientes,
6 COUNT(CASE WHEN estado = 'cancelado' THEN 1 END) AS cancelados
7FROM pedidos
8GROUP BY ciudad
9ORDER BY ciudad;

El mismo pivotado, con COUNT en lugar de SUM: una tabla cruzada de estados por ciudad

### Cuándo NO pivotar en SQL

El pivotado con CASE tiene un límite honesto que conviene conocer antes de que te muerda: las columnas son fijas. Tú escribes a mano una columna por cada valor: ene, feb, mar. Si el mes que viene aparece "abr", la consulta no lo muestra hasta que tú añadas la columna. Con doce meses es llevadero. Con las 200 categorías de producto de un catálogo, o con valores que cambian cada semana, escribir y mantener 200 líneas de CASE es inviable, y además cada categoría nueva rompe el informe en silencio.

  • Pocas columnas y estables (meses, trimestres, estados, un puñado de categorías fijas): pivota en SQL sin miedo, es limpio y el número sale siempre del mismo sitio.
  • Muchas columnas o categorías que cambian solas (todos los productos, todos los países, valores que aparecen y desaparecen): deja el pivotado para la herramienta de BI, que lo hace dinámico, o para pandas en Python con pivot_table, que genera las columnas por sí solo.
  • Regla práctica: si tendrías que volver a editar la consulta cada vez que aparece un valor nuevo, es señal de que ese pivotado no debería vivir en SQL.

Cuando pivotas, las celdas sin datos salen como NULL, no como 0. Si la región Sur no vendió nada en marzo, su celda "mar" dirá NULL, y NULL más cualquier cosa es NULL: un total por fila puede salir vacío por una sola celda hueca. Si el informe necesita ceros, envuelve cada SUM en COALESCE(SUM(CASE ...), 0) para convertir los huecos en 0. Es la diferencia entre "no vendió" y "no lo sé", y en un informe de dirección esa diferencia se nota.

### La hoja de cálculo sigue siendo válida

Nada de esto convierte la tabla dinámica de la hoja de cálculo en algo de segunda. Para una respuesta puntual, con datos que caben en la hoja y que no vas a repetir, arrastrar tres campos y tener el pivote en diez segundos es la decisión correcta, y quien lo hace decide igual de bien. El pivotado en SQL gana cuando el informe se repite cada semana, cuando los datos ya no caben en la hoja, o cuando quieres que el número sea el mismo para todo el equipo y no dependa de qué fichero (archivo) abrió cada uno. Sabes las dos formas; elige la que encaje con el problema que tienes delante, no la que suene más técnica.

Consejo de senior: en la entrevista te pueden pedir "pivota esta tabla de ventas por mes" y esperar precisamente el patrón SUM(CASE WHEN...). Muchos motores tienen además una función PIVOT dedicada (DuckDB y SQL Server la tienen, PostgreSQL no de serie), pero el SUM(CASE WHEN...) funciona en todos y demuestra que entiendes lo que pasa por debajo. Enseña esa versión primero; si además mencionas que existe PIVOT nativo, mejor. Lo que no impresiona es memorizar una sintaxis sin saber qué hace.

### Resumen

  • CASE es el "si… entonces… si no…" de SQL: evalúa cada fila y le asigna un valor según la primera rama WHEN que se cumpla. El orden de las ramas manda.
  • En el SELECT, CASE crea una columna categórica (tramos de importe, edad, traducir códigos a etiquetas).
  • Dentro de COUNT o SUM, CASE decide qué filas cuentan o suman dentro de cada grupo, porque las funciones de agregación ignoran los NULL.
  • Pivotar es girar una tabla de formato largo (una fila por combinación) a formato ancho (una columna por categoría). Es la tabla dinámica de Excel, en SQL.
  • El patrón es SUM(CASE WHEN categoria = 'x' THEN valor END) — o COUNT para contar — una columna por cada valor, agrupando por lo que quieras en filas.
  • Pivota en SQL cuando las columnas son pocas y estables. Con muchas categorías dinámicas, hazlo en la herramienta de BI o en pandas.
  • Las celdas vacías salen NULL, no 0: usa COALESCE si el informe necesita ceros.

Regístrate para guardar tu progreso.

## comentarios

Reporta erratas, ayuda a otros o comparte tu opinión. Sé constructivo.

Inicia sesión para comentar y responder.

cargando comentarios...