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

  1. CASE es una expresión, no una sentencia
  2. CASE simple frente a CASE buscado
  3. ELSE, y qué pasa cuando falta
  4. El orden de evaluación: la primera condición verdadera gana
  5. CASE en el SELECT: clasificar
  6. CASE en el WHERE y en el ORDER BY
  7. CASE en el GROUP BY
  8. El patrón de tabla pivote
  9. CASE, FILTER y COALESCE: cuándo cada uno
  10. Errores Comunes y Consejos
  11. Ejercicios
  12. Conclusión del módulo

  1. CASE es una expresión, no una sentencia

La 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 IF procedimental. PL/pgSQL —el lenguaje de los procedimientos, módulo 10— sí tiene una sentencia IF … THEN … END IF que ejecuta código; el CASE de SQL no ejecuta nada, se evalúa y produce un valor.

  1. CASE simple frente a CASE buscado

Hay 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
END
CASE 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) / , 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 NULL entre en juego, CASE buscado con IS NULL. El simple no puede detectar la ausencia de valor y, lo peor, no da error: devuelve la rama equivocada en silencio.

  1. ELSE, y qué pasa cuando falta

ELSE 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.

  1. 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'"]

  1. CASE en el SELECT: clasificar

El 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.

  1. CASE en el WHERE y en el ORDER BY

En 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 flujopendientepagadoenviadoentregado, 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.

  1. CASE en el GROUP BY

Si 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 gama usando el alias del SELECT, 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.

  1. 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ón tablefunc de PostgreSQL trae crosstab(), que genera pivotes a partir de una consulta de tres columnas (fila, columna, valor). Ahorra escribir un CASE por 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.

  1. CASE, FILTER y COALESCE: cuándo cada uno

En 04-04 conociste FILTER (WHERE …) y se dijo que su equivalente con CASE se explicaría aquí. Estas dos expresiones dicen lo mismo:

SUM(importe) FILTER (WHERE anio = 2025)   -- ≡   SUM(CASE WHEN anio = 2025 THEN importe END)
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

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 CASE simple con NULL (CASE columna WHEN NULL THEN …) nunca entra en esa rama: usa CASE WHEN columna IS NULL THEN ….
  • Omitir el ELSE. Las filas que no encajan salen NULL, 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 CASE en el GROUP BY. En PostgreSQL hay que repetir la expresión, porque el GROUP BY se evalúa antes que el SELECT. Y evita meter un CASE en el WHERE cuando basta un OR: más largo, menos legible y sin índice (08-03).
  • Olvidar el ELSE 0 en un pivote con SUM. El grupo sin filas coincidentes dará NULL en lugar de 0. Y al revés: con COUNT el ELSE 0 sobra y falsea el recuento, porque 0 no 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 CASE tiene más de cuatro ramas, plantéate una tabla de referencia. Un CASE de veinte líneas repetido en diez informes es una tabla de dominio que alguien debería haber creado (módulo 5).
  • Consejo: usa FILTER en PostgreSQL y CASE cuando necesites portabilidad, y alinea las ramas verticalmente: un CASE bien 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:

  • CASE es 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 CASE simple —para dominios cerrados— del buscado —para rangos y condiciones—, y sabes que el simple no puede detectar NULL porque compara con =. Escribes siempre el ELSE, 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 SELECT para clasificar, en el ORDER BY para imponer un orden de negocio que el alfabético no puede dar, y en el GROUP BY para 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 moderno FILTER. Y sabes elegir entre COALESCE, NULLIF, CASE y FILTER segú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

Módulo 2: Consultas básicas de SQL

Módulo 3: Trabajando con múltiples tablas

Módulo 4: Filtrado avanzado de datos

Módulo 5: Manipulación de datos

Módulo 6: Funciones avanzadas de SQL

Módulo 7: Subconsultas y consultas anidadas

Módulo 8: Índices y optimización de rendimiento

Módulo 9: Transacciones y concurrencia

Módulo 10: Temas avanzados

Módulo 11: SQL en la práctica

Módulo 12: Proyecto final

© Copyright 2026. Todos los derechos reservados