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; -- NULL3SELECT NULL != NULL AS resultado; -- NULL4SELECT NULL > 5 AS resultado; -- NULL5SELECT NULL + 100 AS resultado; -- NULL6SELECT 'Hola' || NULL AS resultado; -- NULL78-- En aritmética y comparaciones, NULL contamina el resultado9-- (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; -- → TRUE45-- IS NOT DISTINCT FROM: la comparación que SÍ funciona6SELECT NULL IS NOT DISTINCT FROM NULL; -- → TRUE7SELECT 'Madrid' IS NOT DISTINCT FROM NULL; -- → FALSE8SELECT '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 desconocidos2SELECT nombre, ciudad3FROM clientes4WHERE ciudad IS NULL;56-- ✅ CORRECTO: IS NOT NULL para encontrar valores conocidos7SELECT nombre, email8FROM clientes9WHERE email IS NOT NULL;1011-- ❌ INCORRECTO: = NULL nunca funciona12-- SELECT nombre FROM clientes WHERE ciudad = NULL; -- 0 filas siempre13-- 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 legible2SELECT3 nombre,4 COALESCE(ciudad, 'Sin asignar') AS ciudad,5 COALESCE(email, 'No proporcionado') AS email6FROM clientes7WHERE ciudad IS NULL OR email IS NULL;89-- COALESCE con múltiples opciones (usa el primero no-NULL)10SELECT11 nombre,12 COALESCE(ciudad, canal_adquisicion, 'Desconocido') AS ubicacion13FROM clientes14LIMIT 10;1516-- COALESCE en cálculos: evitar que NULL contamine17SELECT18 id,19 COALESCE(importe, 0) AS importe_seguro,20 COALESCE(importe, 0) * 1.21 AS con_iva21FROM pedidos22WHERE 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 a2SELECT NULLIF(5, 5) AS resultado; -- NULL3SELECT NULLIF(5, 3) AS resultado; -- 545-- Caso real: división segura (evitar dividir por cero)6SELECT7 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ón11 ROUND(100.0 * COUNT(CASE WHEN es_vip THEN 1 END)12 / NULLIF(COUNT(*), 0), 1) AS pct_vip13FROM clientes14GROUP 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 ciudad34-- COUNT(ciudad) IGNORA filas donde ciudad es NULL5SELECT COUNT(ciudad) AS con_ciudad FROM clientes; -- menos filas67-- La diferencia revela cuántos NULLs hay8SELECT9 COUNT(*) AS total_filas,10 COUNT(ciudad) AS con_ciudad,11 COUNT(*) - COUNT(ciudad) AS sin_ciudad12FROM clientes;1314-- AVG ignora NULLs: si hay 100 pedidos y 5 tienen importe NULL,15-- AVG calcula la media de los 95 que SÍ tienen valor16SELECT17 AVG(importe) AS avg_ignorando_nulls,18 SUM(importe) / COUNT(*) AS avg_contando_nulls_como_filas19FROM 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 BY2SELECT nombre, ciudad3FROM clientes4ORDER BY ciudad ASC5LIMIT 20;67-- Puedes forzar NULLs al principio8SELECT nombre, ciudad9FROM clientes10ORDER BY ciudad ASC NULLS FIRST11LIMIT 10;1213-- DISTINCT trata NULLs como un grupo14SELECT 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 coincidencia2SELECT3 c.nombre,4 c.ciudad,5 COALESCE(p.importe::VARCHAR, 'Sin pedidos') AS importe,6 CASE7 WHEN p.id IS NULL THEN 'Sin pedidos'8 WHEN p.importe IS NULL THEN 'Importe pendiente'9 ELSE 'OK'10 END AS estado_dato11FROM clientes c12LEFT JOIN pedidos p ON p.cliente_id = c.id13ORDER BY c.id DESC14LIMIT 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' ...34-- ✅ Usa IS NULL dentro del CASE5SELECT6 nombre,7 CASE8 WHEN ciudad IS NULL THEN 'Sin ciudad'9 WHEN es_vip IS NULL THEN 'Estado desconocido'10 WHEN es_vip = TRUE THEN 'VIP de ' || ciudad11 ELSE 'Regular de ' || ciudad12 END AS clasificacion13FROM clientes14WHERE 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
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.
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.
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.
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.
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.
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...