CASE en SQL
En esta lección
Muchas veces no quieres mostrar un dato tal cual, sino según su valor: “Menor” si el cliente tiene menos de 18 años, “Agotado” si el stock es 0, “Envío gratis” si el pedido pasa de 100 €. En un lenguaje de programación usarías un if. En SQL tienes CASE, una expresión que evalúa condiciones y devuelve un valor distinto para cada caso.
CASE es SQL estándar, funciona igual en MySQL, PostgreSQL y SQLite, y se puede usar en casi cualquier sitio: en el SELECT, en el ORDER BY, dentro de SUM o COUNT y en un UPDATE. En esta lección verás todos esos usos con las tablas del curso.
CASE buscado: condiciones una tras otra
La forma más completa se llama CASE buscado. Tiene esta estructura:
CASE
WHEN condicion1 THEN valor1
WHEN condicion2 THEN valor2
ELSE valor_por_defecto
END
La base de datos comprueba las condiciones de arriba abajo y devuelve el valor de la primera que se cumple. Si no se cumple ninguna, devuelve el del ELSE. Todo el bloque, de CASE a END, es un único valor, así que se le pone un alias con AS como a cualquier columna calculada.
Clasificar clientes por edad
SELECT nombre, edad,
CASE
WHEN edad < 18 THEN 'Menor'
WHEN edad < 30 THEN 'Joven'
WHEN edad < 45 THEN 'Adulto'
ELSE 'Senior'
END AS tramo
FROM clientes;
| nombre | edad | tramo |
|---|---|---|
| Ana García | 34 | Adulto |
| Luis Pérez | 22 | Joven |
| Marta Ruiz | 45 | Senior |
| Pablo Díaz | 17 | Menor |
| Eva Martín | 29 | Joven |
Fíjate en que la segunda condición es solo edad < 30, no edad >= 18 AND edad < 30. No hace falta: si un cliente llega a la segunda línea es porque no cumplía la primera, así que ya sabemos que tiene 18 o más. Pablo (17) se queda en la primera y las demás ni se miran. Marta tiene justo 45: no cumple edad < 45 y cae en el ELSE.
El orden de los WHEN importa
Como gana la primera condición verdadera, el orden cambia el resultado. Mira qué pasa si las escribes al revés:
SELECT nombre, edad,
CASE
WHEN edad < 45 THEN 'Adulto'
WHEN edad < 30 THEN 'Joven'
WHEN edad < 18 THEN 'Menor'
ELSE 'Senior'
END AS tramo
FROM clientes;
| nombre | edad | tramo |
|---|---|---|
| Ana García | 34 | Adulto |
| Luis Pérez | 22 | Adulto |
| Marta Ruiz | 45 | Senior |
| Pablo Díaz | 17 | Adulto |
| Eva Martín | 29 | Adulto |
Pablo, con 17 años, sale como “Adulto”: 17 < 45 es verdadero y la base de datos no sigue mirando. No da ningún error, simplemente devuelve datos incorrectos. Regla práctica: con rangos, ordena las condiciones de la más restrictiva a la más amplia.
Clasificar pedidos por importe
El mismo patrón sirve para cualquier columna numérica:
SELECT id, importe,
CASE
WHEN importe < 20 THEN 'Pequeño'
WHEN importe <= 60 THEN 'Mediano'
ELSE 'Grande'
END AS tamanio
FROM pedidos;
| id | importe | tamanio |
|---|---|---|
| 1 | 45.00 | Mediano |
| 2 | 120.50 | Grande |
| 3 | 12.99 | Pequeño |
| 4 | 60.00 | Mediano |
| 5 | 33.25 | Mediano |
El pedido 4 (60.00) es “Mediano” porque la condición es <= 60. Decide siempre qué pasa exactamente en los límites: es donde aparecen los fallos.
En las condiciones vale todo lo del WHERE: AND, OR, BETWEEN, IN, LIKE, IS NULL…
CASE simple: comparar con valores concretos
Cuando todas las condiciones son del tipo “esta columna es igual a tal valor”, hay una forma más corta, el CASE simple. La columna se escribe una sola vez, justo después de CASE:
SELECT nombre, ciudad,
CASE ciudad
WHEN 'Santander' THEN 'Cantabria'
WHEN 'Bilbao' THEN 'País Vasco'
WHEN 'Madrid' THEN 'Comunidad de Madrid'
END AS comunidad
FROM clientes;
| nombre | ciudad | comunidad |
|---|---|---|
| Ana García | Santander | Cantabria |
| Luis Pérez | Bilbao | País Vasco |
| Marta Ruiz | Santander | Cantabria |
| Pablo Díaz | Madrid | Comunidad de Madrid |
| Eva Martín | Madrid | Comunidad de Madrid |
Es equivalente a escribir WHEN ciudad = 'Santander' THEN ... en un CASE buscado. El simple es más legible, pero solo compara por igualdad: no admite <, BETWEEN ni LIKE, y tampoco sirve para detectar NULL, porque NULL = NULL no es verdadero (lo viste en valores NULL):
SELECT CASE NULL WHEN NULL THEN 'es null' ELSE 'no' END AS simple,
CASE WHEN NULL IS NULL THEN 'es null' ELSE 'no' END AS buscado;
| simple | buscado |
|---|---|
| no | es null |
CASE simple | CASE buscado | |
|---|---|---|
| Forma | CASE columna WHEN valor THEN ... | CASE WHEN condicion THEN ... |
| Comparaciones | Solo igualdad | Cualquier condición |
Detecta NULL | No | Sí, con IS NULL |
| Cuándo usarlo | Traducir códigos o valores fijos | Rangos y condiciones compuestas |
Consejo: si una tabla de traducción como la de las comunidades crece mucho o cambia a menudo, no la escribas en un
CASE: crea una tablaciudadesy usa un JOIN. ElCASEes para reglas pequeñas y estables.
ELSE y el NULL por defecto
El ELSE es opcional. Si lo omites y no se cumple ninguna condición, CASE devuelve NULL:
SELECT id, importe,
CASE WHEN importe >= 100 THEN 'Envío gratis' END AS envio
FROM pedidos;
| id | importe | envio |
|---|---|---|
| 1 | 45.00 | NULL |
| 2 | 120.50 | Envío gratis |
| 3 | 12.99 | NULL |
| 4 | 60.00 | NULL |
| 5 | 33.25 | NULL |
A veces es justo lo que quieres (lo usarás enseguida con COUNT). Pero si no lo es, escribe siempre un ELSE, aunque sea ELSE 'Otro'. Así, si mañana aparece un cliente de Sevilla en la consulta de comunidades, verás “Otro” en lugar de un NULL que no sabrás de dónde viene.
Otro detalle: todos los THEN y el ELSE deberían devolver el mismo tipo de dato. Mezclar un número y un texto (THEN 0 ELSE 'sin dato') da error en PostgreSQL y, en MySQL y SQLite, resultados difíciles de ordenar y comparar.
CASE en ORDER BY: un orden a medida
ORDER BY ordena de forma alfabética o numérica, pero a veces el orden lo marca el negocio. Por ejemplo, la tienda está en Santander y quiere ver primero a sus clientes locales y después al resto:
SELECT nombre, ciudad
FROM clientes
ORDER BY CASE WHEN ciudad = 'Santander' THEN 0 ELSE 1 END,
ciudad, nombre;
| nombre | ciudad |
|---|---|
| Ana García | Santander |
| Marta Ruiz | Santander |
| Luis Pérez | Bilbao |
| Eva Martín | Madrid |
| Pablo Díaz | Madrid |
El CASE convierte cada ciudad en un número (0 para Santander, 1 para las demás) y se ordena por ese número. Los empates se resuelven con las siguientes columnas, como aprendiste en ORDER BY. Es el truco habitual para ordenar estados como “urgente, normal, bajo” o los días de la semana, que alfabéticamente saldrían desordenados.
CASE dentro de funciones de agregación
Este es el uso más potente y aparece en muchos informes. Las funciones de agregación (COUNT, SUM…) resumen muchas filas en un valor. Si les pasas un CASE, puedes contar o sumar solo las filas que cumplen una condición, varias condiciones distintas en la misma consulta.
Contar con SUM(CASE WHEN … THEN 1 ELSE 0 END)
¿Cuántos clientes son mayores de edad y cuántos no?
SELECT
COUNT(*) AS total,
SUM(CASE WHEN edad >= 18 THEN 1 ELSE 0 END) AS adultos,
SUM(CASE WHEN edad < 18 THEN 1 ELSE 0 END) AS menores
FROM clientes;
| total | adultos | menores |
|---|---|---|
| 5 | 4 | 1 |
El CASE convierte cada fila en un 1 (si cumple) o un 0 (si no), y SUM suma esos unos.
Una variante equivalente usa COUNT sin ELSE. Como COUNT no cuenta los NULL, y el CASE sin ELSE devuelve NULL cuando no se cumple la condición, solo cuenta las filas que sí la cumplen:
SELECT
SUM(CASE WHEN importe >= 50 THEN importe ELSE 0 END) AS ventas_grandes,
SUM(CASE WHEN importe < 50 THEN importe ELSE 0 END) AS ventas_pequenias,
COUNT(CASE WHEN importe >= 50 THEN 1 END) AS pedidos_grandes
FROM pedidos;
| ventas_grandes | ventas_pequenias | pedidos_grandes |
|---|---|---|
| 180.50 | 91.24 | 2 |
Aquí SUM no suma unos, sino el propio importe: 120.50 + 60.00 = 180.50 para los pedidos de 50 € o más.
Una tabla de doble entrada con GROUP BY
Combinado con GROUP BY, que verás en dos lecciones, obtienes un recuento por ciudad y por tramo, como una tabla dinámica de una hoja de cálculo:
SELECT ciudad,
COUNT(*) AS clientes,
SUM(CASE WHEN edad >= 18 THEN 1 ELSE 0 END) AS adultos,
SUM(CASE WHEN edad < 18 THEN 1 ELSE 0 END) AS menores
FROM clientes
GROUP BY ciudad;
| ciudad | clientes | adultos | menores |
|---|---|---|---|
| Bilbao | 1 | 1 | 0 |
| Madrid | 2 | 1 | 1 |
| Santander | 2 | 2 | 0 |
También puedes agrupar por el resultado de un CASE para contar cuántos pedidos hay en cada tramo:
SELECT
CASE WHEN importe < 50 THEN 'Menos de 50' ELSE '50 o más' END AS tramo,
COUNT(*) AS pedidos,
SUM(importe) AS total
FROM pedidos
GROUP BY tramo;
| tramo | pedidos | total |
|---|---|---|
| 50 o más | 2 | 180.50 |
| Menos de 50 | 3 | 91.24 |
Nota: agrupar por un alias (
GROUP BY tramo) funciona en MySQL, PostgreSQL y SQLite, aunque el estándar no lo recoge. En otros sistemas, como SQL Server o versiones antiguas de Oracle, tienes que repetir elCASEcompleto en elGROUP BY.
Consejo: en MySQL y SQLite una comparación vale 1 si es verdadera y 0 si es falsa, así que
SUM(edad >= 18)también cuenta los adultos. Es cómodo, pero no funciona en PostgreSQL.SUM(CASE ...)funciona en todas partes.
CASE en UPDATE
CASE también sirve para calcular el nuevo valor en un UPDATE. Para verlo, creamos una tabla de productos de una tienda de montaña:
CREATE TABLE productos (
id INT PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
categoria VARCHAR(50) NOT NULL,
precio DECIMAL(8, 2) NOT NULL,
stock INT NOT NULL DEFAULT 0
);
INSERT INTO productos VALUES
(1, 'Pies de gato', 'Escalada', 89.90, 12),
(2, 'Arnés', 'Escalada', 59.00, 0),
(3, 'Mochila 30 L', 'Montaña', 45.50, 3),
(4, 'Tienda 2 plazas', 'Camping', 120.00, 7),
(5, 'Linterna frontal', 'Camping', 24.95, 25);
Primero, un SELECT que muestra el estado del stock en la web:
SELECT nombre, stock,
CASE
WHEN stock = 0 THEN 'Agotado'
WHEN stock <= 5 THEN 'Últimas unidades'
ELSE 'Disponible'
END AS estado
FROM productos;
| nombre | stock | estado |
|---|---|---|
| Pies de gato | 12 | Disponible |
| Arnés | 0 | Agotado |
| Mochila 30 L | 3 | Últimas unidades |
| Tienda 2 plazas | 7 | Disponible |
| Linterna frontal | 25 | Disponible |
Ahora llegan las rebajas: un 10 % en escalada, un 15 % en camping y nada en el resto. Sin CASE necesitarías dos UPDATE. Con CASE, uno solo:
UPDATE productos
SET precio = CASE categoria
WHEN 'Escalada' THEN ROUND(precio * 0.90, 2)
WHEN 'Camping' THEN ROUND(precio * 0.85, 2)
ELSE precio
END;
SELECT id, nombre, categoria, precio FROM productos;
| id | nombre | categoria | precio |
|---|---|---|---|
| 1 | Pies de gato | Escalada | 80.91 |
| 2 | Arnés | Escalada | 53.10 |
| 3 | Mochila 30 L | Montaña | 45.50 |
| 4 | Tienda 2 plazas | Camping | 102.00 |
| 5 | Linterna frontal | Camping | 21.21 |
El ELSE precio es imprescindible. Sin él, la mochila (categoría Montaña) no cumpliría ningún WHEN, el CASE devolvería NULL y el UPDATE intentaría dejarla sin precio. Aquí la columna es NOT NULL y daría error; si no lo fuera, perderías el precio sin darte cuenta.
Cuidado: este
UPDATEno tieneWHEREy toca todas las filas a propósito. Antes de ejecutarlo, prueba el mismoCASEen unSELECTpara ver los precios nuevos junto a los antiguos.
IF() en MySQL y alternativas
Cuando solo hay dos posibilidades, MySQL ofrece una función más corta: IF(condicion, valor_si_verdadero, valor_si_falso).
-- Solo MySQL y MariaDB
SELECT nombre, IF(edad >= 18, 'Sí', 'No') AS mayor_de_edad
FROM clientes;
| nombre | mayor_de_edad |
|---|---|
| Ana García | Sí |
| Luis Pérez | Sí |
| Marta Ruiz | Sí |
| Pablo Díaz | No |
| Eva Martín | Sí |
Es cómoda, pero no es estándar: PostgreSQL no la tiene, y SQLite y SQL Server usan otro nombre, IIF(condicion, a, b). Además, anidar varios IF para más de dos casos (IF(edad < 18, 'Menor', IF(edad < 30, 'Joven', 'Adulto'))) se lee mucho peor que un CASE.
| Sistema | Dos casos | Varios casos |
|---|---|---|
| MySQL / MariaDB | IF(cond, a, b) | CASE |
| PostgreSQL | CASE | CASE |
| SQLite | IIF(cond, a, b) | CASE |
Errores frecuentes
- Olvidar el
END. Es el fallo más común. El mensaje suele ser poco claro (syntax error near FROM), porque la base de datos sigue leyendo elCASEhasta que encuentra algo que no encaja. CadaCASEnecesita suEND. - Olvidar el
ELSEy obtenerNULL. SinELSE, las filas que no cumplen ningúnWHENsalen comoNULL. En unUPDATEpuede borrar datos. - Poner los
WHENen mal orden. Con rangos, gana la primera condición verdadera.WHEN edad < 45antes deWHEN edad < 18clasifica a todos como adultos. - Separar los
WHENcon comas. Se escriben uno detrás de otro, sin comas:WHEN a THEN 1 WHEN b THEN 2. - Usar el
CASEsimple para buscarNULL.CASE columna WHEN NULLnunca se cumple. UsaCASE WHEN columna IS NULL. - Mezclar tipos en los
THEN: números en unos, textos en otros. - Copiar
IF()de MySQL a PostgreSQL. No existe.CASEfunciona en todas partes.
Resumen
| Quieres… | Escribe |
|---|---|
| Clasificar por rangos | CASE WHEN x < 18 THEN 'a' WHEN x < 30 THEN 'b' ELSE 'c' END |
| Traducir valores fijos | CASE ciudad WHEN 'Bilbao' THEN 'País Vasco' ... END |
| Un orden a medida | ORDER BY CASE WHEN ciudad = 'Santander' THEN 0 ELSE 1 END |
| Contar los que cumplen algo | SUM(CASE WHEN cond THEN 1 ELSE 0 END) o COUNT(CASE WHEN cond THEN 1 END) |
| Sumar solo algunos importes | SUM(CASE WHEN cond THEN importe ELSE 0 END) |
| Actualizar con reglas distintas | UPDATE t SET col = CASE ... ELSE col END |
| Dos casos en MySQL | IF(cond, a, b) |
- La primera condición verdadera gana; las demás no se miran.
- Sin
ELSE, el resultado esNULL. - Todo
CASEtermina enEND.
En la siguiente lección profundizarás en las funciones de agregación: COUNT, SUM, AVG, MIN y MAX.
Pon a prueba lo que has aprendido
¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.