1. Qué es
CASE es el if / else de SQL. Evalúa condiciones en orden y devuelve el valor de la primera que se cumple.
Y es más importante de lo que parece, porque SQL no tiene bucles ni variables: CASE es prácticamente la única lógica de decisión que existe dentro de una consulta. Con él se hacen tres cosas que se usan a diario:
- Clasificar — convertir un número en una categoría (
económico,medio,premium). - Ordenar a medida — un orden que no es alfabético ni numérico.
- Pivotar — convertir filas en columnas.
2. Sintaxis
Hay dos formas.
Buscada — la que se usa el 90% de las veces. Cada WHEN lleva su propia condición completa:
CASE WHEN condición1 THEN valor1
WHEN condición2 THEN valor2
ELSE valor_por_defecto
END
Simple — compara una expresión contra valores concretos. Más corta, pero solo sirve para igualdad:
CASE expresión
WHEN valor1 THEN resultado1
WHEN valor2 THEN resultado2
ELSE otro
END
Tres reglas:
- Se evalúa de arriba abajo y gana el primer
WHENque se cumple. El orden no es decorativo. ELSEes opcional. Si falta y nada coincide, devuelveNULL.- Todos los
THENdeben devolver tipos compatibles. UnTHEN 'texto'junto a unTHEN 0da error de tipos.
3. Ejemplos sobre Northwind
3.1 Clasificar
SELECT product_name, unit_price,
CASE WHEN unit_price < 20 THEN 'económico'
WHEN unit_price < 50 THEN 'medio'
ELSE 'premium'
END AS gama
FROM products
ORDER BY unit_price DESC
LIMIT 5;
product_name | unit_price | gama
-------------------------+------------+---------
Côte de Blaye | 263.50 | premium
Thüringer Rostbratwurst | 123.79 | premium
Mishi Kobe Niku | 97.00 | premium
Fíjate en que el segundo WHEN dice unit_price < 50 a secas, no BETWEEN 20 AND 50. No hace falta: si la fila llegó al segundo WHEN es porque el primero falló, así que ya sabemos que vale 20 o más. Escribir el rango completo es redundante y una fuente de huecos.
3.2 El orden de los WHEN decide el resultado
La misma clasificación, con los dos primeros WHEN intercambiados:
SELECT product_name, unit_price,
CASE WHEN unit_price < 50 THEN 'medio'
WHEN unit_price < 20 THEN 'económico'
ELSE 'premium'
END AS gama_mal
FROM products
WHERE unit_price < 20
LIMIT 3;
product_name | unit_price | gama_mal
---------------+------------+----------
Chai | 18.00 | medio
Chang | 19.00 | medio
Aniseed Syrup | 10.00 | medio
Un producto de 10 euros clasificado como "medio". El segundo WHEN es inalcanzable: todo lo que vale menos de 20 vale también menos de 50, así que el primero se lo lleva siempre. No hay error, no hay aviso — solo una categoría que nunca aparece.
Regla: en un
CASEde rangos, escribe losWHENde más restrictivo a menos. Y comprueba que todas las categorías aparecen en el resultado; si una no sale nunca, probablemente es inalcanzable.
3.3 CASE en el GROUP BY
Como el CASE produce una columna, se puede agrupar por ella:
SELECT CASE WHEN unit_price < 20 THEN 'económico'
WHEN unit_price < 50 THEN 'medio'
ELSE 'premium'
END AS gama,
count(*) AS productos,
round(avg(unit_price),2) AS precio_medio
FROM products
GROUP BY 1
ORDER BY 3;
gama | productos | precio_medio
-----------+-----------+--------------
económico | 39 | 12.95
medio | 31 | 31.59
premium | 7 | 105.11
39 productos económicos, 31 medios y 7 premium. Ese GROUP BY 1 es el número de posición; también vale GROUP BY gama (el alias, que sí se acepta en GROUP BY — ver orden-logico-de-ejecucion). Lo que no vale es repetir el CASE entero: funciona, pero es doce líneas duplicadas que hay que mantener sincronizadas.
3.4 La forma simple
SELECT CASE extract(dow FROM order_date)
WHEN 0 THEN 'domingo' WHEN 1 THEN 'lunes'
WHEN 2 THEN 'martes' WHEN 3 THEN 'miércoles'
WHEN 4 THEN 'jueves' WHEN 5 THEN 'viernes'
ELSE 'sábado'
END AS dia,
count(*) AS pedidos
FROM orders
GROUP BY 1
ORDER BY 2 DESC;
dia | pedidos
-----------+---------
martes | 168
jueves | 168
lunes | 165
miércoles | 165
viernes | 164
Cinco filas, no siete. Ni sábado ni domingo aparecen:
SELECT count(*) FROM orders WHERE extract(dow FROM order_date) IN (0, 6); -- 0
Cero pedidos en fin de semana en toda la base. Es un hallazgo real sobre estos datos —Northwind registra pedidos solo en días laborables— y es el tipo de cosa que descubres solo cuando agrupas. Si alguien te pide una media diaria de pedidos, dividir entre 671 días naturales está mal: hay que dividir entre días hábiles.
3.5 CASE en el ORDER BY — orden a medida
Ordenar los cargos por jerarquía, que no es su orden alfabético:
SELECT title,
CASE WHEN title LIKE '%Vice%' THEN 0
WHEN title LIKE '%Manager%' THEN 1
ELSE 2
END AS orden
FROM employees
ORDER BY orden, title;
title | orden
--------------------------+-------
Vice President, Sales | 0
Sales Manager | 1
Inside Sales Coordinator | 2
Sales Representative | 2
El CASE puede ir directamente en el ORDER BY sin sacarlo al SELECT. Lo he sacado aquí solo para que se vea el número. En un informe real, ordenar por una columna invisible es incómodo de verificar; enseñarla suele compensar.
3.6 CASE en el WHERE
Es legal, y casi siempre innecesario:
-- Funciona, pero es una forma retorcida de escribir un OR
WHERE CASE WHEN discontinued THEN units_in_stock > 0 ELSE true END
El caso donde sí vale la pena es forzar el orden de evaluación para evitar un error:
WHERE CASE WHEN quantity = 0 THEN false ELSE total / quantity > 5 END
AND no garantiza que se evalúe de izquierda a derecha (where-y-operadores); CASE sí garantiza que no se evalúa un THEN cuyo WHEN falló. Es la forma de blindarse contra una división por cero. La otra es nullif(quantity, 0), más corta.
3.7 Pivotar: filas a columnas
Es el uso más vistoso de CASE. Pedidos por país y por año, en columnas:
SELECT ship_country AS pais,
sum(CASE WHEN extract(year FROM order_date) = 1996 THEN 1 ELSE 0 END) AS y1996,
sum(CASE WHEN extract(year FROM order_date) = 1997 THEN 1 ELSE 0 END) AS y1997,
sum(CASE WHEN extract(year FROM order_date) = 1998 THEN 1 ELSE 0 END) AS y1998,
count(*) AS total
FROM orders
GROUP BY 1
ORDER BY total DESC
LIMIT 6;
pais | y1996 | y1997 | y1998 | total
-----------+-------+-------+-------+-------
Germany | 24 | 64 | 34 | 122
USA | 23 | 60 | 39 | 122
Brazil | 13 | 42 | 28 | 83
France | 15 | 39 | 23 | 77
UK | 10 | 30 | 16 | 56
Venezuela | 8 | 20 | 18 | 46
El patrón es siempre el mismo: sum(CASE WHEN <columna a pivotar> = <valor> THEN <métrica> ELSE 0 END) — una columna del resultado por cada valor.
PostgreSQL tiene una forma más limpia de escribir exactamente esto:
SELECT ship_country AS pais,
count(*) FILTER (WHERE extract(year FROM order_date) = 1996) AS y1996,
count(*) FILTER (WHERE extract(year FROM order_date) = 1997) AS y1997,
count(*) FILTER (WHERE extract(year FROM order_date) = 1998) AS y1998,
count(*) AS total
FROM orders GROUP BY 1 ORDER BY total DESC;
Mismo resultado, y se lee. FILTER es del estándar SQL pero no lo implementa SQL Server; se ve en agregacion-avanzada.
La limitación del pivote con CASE: hay que escribir una línea por valor, a mano. Si el año que viene aparece 1999, la consulta no lo muestra y no da error. Un pivote dinámico no se puede hacer en SQL puro — se resuelve en la herramienta de informes, o con crosstab (pivotar-datos).
3.8 CASE sin ELSE
SELECT product_name,
CASE WHEN discontinued THEN 'baja' END AS estado
FROM products LIMIT 3;
product_name | estado
---------------+--------
Chai | baja
Chang | baja
Aniseed Syrup |
Sin ELSE, lo que no encaja sale NULL. A veces es lo que quieres; muchas veces es un ELSE olvidado. Y como NULL se propaga, ese hueco puede reaparecer tres consultas más tarde convertido en una suma vacía (null-y-la-logica-de-tres-valores).
4. Errores comunes
WHENen mal orden. El caso de "medio" comiéndose a "económico". Ni error ni aviso.- Olvidar el
ELSE. AparecenNULLdonde esperabas una categoría. - Mezclar tipos en los
THEN.THEN 'sin datos'junto aTHEN 0da error de tipo. - Comparar con
NULLdentro de unWHEN.WHEN region = NULLnunca se cumple. Se escribeWHEN region IS NULL. - Repetir el
CASEentero en elGROUP BY. Usa el número de posición o el alias. - Pivotar con
CASEy olvidar actualizar la lista de valores. El dato nuevo desaparece del informe en silencio. - Usar
CASEdonde bastacoalesce.CASE WHEN x IS NULL THEN y ELSE x ENDes exactamentecoalesce(x, y).
5. Diferencias con SQL Server
CASE es idéntico en los dos motores: mismas dos formas, mismas reglas, misma sintaxis. Es de lo poco que se copia y pega sin tocar nada.
Lo que cambia es lo que hay alrededor:
| Necesidad | SQL Server | PostgreSQL |
|---|---|---|
CASE |
Igual | Igual |
| Condicional corto | IIF(cond, a, b) |
❌ No existe → CASE WHEN cond THEN a ELSE b END |
| Elegir de una lista | CHOOSE(n, a, b, c) |
❌ No existe |
| Agregar con filtro | SUM(CASE WHEN ... THEN 1 ELSE 0 END) |
count(*) FILTER (WHERE ...) |
| Pivotar | PIVOT / UNPIVOT (operador propio) |
❌ No existe → CASE o crosstab |
IIF y PIVOT son extensiones de SQL Server. PIVOT tampoco es dinámico: también hay que enumerar los valores a mano, así que la limitación es la misma con mejor sintaxis.
6. Ejercicios
- Clasifica los clientes por continente aproximado según su país:
América,Europa,Otros. Cuenta cuántos hay de cada uno. - Clasifica los productos por estado de stock:
sin stock,bajo(por debajo del punto de reposición) yok. ¿Cuántos hay de cada uno? - Ordena los productos poniendo primero los descatalogados y, dentro de cada grupo, del más caro al más barato.
- Pivota los pedidos por año (filas) y trimestre (columnas).
- Reescribe esto sin
CASE:sql SELECT company_name, CASE WHEN region IS NULL THEN 'n/a' ELSE region END FROM customers;
7. Pregunta de negocio
Dirección quiere un semáforo de inventario para la reunión del lunes: por categoría de producto, cuántos están en rojo, ámbar y verde. Rojo es sin stock, ámbar es por debajo del punto de reposición, verde el resto. Y quieren verlo de un vistazo, no una lista de 77 filas.
Todavía no sabes hacer JOIN, así que la categoría tendrá que ser su id. Eso también es información que hay que dar.