Saltar al contenido

lección 11

NULLs: la trampa invisible de SQL

Qué es NULL, por qué no es lo mismo que 0 o cadena vacía, y las trampas que ponen a los juniors.

50 min

NULL es el concepto más malentendido de SQL. No es cero. No es una cadena vacía. No es "falso". NULL significa "desconocido" — no tenemos información sobre ese valor. Puede ser que el cliente no puso su ciudad al registrarse, que el pedido no tiene importe porque aún no se procesó, o que el sensor no envió dato esa hora. El problema es que SQL trata NULL con reglas diferentes a todo lo demás, y si no las conoces, tus queries darán resultados incorrectos sin avisarte.

La analogía: pregunta a alguien "¿cuántos años tiene tu vecino del quinto?". Si no conoces a tu vecino, la respuesta no es 0 — es "no lo sé". ¿Es tu vecino mayor que 30? No "falso" — "no lo sé". ¿Tu vecino tiene la misma edad que mi vecino (que tampoco conozco)? No "sí" — "no lo sé, no puedo comparar dos cosas desconocidas". Eso es NULL.

### La trampa #1: NULL = NULL es NULL, no TRUE

Esta es la trampa fundamental. En cualquier lenguaje de programación, x == x es TRUE. En SQL, NULL = NULL no es TRUE — es NULL. Porque no puedes saber si dos cosas desconocidas son iguales:

1-- NULL = NULL NO es TRUE (es NULL)
2SELECT NULL = NULL AS resultado; -- NULL
3SELECT NULL != NULL AS resultado; -- NULL
4SELECT NULL > 5 AS resultado; -- NULL
5SELECT NULL + 100 AS resultado; -- NULL
6SELECT 'Hola' || NULL AS resultado; -- NULL
7
8-- En aritmética y comparaciones, NULL contamina el resultado
9-- (excepciones: operadores booleanos — ver tip abajo)

NULL es contagioso en aritmética y comparaciones — con excepciones en booleanos

WHERE ciudad = NULL NUNCA devuelve filas. NUNCA. Ni siquiera si hay filas con ciudad NULL. La comparación con = siempre devuelve NULL (que no es TRUE), así que ninguna fila pasa el filtro. Usa IS NULL.

¿Y si necesitas comparar dos columnas que pueden tener NULLs? El operador = falla: NULL = NULL da NULL, no TRUE. Para eso existe IS DISTINCT FROM (y su inversa IS NOT DISTINCT FROM), que trata NULLs como un valor más:

1-- = falla con NULLs:
2SELECT NULL = NULL; -- → NULL (no es TRUE)
3SELECT NULL IS NULL; -- → TRUE
4
5-- IS NOT DISTINCT FROM: la comparación que SÍ funciona
6SELECT NULL IS NOT DISTINCT FROM NULL; -- → TRUE
7SELECT 'Madrid' IS NOT DISTINCT FROM NULL; -- → FALSE
8SELECT 'Madrid' IS NOT DISTINCT FROM 'Madrid'; -- → TRUE

IS NOT DISTINCT FROM: como = pero que funciona con NULLs. Piensa en él como "¿son iguales, contando NULL como un valor?"

Excepción importante — operadores booleanos (AND, OR): SQL usa "short-circuit" lógico. Si un lado del OR ya es TRUE, el resultado es TRUE aunque el otro sea NULL. Si un lado del AND ya es FALSE, el resultado es FALSE aunque el otro sea NULL. Tabla de verdad trivaluada: TRUE OR NULL → TRUE | FALSE OR NULL → NULL | TRUE AND NULL → NULL | FALSE AND NULL → FALSE. Memoriza esto: OR solo necesita UN verdadero, AND solo necesita UN falso.

### IS NULL e IS NOT NULL: la forma correcta

Para verificar si un valor es NULL, SQL tiene operadores especiales: IS NULL e IS NOT NULL. Son los ÚNICOS que funcionan correctamente con NULLs:

1-- ✅ CORRECTO: IS NULL para encontrar valores desconocidos
2SELECT nombre, ciudad
3FROM clientes
4WHERE ciudad IS NULL;
5
6-- ✅ CORRECTO: IS NOT NULL para encontrar valores conocidos
7SELECT nombre, email
8FROM clientes
9WHERE email IS NOT NULL;
10
11-- ❌ INCORRECTO: = NULL nunca funciona
12-- SELECT nombre FROM clientes WHERE ciudad = NULL; -- 0 filas siempre
13-- SELECT nombre FROM clientes WHERE ciudad != NULL; -- 0 filas siempre

IS NULL / IS NOT NULL: los únicos operadores válidos para NULLs

### COALESCE: el valor por defecto

COALESCE toma una lista de valores y devuelve el primero que NO sea NULL. Es tu herramienta para "reemplazar NULLs por algo útil" en reportes y cálculos:

1-- Reemplazar NULL por un texto legible
2SELECT
3 nombre,
4 COALESCE(ciudad, 'Sin asignar') AS ciudad,
5 COALESCE(email, 'No proporcionado') AS email
6FROM clientes
7WHERE ciudad IS NULL OR email IS NULL;
8
9-- COALESCE con múltiples opciones (usa el primero no-NULL)
10SELECT
11 nombre,
12 COALESCE(ciudad, canal_adquisicion, 'Desconocido') AS ubicacion
13FROM clientes
14LIMIT 10;
15
16-- COALESCE en cálculos: evitar que NULL contamine
17SELECT
18 id,
19 COALESCE(importe, 0) AS importe_seguro,
20 COALESCE(importe, 0) * 1.21 AS con_iva
21FROM pedidos
22WHERE importe IS NULL;

COALESCE: primer valor no-NULL de la lista. Imprescindible en reportes.

### NULLIF: crear NULLs intencionalmente

NULLIF hace lo contrario de COALESCE: convierte un valor específico en NULL. Es particularmente útil para evitar divisiones por cero:

1-- NULLIF(a, b) devuelve NULL si a = b, sino devuelve a
2SELECT NULLIF(5, 5) AS resultado; -- NULL
3SELECT NULLIF(5, 3) AS resultado; -- 5
4
5-- Caso real: división segura (evitar dividir por cero)
6SELECT
7 ciudad,
8 COUNT(CASE WHEN es_vip THEN 1 END) AS vips,
9 COUNT(*) AS total,
10 -- Sin NULLIF: si total = 0, da error de división
11 ROUND(100.0 * COUNT(CASE WHEN es_vip THEN 1 END)
12 / NULLIF(COUNT(*), 0), 1) AS pct_vip
13FROM clientes
14GROUP BY ciudad;

NULLIF para divisiones seguras: si el denominador es 0, devuelve NULL

### NULLs en funciones de agregación

Las funciones de agregación (COUNT, SUM, AVG) ignoran NULLs silenciosamente. Esto puede sesgar tus resultados sin que te des cuenta:

1-- COUNT(*) cuenta TODAS las filas (incluidas las de NULL)
2SELECT COUNT(*) AS total FROM clientes; -- incluye clientes sin ciudad
3
4-- COUNT(ciudad) IGNORA filas donde ciudad es NULL
5SELECT COUNT(ciudad) AS con_ciudad FROM clientes; -- menos filas
6
7-- La diferencia revela cuántos NULLs hay
8SELECT
9 COUNT(*) AS total_filas,
10 COUNT(ciudad) AS con_ciudad,
11 COUNT(*) - COUNT(ciudad) AS sin_ciudad
12FROM clientes;
13
14-- AVG ignora NULLs: si hay 100 pedidos y 5 tienen importe NULL,
15-- AVG calcula la media de los 95 que SÍ tienen valor
16SELECT
17 AVG(importe) AS avg_ignorando_nulls,
18 SUM(importe) / COUNT(*) AS avg_contando_nulls_como_filas
19FROM pedidos;

Las agregaciones ignoran NULLs — puede sesgar resultados

### NULLs en ORDER BY y DISTINCT

Otro comportamiento sutil: en ORDER BY, los NULLs van al FINAL por defecto (en DuckDB). Y DISTINCT trata todos los NULLs como un solo valor (agrupa todos los NULLs juntos):

1-- NULLs al final en ORDER BY
2SELECT nombre, ciudad
3FROM clientes
4ORDER BY ciudad ASC
5LIMIT 20;
6
7-- Puedes forzar NULLs al principio
8SELECT nombre, ciudad
9FROM clientes
10ORDER BY ciudad ASC NULLS FIRST
11LIMIT 10;
12
13-- DISTINCT trata NULLs como un grupo
14SELECT DISTINCT ciudad FROM clientes ORDER BY ciudad;

NULLS FIRST / NULLS LAST controla dónde aparecen en ORDER BY

### NULLs en LEFT JOIN: distinguir "sin datos" de "no existe"

Cuando haces LEFT JOIN, las columnas de la tabla derecha vienen como NULL para filas sin coincidencia. Esto es útil pero también es una fuente de confusión: ¿el importe es NULL porque no existe pedido, o porque el pedido tiene importe NULL?

1-- LEFT JOIN: NULLs donde no hay coincidencia
2SELECT
3 c.nombre,
4 c.ciudad,
5 COALESCE(p.importe::VARCHAR, 'Sin pedidos') AS importe,
6 CASE
7 WHEN p.id IS NULL THEN 'Sin pedidos'
8 WHEN p.importe IS NULL THEN 'Importe pendiente'
9 ELSE 'OK'
10 END AS estado_dato
11FROM clientes c
12LEFT JOIN pedidos p ON p.cliente_id = c.id
13ORDER BY c.id DESC
14LIMIT 20;

Distinguir NULLs del LEFT JOIN de NULLs reales en los datos

Consejo de senior: cuando depures un resultado inesperado, lo primero que reviso es si hay NULLs escondidos. Añado COUNT(*) vs COUNT(columna) para cada columna sospechosa. La diferencia te dice cuántos NULLs hay. 9 de cada 10 bugs en reportes financieros que he visto eran por NULLs no contemplados.

### CASE WHEN con NULL: otra trampa

1-- ❌ CASE WHEN ciudad = NULL no funciona (siempre va al ELSE)
2-- CASE WHEN ciudad = NULL THEN 'Sin ciudad' ...
3
4-- ✅ Usa IS NULL dentro del CASE
5SELECT
6 nombre,
7 CASE
8 WHEN ciudad IS NULL THEN 'Sin ciudad'
9 WHEN es_vip IS NULL THEN 'Estado desconocido'
10 WHEN es_vip = TRUE THEN 'VIP de ' || ciudad
11 ELSE 'Regular de ' || ciudad
12 END AS clasificacion
13FROM clientes
14WHERE ciudad IS NULL OR es_vip IS NULL OR es_vip = TRUE;

En CASE WHEN, usa IS NULL — no = NULL

Consejo de senior sobre velocidad: si escribes WHERE YEAR(fecha) = 2025, la base de datos tiene que mirar TODAS las filas porque no puede usar un índice (le estás pidiendo que calcule algo antes de comparar). La alternativa rápida: WHERE fecha >= '2025-01-01' AND fecha < '2026-01-01' — ahora sí puede ir directo al rango con el índice. Misma regla para UPPER(), COALESCE() o cualquier función en el WHERE: deja la columna "limpia" a un lado del = para que el índice funcione.

## ejercicios

[01]

Encontrar registros incompletos

Genera un informe de calidad de datos: muestra los clientes que tienen ciudad o email como NULL. Para cada uno, muestra nombre, y usa COALESCE para mostrar "Desconocida" en lugar de NULL en ciudad y "No proporcionado" en email.

Cargando editor...
[02]

COALESCE con LEFT JOIN

Muestra TODOS los clientes (los 10 últimos por id) con su total de pedidos y gasto. Para clientes sin pedidos, muestra 0 pedidos y 0 gasto en vez de NULL. Usa LEFT JOIN + COALESCE.

Cargando editor...
[03]

División segura con NULLIF

Calcula el porcentaje de pedidos completados vs total por ciudad. Usa NULLIF para evitar división por cero en caso de ciudades sin pedidos. Muestra ciudad, completados, total y porcentaje.

Cargando editor...
[04]

Clasificar clientes con NULLs

Clasifica los clientes que tienen algún dato incompleto o son VIP usando CASE WHEN: si ciudad IS NULL → "Ubicación desconocida", si es_vip IS NULL → "Estado pendiente", si es_vip = TRUE → "VIP", else → "Regular". Muestra nombre y clasificación.

Cargando editor...
[05]

Auditoría de calidad de datos

Genera un informe de calidad para la tabla clientes: para cada columna (nombre, email, ciudad, canal_adquisicion, es_vip), cuenta cuántas filas son NULL y el porcentaje sobre el total. Muestra una fila por columna.

Cargando editor...

Regístrate para guardar tu progreso.

## comentarios

Reporta erratas, ayuda a otros o comparte tu opinión. Sé constructivo.

Inicia sesión para comentar y responder.

cargando comentarios...