SQL: Lógica Condicional con la Expresión CASE

SQL: Lógica Condicional con la Expresión CASE

Tabla de Contenido

Cuando una consulta necesita clasificar registros, calcular valores derivados o protegerse de casos límite, la lógica condicional en SQL evita mover decisiones simples a la capa de aplicación. Después de trabajar con filtrado de datos con WHERE, el siguiente paso natural es usar la expresión CASE para transformar resultados directamente dentro del query.

Note

En este artículo analizaremos cómo usar CASE en SQL con condiciones múltiples, validaciones de existencia, protección contra división por cero, actualizaciones condicionales y manejo explícito de valores NULL.

La expresión CASE cumple un rol similar a if / else if / else o switch / case en lenguajes de programación. Su valor está en que opera dentro del motor de base de datos: puedes derivar columnas, etiquetar estados y condicionar cálculos sin postprocesar cada fila en tu backend.

Observemos un primer ejemplo directo. A partir de una columna active, convertimos 1 y 0 en etiquetas más legibles:

mysql> SELECT first_name, last_name,
    ->   CASE
    ->     WHEN active = 1 THEN 'ACTIVE'
    ->     ELSE 'INACTIVE'
    ->   END activity_type
    -> FROM customer;
+-------------+--------------+---------------+
| first_name  | last_name    | activity_type |
+-------------+--------------+---------------+
| MARY        | SMITH        | ACTIVE        |
| PATRICIA    | JOHNSON      | ACTIVE        |
| LINDA       | WILLIAMS     | ACTIVE        |
| BARBARA     | JONES        | ACTIVE        |
| ELIZABETH   | BROWN        | ACTIVE        |
| JENNIFER    | DAVIS        | ACTIVE        |
...
| KENT        | ARSENAULT    | ACTIVE        |
| TERRANCE    | ROUSH        | INACTIVE      |
| RENE        | MCALISTER    | ACTIVE        |
| EDUARDO     | HIATT        | ACTIVE        |
| TERRENCE    | GUNDERSON    | ACTIVE        |
| ENRIQUE     | FORSYTHE     | ACTIVE        |
| FREDDIE     | DUGGAN       | ACTIVE        |
| WADE        | DELVALLE     | ACTIVE        |
| AUSTIN      | CINTRON      | ACTIVE        |
+-------------+--------------+---------------+
599 rows in set (0.00 sec)

La expresión CASE

La expresión CASE evalúa una o varias condiciones y retorna un valor según la primera coincidencia encontrada. En SQL, CASE no es solo una herramienta cosmética; también permite modelar reglas de negocio dentro de consultas, reportes y operaciones UPDATE.

Existen dos variantes principales:

  • Searched CASE: cada WHEN contiene una condición completa.
  • Simple CASE: se evalúa una expresión base contra valores específicos.

Info

Si ninguna condición coincide y no defines ELSE, el resultado de CASE será NULL. Conviene declarar ELSE explícitamente cuando el consumidor del dato espera una categoría estable.

Searched CASE

El Searched CASE es la forma más flexible porque cada rama puede evaluar una condición distinta. Su estructura general es:

CASE
  WHEN C1 THEN E1
  WHEN C2 THEN E2
  ...
  WHEN CN THEN EN
  [ELSE ED]
END

En este patrón, C1, C2 y CN representan condiciones booleanas. Cuando una condición se cumple, CASE retorna su expresión asociada (E1, E2, EN) y deja de evaluar las siguientes ramas.

Analicemos una clasificación de categorías de películas:

CASE
  WHEN category.name IN ('Children','Family','Sports','Animation')
    THEN 'All Ages'
  WHEN category.name = 'Horror'
    THEN 'Adult'
  WHEN category.name IN ('Music','Games')
    THEN 'Teens'
  ELSE 'Other'
END

Esta variante permite mezclar operadores como IN, =, comparaciones numéricas, subconsultas y funciones. También se puede usar para evitar cálculos innecesarios en ciertas filas.

Consideremos el siguiente caso: si un cliente está inactivo, queremos retornar 0; si está activo, contamos sus rentas:

mysql> SELECT c.first_name, c.last_name,
    ->   CASE
    ->     WHEN active = 0 THEN 0
    ->     ELSE
    ->      (SELECT count(*) FROM rental r
    ->       WHERE r.customer_id = c.customer_id)
    ->   END num_rentals
    -> FROM customer c;
+-------------+--------------+-------------+
| first_name  | last_name    | num_rentals |
+-------------+--------------+-------------+
| MARY        | SMITH        |          32 |
| PATRICIA    | JOHNSON      |          27 |
| LINDA       | WILLIAMS     |          26 |
| BARBARA     | JONES        |          22 |
| ELIZABETH   | BROWN        |          38 |
| JENNIFER    | DAVIS        |          28 |
...
| TERRANCE    | ROUSH        |           0 |
| RENE        | MCALISTER    |          26 |
| EDUARDO     | HIATT        |          27 |
| TERRENCE    | GUNDERSON    |          30 |
| ENRIQUE     | FORSYTHE     |          28 |
| FREDDIE     | DUGGAN       |          25 |
| WADE        | DELVALLE     |          22 |
| AUSTIN      | CINTRON      |          19 |
+-------------+--------------+-------------+
599 rows in set (0.01 sec)

Simple CASE

El Simple CASE es útil cuando todas las ramas comparan la misma expresión contra valores concretos. En lugar de repetir category.name = ..., colocamos category.name una sola vez después de CASE.

CASE category.name
  WHEN 'Children' THEN 'All Ages'
  WHEN 'Family' THEN 'All Ages'
  WHEN 'Sports' THEN 'All Ages'
  WHEN 'Animation' THEN 'All Ages'
  WHEN 'Horror' THEN 'Adult'
  WHEN 'Music' THEN 'Teens'
  WHEN 'Games' THEN 'Teens'
  ELSE 'Other'
END

Esta forma es más compacta y legible cuando la decisión depende de una única columna o expresión. Su limitación es que no permite condiciones heterogéneas por rama; para eso conviene usar Searched CASE.

Tip

Usa Simple CASE cuando estás mapeando valores discretos de una misma columna. Usa Searched CASE cuando necesitas rangos, IN, EXISTS, comparaciones entre columnas o reglas más compuestas.

Comprobar la existencia

Uno de los usos más prácticos de CASE es combinarlo con EXISTS. Esta técnica permite crear columnas tipo bandera (Y / N, true / false, 1 / 0) según la existencia de filas relacionadas.

Observemos una consulta que marca si un actor participó en películas con ciertas clasificaciones:

mysql> SELECT a.first_name, a.last_name,
    ->   CASE
    ->     WHEN EXISTS (SELECT 1 FROM film_actor fa
    ->                    INNER JOIN film f ON fa.film_id = f.film_id
    ->                  WHERE fa.actor_id = a.actor_id
    ->                    AND f.rating = 'G') THEN 'Y'
    ->     ELSE 'N'
    ->   END g_actor,
    ->   CASE
    ->     WHEN EXISTS (SELECT 1 FROM film_actor fa
    ->                    INNER JOIN film f ON fa.film_id = f.film_id
    ->                  WHERE fa.actor_id = a.actor_id
    ->                    AND f.rating = 'PG') THEN 'Y'
    ->     ELSE 'N'
    ->   END pg_actor,
    ->   CASE
    ->     WHEN EXISTS (SELECT 1 FROM film_actor fa
    ->                    INNER JOIN film f ON fa.film_id = f.film_id
    ->                  WHERE fa.actor_id = a.actor_id
    ->                    AND f.rating = 'NC-17') THEN 'Y'
    ->     ELSE 'N'
    ->   END nc17_actor
    -> FROM actor a
    -> WHERE a.last_name LIKE 'S%' OR a.first_name LIKE 'S%';
+------------+-------------+---------+----------+------------+
| first_name | last_name   | g_actor | pg_actor | nc17_actor |
+------------+-------------+---------+----------+------------+
| JOE        | SWANK       | Y       | Y        | Y          |
| SANDRA     | KILMER      | Y       | Y        | Y          |
| CAMERON    | STREEP      | Y       | Y        | Y          |
| SANDRA     | PECK        | Y       | Y        | Y          |
| SISSY      | SOBIESKI    | Y       | Y        | N          |
| NICK       | STALLONE    | Y       | Y        | Y          |
| SEAN       | WILLIAMS    | Y       | Y        | Y          |
| GROUCHO    | SINATRA     | Y       | Y        | Y          |
| SCARLETT   | DAMON       | Y       | Y        | Y          |
| SPENCER    | PECK        | Y       | Y        | Y          |
| SEAN       | GUINESS     | Y       | Y        | Y          |
| SPENCER    | DEPP        | Y       | Y        | Y          |
| SUSAN      | DAVIS       | Y       | Y        | Y          |
| SIDNEY     | CROWE       | Y       | Y        | Y          |
| SYLVESTER  | DERN        | Y       | Y        | Y          |
| SUSAN      | DAVIS       | Y       | Y        | Y          |
| DAN        | STREEP      | Y       | Y        | Y          |
| SALMA      | NOLTE       | Y       | N        | Y          |
| SCARLETT   | BENING      | Y       | Y        | Y          |
| JEFF       | SILVERSTONE | Y       | Y        | Y          |
| JOHN       | SUVARI      | Y       | Y        | Y          |
| JAYNE      | SILVERSTONE | Y       | Y        | Y          |
+------------+-------------+---------+----------+------------+
22 rows in set (0.00 sec)

También podemos usar CASE con una subconsulta escalar. Aquí se clasifica la disponibilidad de una película según la cantidad de copias en inventario:

mysql> SELECT f.title,
    ->   CASE (SELECT count(*) FROM inventory i 
    ->         WHERE i.film_id = f.film_id)
    ->     WHEN 0 THEN 'Out Of Stock'
    ->     WHEN 1 THEN 'Scarce'
    ->     WHEN 2 THEN 'Scarce'
    ->     WHEN 3 THEN 'Available'
    ->     WHEN 4 THEN 'Available'
    ->     ELSE 'Common'
    ->   END film_availability
    -> FROM film f
    -> ;
+-----------------------------+-------------------+
| title                       | film_availability |
+-----------------------------+-------------------+
| ACADEMY DINOSAUR            | Common            |
| ACE GOLDFINGER              | Available         |
| ADAPTATION HOLES            | Available         |
| AFFAIR PREJUDICE            | Common            |
| AFRICAN EGG                 | Available         |
| AGENT TRUMAN                | Common            |
| AIRPLANE SIERRA             | Common            |
| AIRPORT POLLOCK             | Available         |
| ALABAMA DEVIL               | Common            |
| ALADDIN CALENDAR            | Common            |
| ALAMO VIDEOTAPE             | Common            |
| ALASKA PHANTOM              | Common            |
| ALI FOREVER                 | Available         |
| ALICE FANTASIA              | Out Of Stock      |
...
| YOUNG LANGUAGE              | Scarce            |
| YOUTH KICK                  | Scarce            |
| ZHIVAGO CORE                | Scarce            |
| ZOOLANDER FICTION           | Common            |
| ZORRO ARK                   | Common            |
+-----------------------------+-------------------+
1000 rows in set (0.01 sec)

Errores de división por cero

La división por cero no está definida matemáticamente y los motores de base de datos deben tratarla como un caso especial. En MySQL, por ejemplo, una división directa entre cero puede retornar NULL:

mysql> SELECT 100 / 0;
+---------+
| 100 / 0 |
+---------+
|    NULL |
+---------+
1 row in set (0.00 sec)

En reportes, un NULL inesperado puede contaminar cálculos posteriores o romper supuestos del consumidor. CASE permite controlar el denominador antes de ejecutar la división.

mysql> SELECT c.first_name, c.last_name,
    ->   sum(p.amount) tot_payment_amt,
    ->   count(p.amount) num_payments,
    ->   sum(p.amount) /
    ->     CASE WHEN count(p.amount) = 0 THEN 1
    ->       ELSE count(p.amount)
    ->     END avg_payment
    -> FROM customer c
    ->   LEFT OUTER JOIN payment p
    ->   ON c.customer_id = p.customer_id
    -> GROUP BY c.first_name, c.last_name;
+------------+------------+-----------------+--------------+-------------+
| first_name | last_name  | tot_payment_amt | num_payments | avg_payment |
+------------+------------+-----------------+--------------+-------------+
| MARY       | SMITH      |          118.68 |           32 |    3.708750 |
| PATRICIA   | JOHNSON    |          128.73 |           27 |    4.767778 |
| LINDA      | WILLIAMS   |          135.74 |           26 |    5.220769 |
| BARBARA    | JONES      |           81.78 |           22 |    3.717273 |
| ELIZABETH  | BROWN      |          144.62 |           38 |    3.805789 |
...
| EDUARDO    | HIATT      |          130.73 |           27 |    4.841852 |
| TERRENCE   | GUNDERSON  |          117.70 |           30 |    3.923333 |
| ENRIQUE    | FORSYTHE   |           96.72 |           28 |    3.454286 |
| FREDDIE    | DUGGAN     |           99.75 |           25 |    3.990000 |
| WADE       | DELVALLE   |           83.78 |           22 |    3.808182 |
| AUSTIN     | CINTRON    |           83.81 |           19 |    4.411053 |
+------------+------------+-----------------+--------------+-------------+
599 rows in set (0.07 sec)

Warning

Esta técnica evita el error o el NULL, pero no siempre significa que dividir entre 1 sea semánticamente correcto. En métricas financieras o analíticas, valida si conviene retornar 0, NULL controlado o una etiqueta explícita.

Actualizaciones condicionales

CASE también puede usarse dentro de UPDATE. Esto permite modificar registros según reglas calculadas al momento de la escritura.

El siguiente ejemplo desactiva clientes activos si su última renta ocurrió hace 90 días o más; en caso contrario, mantiene el registro activo:

UPDATE customer
SET active =
  CASE
    WHEN 90 <= (SELECT datediff(now(), max(rental_date))
                FROM rental r
                WHERE r.customer_id = customer.customer_id)
      THEN 0
    ELSE 1
  END
WHERE active = 1;

Esta aproximación concentra la regla en una sola sentencia SQL. Antes de ejecutar un UPDATE de este tipo en producción, conviene probar el mismo CASE en un SELECT, revisar cuántas filas cambiarán y ejecutar la operación dentro de una transacción si el motor lo permite.

Manejo de valores NULL

Los valores NULL aparecen con frecuencia cuando usamos LEFT OUTER JOIN, porque no siempre existe una fila relacionada. CASE nos permite reemplazarlos por un valor controlado para reportes o interfaces.

SELECT c.first_name, c.last_name,
  CASE
    WHEN a.address IS NULL THEN 'Unknown'
    ELSE a.address
  END address,
  CASE
    WHEN ct.city IS NULL THEN 'Unknown'
    ELSE ct.city
  END city,
  CASE
    WHEN cn.country IS NULL THEN 'Unknown'
    ELSE cn.country
  END country
FROM customer c
  LEFT OUTER JOIN address a
  ON c.address_id = a.address_id
  LEFT OUTER JOIN city ct
  ON a.city_id = ct.city_id
  LEFT OUTER JOIN country cn
  ON ct.country_id = cn.country_id;

En varios motores también existen funciones como COALESCE, útiles para este caso específico. Aun así, CASE sigue siendo preferible cuando la lógica depende de más de una condición o cuando el valor de salida no es simplemente el primer dato no nulo.

Cierre

La expresión CASE convierte SQL en una herramienta más expresiva para preparar datos cerca de donde viven. Con ella puedes clasificar filas, derivar columnas, controlar casos límite, ejecutar actualizaciones condicionales y normalizar valores faltantes.

Como regla práctica, usa CASE para reglas de presentación, clasificación y cálculo que pertenecen naturalmente al resultado de la consulta. Si la lógica empieza a depender de múltiples procesos externos o de reglas de dominio altamente cambiantes, evalúa moverla a una capa de aplicación o a una vista bien documentada.

En el siguiente artículo de la serie vamos a trabajar con agrupamiento y funciones agregadas, donde GROUP BY, COUNT, SUM y otras funciones nos permitirán resumir datos de manera consistente.