COALESCE sabe responder a una sola pregunta: "¿es nulo?". Todas las demás —¿es caro?, ¿queda poco stock?, ¿en qué tramo cae este cliente?— necesitan algo más general. CASE es ese algo: el if de SQL, y la última pieza del módulo 6. Con él una consulta deja de limitarse a devolver y transformar datos y empieza a decidir: clasificar un catálogo en gamas de precio, poner un semáforo de stock, ordenar los estados de un pedido por su orden de flujo en lugar de alfabéticamente, o convertir filas en columnas para un informe de dirección.
Contenido
CASEes una expresión, no una sentenciaCASEsimple frente aCASEbuscadoELSE, y qué pasa cuando falta- El orden de evaluación: la primera condición verdadera gana
CASEen elSELECT: clasificarCASEen elWHEREy en elORDER BYCASEen elGROUP BY- El patrón de tabla pivote
CASE,FILTERyCOALESCE: cuándo cada uno- Errores Comunes y Consejos
- Ejercicios
- Conclusión del módulo
CASE es una expresión, no una sentencia
CASE es una expresión, no una sentenciaLa idea que hay que fijar antes que la sintaxis: CASE devuelve un valor. No ejecuta bloques de código, no salta, no controla el flujo del programa. Es una expresión, exactamente igual que precio * 1.21 o UPPER(nombre).
De ahí se deriva todo lo demás: puede ir en cualquier sitio donde quepa un valor (SELECT, WHERE, ORDER BY, GROUP BY, HAVING, dentro de una función o de un agregado); todas sus ramas deben devolver un tipo compatible, no un número en una y un texto en otra; y devuelve exactamente un valor por fila, como toda función escalar (06-01).
SELECT id, nombre, precio,
CASE WHEN precio >= 15 THEN 'Premium' ELSE 'Estándar' END AS gama,
precio * CASE WHEN precio >= 15 THEN 0.90 ELSE 1.00 END AS precio_promocion
FROM productos WHERE id IN (5, 6, 15) ORDER BY id;| id | nombre | precio | gama | precio_promocion |
|---|---|---|---|---|
| 5 | Tomate triturado ecológico 400 g | 1.95 | Estándar | 1.9500 |
| 6 | Crema facial de aloe vera 50 ml | 18.90 | Premium | 17.0100 |
| 15 | Té verde matcha ceremonial 30 g | 22.00 | Premium | 19.8000 |
La segunda CASE está dentro de una multiplicación: es un operando más. Y fíjate en los cuatro decimales de precio_promocion: NUMERIC(10,2) * NUMERIC(3,2) da escala 4, y redondearías al presentar (06-02).
No confundir con el
IFprocedimental. PL/pgSQL —el lenguaje de los procedimientos, módulo 10— sí tiene una sentenciaIF … THEN … END IFque ejecuta código; elCASEde SQL no ejecuta nada, se evalúa y produce un valor.
CASE simple frente a CASE buscado
CASE simple frente a CASE buscadoHay dos formas y no son intercambiables. El CASE simple compara una expresión con una lista de valores; el buscado, condiciones completas e independientes:
CASE expresion CASE WHEN condicion1 THEN resultado1
WHEN valor1 THEN resultado1 WHEN condicion2 THEN resultado2
WHEN valor2 THEN resultado2 ELSE resultado_por_defecto
ELSE resultado_por_defecto END
ENDCASE simple |
CASE buscado |
|
|---|---|---|
| Compara | Una expresión con valores concretos | Condiciones booleanas cualesquiera |
| Operadores | Solo igualdad implícita | >, <, BETWEEN, LIKE, IS NULL, AND, OR… |
Varias columnas / detecta NULL |
No / No (ver abajo) | Sí / Sí, con IS NULL |
| Cuándo usarlo | Traducir códigos, dominios cerrados | Rangos, comparaciones, todo lo demás |
El CASE simple es perfecto para traducir un dominio cerrado —metodo_pago, estado, un código de país— y el buscado sirve para todo lo demás. Y hay un caso donde el simple no puede hacer el trabajo:
-- ⚠️ INCORRECTA: nunca entra en la rama del NULL
SELECT id, CASE empleado_id WHEN NULL THEN 'Venta web' ELSE 'Con comercial' END AS canal
FROM pedidos WHERE id IN (1, 2) ORDER BY id;
-- ✅ CORRECTA
SELECT id, CASE WHEN empleado_id IS NULL THEN 'Venta web' ELSE 'Con comercial' END AS canal
FROM pedidos WHERE id IN (1, 2) ORDER BY id;| id | canal (incorrecta) | canal (correcta) |
|---|---|---|
| 1 | Con comercial | Venta web |
| 2 | Con comercial | Con comercial |
El pedido 1 no tiene comercial y la primera consulta dice "Con comercial". La razón es 04-03 al pie de la letra: el CASE simple compara con = y empleado_id = NULL se evalúa a UNKNOWN, nunca a TRUE. La rama WHEN NULL es código muerto: no se ejecutará jamás, en ninguna fila.
Regla: en cuanto un
NULLentre en juego,CASEbuscado conIS NULL. El simple no puede detectar la ausencia de valor y, lo peor, no da error: devuelve la rama equivocada en silencio.
ELSE, y qué pasa cuando falta
ELSE, y qué pasa cuando faltaELSE es opcional, y si lo omites y ninguna condición se cumple, CASE devuelve NULL.
SELECT id, estado,
CASE estado WHEN 'entregado' THEN 'Cerrado' WHEN 'cancelado' THEN 'Anulado' END
AS situacion_sin_else
FROM pedidos WHERE id IN (1, 6, 17, 20) ORDER BY id;| id | estado | situacion_sin_else |
|---|---|---|
| 1 | entregado | Cerrado |
| 6 | cancelado | Anulado |
| 17 | enviado | (null) |
| 20 | pendiente | (null) |
Los estados enviado y pendiente no encajan en ninguna rama y salen NULL. Esos nulos son traicioneros porque no vienen de los datos: los fabrica tu propia expresión. Si luego agrupas por esa columna tendrás un grupo NULL que nadie pidió; si la usas en un WHERE, esas filas desaparecerán (04-03). Escribe siempre el ELSE, aunque sea ELSE 'Otro' o ELSE NULL explícito: un ELSE NULL a mano dice "he pensado en este caso" y un ELSE ausente dice "se me olvidó", y dentro de seis meses no sabrás cuál era.
- El orden de evaluación: la primera condición verdadera gana
CASE evalúa sus WHEN de arriba abajo y se detiene en el primero que sea TRUE; los demás no se evalúan siquiera (es la misma pereza de COALESCE, que no en vano es un CASE disfrazado). Eso convierte el orden en parte de la lógica y produce el error más frecuente con CASE: poner el rango más amplio primero.
-- ⚠️ INCORRECTA: todo cae en la primera rama
SELECT id, nombre, precio,
CASE WHEN precio < 25 THEN 'Económico' WHEN precio < 15 THEN 'Medio'
WHEN precio < 5 THEN 'Barato' ELSE 'Premium' END AS gama
FROM productos WHERE id IN (5, 8, 15) ORDER BY id;| id | nombre | precio | gama |
|---|---|---|---|
| 5 | Tomate triturado ecológico 400 g | 1.95 | Económico |
| 8 | Aceite corporal de almendras 200 ml | 14.25 | Económico |
| 15 | Té verde matcha ceremonial 30 g | 22.00 | Económico |
Los tres productos, del más barato al más caro, caen en la misma gama: todos cumplen precio < 25 y las demás condiciones nunca se evalúan. La consulta no da error; simplemente clasifica mal el catálogo entero. La versión correcta ordena las condiciones de la más restrictiva a la más general:
-- ✅ CORRECTA
SELECT id, nombre, precio,
CASE WHEN precio < 5 THEN 'Económico' WHEN precio < 15 THEN 'Medio'
ELSE 'Premium' END AS gama
FROM productos WHERE id IN (5, 8, 15) ORDER BY id;| id | nombre | precio | gama |
|---|---|---|---|
| 5 | Tomate triturado ecológico 400 g | 1.95 | Económico |
| 8 | Aceite corporal de almendras 200 ml | 14.25 | Medio |
| 15 | Té verde matcha ceremonial 30 g | 22.00 | Premium |
Como la primera verdadera gana, cada rama solo necesita su límite superior: no hace falta escribir WHEN precio >= 5 AND precio < 15. Esa es la elegancia del CASE en cascada, y también su trampa: si reordenas las líneas, cambias el resultado.
flowchart LR
B{"¿precio < 5?"} -->|"sí"| C["'Económico'"]
B -->|"no"| D{"¿precio < 15?"} -->|"sí"| E["'Medio'"]
D -->|"no"| F["ELSE → 'Premium'"]
CASE en el SELECT: clasificar
CASE en el SELECT: clasificarEl uso principal. Un semáforo de stock combinado con la gama de precio:
SELECT id, nombre, precio, stock,
CASE WHEN precio < 5 THEN 'Económico'
WHEN precio < 15 THEN 'Medio'
ELSE 'Premium' END AS gama,
CASE WHEN stock = 0 THEN '🔴 Sin stock'
WHEN stock < 50 THEN '🟠 Bajo'
WHEN stock < 150 THEN '🟡 Normal'
ELSE '🟢 Alto' END AS semaforo
FROM productos WHERE id IN (5, 8, 13, 15) ORDER BY id;| id | nombre | precio | stock | gama | semaforo |
|---|---|---|---|---|---|
| 5 | Tomate triturado ecológico 400 g | 1.95 | 300 | Económico | 🟢 Alto |
| 8 | Aceite corporal de almendras 200 ml | 14.25 | 45 | Medio | 🟠 Bajo |
| 13 | Velas de cera de soja (pack 2) | 13.75 | 0 | Medio | 🔴 Sin stock |
| 15 | Té verde matcha ceremonial 30 g | 22.00 | 40 | Premium | 🟠 Bajo |
El producto 13 —el único del catálogo con stock 0— queda identificado sin buscarlo. Ese es el valor de un semáforo: convertir un número que hay que interpretar en una etiqueta que se lee de un vistazo.
CASE en el WHERE y en el ORDER BY
CASE en el WHERE y en el ORDER BYEn el WHERE: se puede, pero casi nunca conviene
Como CASE devuelve un valor, puedes compararlo:
-- ⚠️ Funciona, pero es rebuscado
SELECT id, nombre FROM productos
WHERE CASE WHEN precio < 5 THEN 'Económico' ELSE 'Otro' END = 'Económico';
-- ✅ CORRECTA: dice lo mismo en una línea
SELECT id, nombre FROM productos WHERE precio < 5;Las dos devuelven los 7 productos de menos de 5 €, pero un CASE en el WHERE es más largo, más difícil de leer y —lo importante— impide usar el índice, porque la columna queda envuelta en una expresión (08-03). Casi siempre lo que quieres es un OR, un IN o un BETWEEN. La única excepción razonable es un filtro cuya condición depende de un parámetro (WHERE columna = CASE WHEN $1 = 'todos' THEN columna ELSE $1 END), y aun ahí hay soluciones mejores.
En el ORDER BY: aquí sí, y mucho
Aquí CASE no tiene sustituto. Los estados de un pedido tienen un orden de flujo —pendiente → pagado → enviado → entregado, con cancelado aparte— que no coincide con el alfabético:
SELECT estado, COUNT(*) AS pedidos
FROM pedidos GROUP BY estado
ORDER BY CASE estado WHEN 'pendiente' THEN 1 WHEN 'pagado' THEN 2
WHEN 'enviado' THEN 3 WHEN 'entregado' THEN 4
WHEN 'cancelado' THEN 5 END;| estado | pedidos |
|---|---|
| pendiente | 1 |
| pagado | 2 |
| enviado | 2 |
| entregado | 14 |
| cancelado | 1 |
Ordenado alfabéticamente saldría cancelado, entregado, enviado, pagado, pendiente: una secuencia sin significado que obliga al lector a recomponer mentalmente el ciclo de vida. Aquí el CASE es la información. Fíjate en dos detalles: es un CASE simple, porque compara con valores de un dominio cerrado, y la expresión del ORDER BY no aparece en el SELECT, cosa perfectamente legal (02-05). Otra variante muy útil, "esto primero y el resto después", es ORDER BY CASE WHEN estado = 'pendiente' THEN 0 ELSE 1 END, fecha_pedido DESC.
CASE en el GROUP BY
CASE en el GROUP BYSi puedes clasificar en el SELECT, puedes agrupar por la clasificación. La única regla es la de 04-05: repetir la expresión completa en el GROUP BY, no el alias.
SELECT CASE WHEN precio < 5 THEN 'Económico'
WHEN precio < 15 THEN 'Medio'
ELSE 'Premium' END AS gama,
COUNT(*) AS productos, ROUND(AVG(precio), 2) AS precio_medio
FROM productos
GROUP BY CASE WHEN precio < 5 THEN 'Económico'
WHEN precio < 15 THEN 'Medio'
ELSE 'Premium' END
ORDER BY precio_medio;| gama | productos | precio_medio |
|---|---|---|
| Económico | 7 | 3.56 |
| Medio | 10 | 9.85 |
| Premium | 3 | 19.10 |
7 + 10 + 3 = 20 productos, y la media general del catálogo sigue siendo 9,035 €. La duplicación de la expresión es fea pero necesaria: el GROUP BY se evalúa antes que el SELECT (orden lógico del módulo 2) y el alias gama todavía no existe.
Nota de dialecto: PostgreSQL y SQL Server exigen repetir la expresión; MySQL, SQLite y MariaDB permiten
GROUP BY gamausando el alias delSELECT, que es cómodo y no es estándar. La alternativa limpia —dar nombre a la clasificación una sola vez— es una CTE, y llega en 10-02.
- El patrón de tabla pivote
Y llegamos al uso más potente de CASE: meterlo dentro de un agregado para convertir filas en columnas. El problema de partida es el informe de 04-05: la facturación por categoría y año sale como una lista larga con una fila por combinación, y dirección la quiere como tabla de doble entrada. La idea es de una simplicidad engañosa: SUM(CASE WHEN año = 2025 THEN importe ELSE 0 END) suma solo lo de 2025, porque el resto de filas aportan un cero; repite el truco con otra condición y tienes otra columna.
SELECT cat.id,
cat.nombre AS categoria,
ROUND(SUM(CASE WHEN EXTRACT(YEAR FROM pe.fecha_pedido) = 2025
THEN lp.cantidad * lp.precio_unitario * (1 - lp.descuento)
ELSE 0 END), 2) AS anio_2025,
ROUND(SUM(CASE WHEN EXTRACT(YEAR FROM pe.fecha_pedido) = 2026
THEN lp.cantidad * lp.precio_unitario * (1 - lp.descuento)
ELSE 0 END), 2) AS anio_2026,
ROUND(SUM(CASE WHEN lp.id IS NOT NULL
THEN lp.cantidad * lp.precio_unitario * (1 - lp.descuento)
ELSE 0 END), 2) AS total
FROM categorias AS cat
LEFT JOIN productos AS p ON p.categoria_id = cat.id
LEFT JOIN lineas_pedido AS lp ON lp.producto_id = p.id
LEFT JOIN pedidos AS pe ON pe.id = lp.pedido_id
GROUP BY cat.id, cat.nombre ORDER BY cat.id;| id | categoria | anio_2025 | anio_2026 | total |
|---|---|---|---|---|
| 1 | Alimentación | 215.67 | 40.60 | 256.27 |
| 2 | Cosmética natural | 128.22 | 28.10 | 156.32 |
| 3 | Hogar sostenible | 88.58 | 0.00 | 88.58 |
| 4 | Bebidas | 146.55 | 48.73 | 195.28 |
| 5 | Higiene personal | 24.50 | 7.00 | 31.50 |
| 6 | Complementos | 0.00 | 0.00 | 0.00 |
Las columnas cuadran con las cifras canónicas del curso: 603,52 € en 2025, 124,43 € en 2026 y 727,95 € en total. Y la lectura es inmediata: Hogar sostenible no ha vendido nada en 2026 y Complementos no ha vendido nunca — algo que en una lista de doce filas por categoría y año se ve mucho peor.
Tres cosas que hay que entender del patrón. Las columnas son fijas: si mañana hay datos de 2027 hay que editar la consulta, porque SQL decide la lista de columnas al analizarla, antes de leer un solo dato. El ELSE 0 es el motor del truco: sin él las filas que no cumplen aportarían NULL, y un grupo entero de nulos daría NULL en lugar de 0. Y sirve igual con COUNT, AVG o MAX — con COUNT se escribe sin ELSE, precisamente porque COUNT ignora los nulos.
Otro pivote, esta vez con COUNT: métodos de pago por país del cliente.
SELECT pe.metodo_pago,
COUNT(CASE WHEN c.pais = 'España' THEN 1 END) AS espana,
COUNT(CASE WHEN c.pais = 'Portugal' THEN 1 END) AS portugal,
COUNT(CASE WHEN c.pais = 'Francia' THEN 1 END) AS francia,
COUNT(*) AS total
FROM pedidos AS pe JOIN clientes AS c ON c.id = pe.cliente_id
GROUP BY pe.metodo_pago ORDER BY total DESC, pe.metodo_pago;| metodo_pago | espana | portugal | francia | total |
|---|---|---|---|---|
| tarjeta | 9 | 1 | 1 | 11 |
| paypal | 2 | 2 | 0 | 4 |
| transferencia | 2 | 0 | 1 | 3 |
| contrareembolso | 1 | 0 | 1 | 2 |
11 + 4 + 3 + 2 = 20 pedidos. La tarjeta domina en España, mientras que los dos clientes portugueses con pedidos usan exclusivamente PayPal: una conclusión comercial que la lista plana no dejaba ver.
Alternativa:
crosstab. La extensióntablefuncde PostgreSQL traecrosstab(), que genera pivotes a partir de una consulta de tres columnas (fila, columna, valor). Ahorra escribir unCASEpor columna, pero obliga a declarar los tipos de salida a mano y es específica de PostgreSQL; la verás aplicada en la práctica del módulo 11. Para tres o cuatro columnas,SUM(CASE …)sigue siendo la opción más clara y la única portable.
CASE, FILTER y COALESCE: cuándo cada uno
CASE, FILTER y COALESCE: cuándo cada unoEn 04-04 conociste FILTER (WHERE …) y se dijo que su equivalente con CASE se explicaría aquí. Estas dos expresiones dicen lo mismo:
FILTER (WHERE …) |
SUM(CASE WHEN …) |
|
|---|---|---|
| Legibilidad | Muy alta: la condición está separada | Media: la condición va dentro |
| Disponibilidad | PostgreSQL 9.4+; no existe en MySQL, SQL Server ni SQLite | Todos los motores |
| Grupo sin ninguna fila que pase | Devuelve NULL |
Devuelve 0 si pones ELSE 0 |
| Fuera de un agregado | No se puede | Sí |
La penúltima fila es la que decide muchas veces: en el pivote anterior, Complementos muestra 0.00 porque el ELSE 0 aporta ceros; con FILTER mostraría *(null)* y habría que envolverlo en un COALESCE (06-04). Ninguna es mejor: FILTER es más legible, CASE … ELSE 0 es más portable y controla el valor por defecto.
Y la regla que cierra el cuarteto:
| Situación | Herramienta |
|---|---|
| "Si es nulo, pon esto otro" | COALESCE |
| "Si es nulo o cumple otra condición…" | CASE |
| "Convierte este valor concreto en nulo" | NULLIF |
| "Agrega solo las filas que cumplen X" | FILTER o CASE dentro del agregado |
COALESCE(coste, 0) y CASE WHEN coste IS NULL THEN 0 ELSE coste END son idénticos y el primero es mejor; pero en cuanto la regla se complica —"si el coste es nulo pon 0, y si además el producto está descatalogado pon −1"— COALESCE no llega y CASE sí.
Errores Comunes y Consejos
- Poner el rango más amplio primero. La primera condición verdadera gana y las demás son código muerto: de la más restrictiva a la más general. Y el
CASEsimple conNULL(CASE columna WHEN NULL THEN …) nunca entra en esa rama: usaCASE WHEN columna IS NULL THEN …. - Omitir el
ELSE. Las filas que no encajan salenNULL, y son nulos que fabrica tu consulta, no los datos. - Mezclar tipos entre ramas. Todas deben devolver un tipo compatible; si no,
ERROR: CASE types text and integer cannot be matched. - Usar el alias del
CASEen elGROUP BY. En PostgreSQL hay que repetir la expresión, porque elGROUP BYse evalúa antes que elSELECT. Y evita meter unCASEen elWHEREcuando basta unOR: más largo, menos legible y sin índice (08-03). - Olvidar el
ELSE 0en un pivote conSUM. El grupo sin filas coincidentes daráNULLen lugar de0. Y al revés: conCOUNTelELSE 0sobra y falsea el recuento, porque0no es nulo y se cuenta. - Esperar que un pivote genere columnas solo. La lista de columnas es fija; un año nuevo exige editar la consulta.
- Consejo: si el
CASEtiene más de cuatro ramas, plantéate una tabla de referencia. UnCASEde veinte líneas repetido en diez informes es una tabla de dominio que alguien debería haber creado (módulo 5). - Consejo: usa
FILTERen PostgreSQL yCASEcuando necesites portabilidad, y alinea las ramas verticalmente: unCASEbien indentado se lee como una tabla.
Ejercicios
Ejercicio 1
Dirección quiere el cuadro de mando del catálogo. Agrupa los productos activos por gama (< 5 Económico, < 15 Medio, resto Premium) y muestra el número de productos, el precio medio y cuántos de ellos tienen menos de 50 unidades en stock. (Pista: COUNT(CASE WHEN … THEN 1 END) o FILTER.)
Ejercicio 2
Un compañero ha escrito esta clasificación de portes y concluye que "todos nuestros pedidos llevan portes":
-- ⚠️ Sospechosa
SELECT id, estado,
CASE WHEN gastos_envio >= 0 THEN 'Con portes'
WHEN gastos_envio > 10 THEN 'Portes caros'
WHEN gastos_envio = 0 THEN 'Envío gratis'
END AS tipo_envio
FROM pedidos ORDER BY id;(1) ¿Cuántas etiquetas distintas puede devolver realmente y por qué? (2) Corrígela para que distinga de verdad los tres casos y da el recuento de cada uno. (3) La consulta no tiene ELSE: ¿por qué no se nota, y cuándo se notaría?
Ejercicio 3
Construye la tabla pivote de pedidos por estado y año: una fila por estado y columnas p2025, p2026 y total, ordenada por el orden de flujo (pendiente, pagado, enviado, entregado, cancelado) y no alfabéticamente. Escríbela dos veces, con SUM(CASE …) y con COUNT(*) FILTER (WHERE …), y explica en qué se diferencian los resultados.
Soluciones
Solución 1
SELECT CASE WHEN precio < 5 THEN 'Económico'
WHEN precio < 15 THEN 'Medio'
ELSE 'Premium' END AS gama,
COUNT(*) AS productos,
ROUND(AVG(precio), 2) AS precio_medio,
COUNT(CASE WHEN stock < 50 THEN 1 END) AS con_stock_critico
FROM productos WHERE activo = TRUE
GROUP BY CASE WHEN precio < 5 THEN 'Económico'
WHEN precio < 15 THEN 'Medio'
ELSE 'Premium' END
ORDER BY precio_medio;| gama | productos | precio_medio | con_stock_critico |
|---|---|---|---|
| Económico | 7 | 3.56 | 0 |
| Medio | 10 | 9.85 | 2 |
| Premium | 2 | 20.45 | 1 |
19 productos, no 20: el WHERE activo = TRUE excluye el 20 (Cápsulas de espirulina, 16,40 €), que era Premium — por eso esa gama baja de 3 a 2 y su precio medio sube a 20,45 €. Los tres productos con stock crítico son el 8 (45 unidades), el 13 (0) y el 15 (40), repartidos entre Medio y Premium: los productos caros son los que se quedan sin existencias. Y COUNT(CASE WHEN stock < 50 THEN 1 END) va sin ELSE a propósito: las filas que no cumplen devuelven NULL y COUNT las ignora; con ELSE 0 contaría los 19.
Solución 2
1. Solo una: 'Con portes'. Todos los gastos_envio de TiendaVerde son >= 0 —la restricción CHECK de 01-06 lo garantiza—, así que la primera condición es verdadera para las veinte filas y las otras dos son código muerto. Es el error de la sección 4 en estado puro: la condición más general, primero. 2. Ordenando de lo más restrictivo a lo más general:
-- ✅ CORRECTA
SELECT CASE WHEN gastos_envio = 0 THEN 'Envío gratis'
WHEN gastos_envio > 10 THEN 'Portes caros'
ELSE 'Portes normales' END AS tipo_envio,
COUNT(*) AS pedidos,
ROUND(SUM(gastos_envio), 2) AS portes
FROM pedidos
GROUP BY CASE WHEN gastos_envio = 0 THEN 'Envío gratis'
WHEN gastos_envio > 10 THEN 'Portes caros'
ELSE 'Portes normales' END
ORDER BY portes DESC;| tipo_envio | pedidos | portes |
|---|---|---|
| Portes normales | 13 | 80.75 |
| Portes caros | 3 | 37.50 |
| Envío gratis | 4 | 0.00 |
20 pedidos y 118,25 € de portes, la cifra canónica del módulo 4: cuatro pedidos con envío gratuito y tres con portes de 12,50 €.
3. No se nota únicamente porque la primera condición captura todas las filas. Es una bomba de relojería: el día que alguien inserte un pedido con gastos_envio nulo —hoy imposible por el NOT NULL, pero los esquemas cambian (05-06)—, esa fila devolvería NULL en tipo_envio y sería un grupo fantasma en el informe. Escribe siempre el ELSE.
Solución 3
SELECT estado,
SUM(CASE WHEN EXTRACT(YEAR FROM fecha_pedido) = 2025 THEN 1 ELSE 0 END) AS p2025,
SUM(CASE WHEN EXTRACT(YEAR FROM fecha_pedido) = 2026 THEN 1 ELSE 0 END) AS p2026,
COUNT(*) AS total
FROM pedidos GROUP BY estado
ORDER BY CASE estado WHEN 'pendiente' THEN 1 WHEN 'pagado' THEN 2
WHEN 'enviado' THEN 3 WHEN 'entregado' THEN 4
WHEN 'cancelado' THEN 5 END;| estado | p2025 | p2026 | total |
|---|---|---|---|
| pendiente | 0 | 1 | 1 |
| pagado | 0 | 2 | 2 |
| enviado | 1 | 1 | 2 |
| entregado | 14 | 0 | 14 |
| cancelado | 1 | 0 | 1 |
La versión con FILTER cambia solo las dos columnas centrales, que pasan a ser COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM fecha_pedido) = 2025) AS p2025 y su equivalente para 2026. Y aquí los resultados coinciden exactamente, incluidos los ceros, porque COUNT sobre un conjunto vacío devuelve 0 y no NULL (04-04): FILTER con COUNT es seguro. Si en lugar de COUNT(*) usaras SUM(gastos_envio) FILTER (…), los grupos sin filas de ese año darían *(null)* mientras que SUM(CASE … ELSE 0 END) daría 0.00. La diferencia no está en FILTER frente a CASE: está en qué devuelve cada agregado cuando no tiene nada que agregar.
El informe, leído: TiendaVerde tiene 14 pedidos entregados, todos de 2025, y los cuatro de 2026 siguen en curso —uno pendiente, dos pagados, uno enviado—.
Conclusión del módulo
Con CASE cierras el módulo 6:
CASEes una expresión, no una sentencia: devuelve un valor y cabe en cualquier cláusula, incluso dentro de una multiplicación o de un agregado.- Distingues el
CASEsimple —para dominios cerrados— del buscado —para rangos y condiciones—, y sabes que el simple no puede detectarNULLporque compara con=. Escribes siempre elELSE, porque sin él las filas que no encajan devuelven nulos que fabrica tu propia consulta. - Ordenas las condiciones de la más restrictiva a la más general: la primera verdadera gana y el resto es código muerto.
- Lo usas en el
SELECTpara clasificar, en elORDER BYpara imponer un orden de negocio que el alfabético no puede dar, y en elGROUP BYpara agregar por la clasificación repitiendo la expresión completa. - Dominas el patrón de tabla pivote,
SUM(CASE WHEN … THEN … ELSE 0 END), con sus límites (columnas fijas) y su equivalente modernoFILTER. Y sabes elegir entreCOALESCE,NULLIF,CASEyFILTERsegún lo que estés preguntando.
Y con esta lección termina el módulo 6 entero. Empezaste sin poder juntar nombre y apellidos en una columna; ahora compones texto, extraes gramajes con expresiones regulares, redondeas dinero sin perder céntimos, agrupas por mes y por trimestre, conviertes tipos con intención, domas los nulos y clasificas filas con lógica condicional. Tus consultas ya no devuelven datos: devuelven respuestas.
Pero todas esas respuestas se calculan mirando una fila cada vez, o un grupo cada vez. Y hay una familia entera de preguntas que no funciona así, porque necesita comparar cada fila con el resultado de otra consulta: ¿qué productos están por encima de la media de su categoría?; ¿qué clientes tienen un ticket medio superior a la media general —la pregunta que quedó explícitamente pendiente en 04-06 cuando descubriste que HAVING no puede referirse a un agregado global—; ¿qué pedidos incluyen el producto más caro del catálogo?; ¿qué clientes no han comprado nunca, sin recurrir a un LEFT JOIN con IS NULL? Todas comparten la misma forma: una consulta dentro de otra consulta. En el módulo 7, Subconsultas, aprenderás a escribirlas: subconsultas escalares y de lista, correlacionadas —que se ejecutan una vez por fila—, EXISTS y NOT EXISTS, subconsultas en el SELECT, en el FROM y en el WHERE, y el criterio para decidir cuándo una subconsulta es la herramienta adecuada y cuándo lo correcto es un JOIN.
Curso de SQL
Módulo 1: Introducción a SQL
- ¿Qué es SQL?
- Configurando tu entorno SQL
- Sintaxis básica de SQL
- Entendiendo bases de datos y tablas
- El modelo relacional: claves primarias y foráneas
- La base de datos del curso: TiendaVerde
Módulo 2: Consultas básicas de SQL
- Instrucción SELECT
- Alias, expresiones y columnas calculadas
- Filtrando datos con WHERE
- DISTINCT y eliminación de duplicados
- Ordenando datos con ORDER BY
- Limitando resultados con LIMIT
Módulo 3: Trabajando con múltiples tablas
- Operaciones JOIN
- INNER JOIN
- LEFT JOIN
- RIGHT JOIN
- FULL OUTER JOIN
- SELF JOIN y CROSS JOIN
- Uniones de conjuntos: UNION, INTERSECT y EXCEPT
Módulo 4: Filtrado avanzado de datos
- Usando LIKE para coincidencia de patrones
- Operadores IN y BETWEEN
- Valores NULL y IS NULL
- Funciones de agregación: COUNT, SUM, AVG, MIN y MAX
- Agregando datos con GROUP BY
- Cláusula HAVING
Módulo 5: Manipulación de datos
- Creando tablas y restricciones con CREATE TABLE
- Instrucción INSERT
- Instrucción UPDATE
- Instrucción DELETE
- Instrucción UPSERT (MERGE)
- Modificando el esquema: ALTER TABLE y migraciones seguras
Módulo 6: Funciones avanzadas de SQL
- Funciones de cadena
- Funciones numéricas
- Funciones de fecha y hora
- Conversión de tipos y manejo de NULL: CAST y COALESCE
- Expresiones condicionales
Módulo 7: Subconsultas y consultas anidadas
- Introducción a subconsultas
- Subconsultas correlacionadas
- EXISTS y NOT EXISTS
- Usando subconsultas en cláusulas SELECT, FROM y WHERE
- Subconsultas o JOIN: cuál elegir
Módulo 8: Índices y optimización de rendimiento
- Entendiendo los índices
- Creación y gestión de índices
- Tipos de índice y cuándo no indexar
- Técnicas de optimización de consultas
- Análisis del rendimiento de consultas
Módulo 9: Transacciones y concurrencia
- Introducción a las transacciones
- Propiedades ACID
- Instrucciones de control de transacciones
- Niveles de aislamiento y anomalías de concurrencia
- Manejo de concurrencia: bloqueos e interbloqueos
Módulo 10: Temas avanzados
- Vistas
- Expresiones de tabla comunes (CTE)
- Funciones de ventana
- Procedimientos almacenados
- Triggers
- JSON y datos semiestructurados
Módulo 11: SQL en la práctica
- Casos de uso en el mundo real
- Mejores prácticas
- Seguridad: inyección SQL, permisos y roles
- SQL para análisis de datos
- SQL en desarrollo web
