El síntoma: «distinto de Madrid» devuelve menos filas de las esperadas
Imagina que ciudad puede contener un nombre o NULL. Quieres excluir Madrid y escribes WHERE ciudad <> 'Madrid'. Las ciudades conocidas y diferentes sí pasan el filtro, pero las filas con ciudad desconocida no aparecen.
La causa es la lógica de tres valores de SQL. Una comparación como NULL <> 'Madrid' no es verdadera ni falsa: produce un resultado desconocido. WHERE conserva únicamente las filas cuya condición es verdadera, así que también descarta ese caso.
Ejemplo reproducible: <> excluye Madrid y también los NULL
WITH clientes(id, nombre, ciudad) AS (
VALUES
(1, 'Ana', 'Madrid'),
(2, 'Luis', 'Valencia'),
(3, 'Marta', NULL),
(4, 'Pablo', 'Sevilla'),
(5, 'Sara', NULL)
)
SELECT id, nombre, ciudad
FROM clientes
WHERE ciudad <> 'Madrid'
ORDER BY id;| id | nombre | ciudad |
|---|---|---|
| 2 | Luis | Valencia |
| 4 | Pablo | Sevilla |
Marta y Sara no aparecen porque su ciudad es desconocida. SQL no puede afirmar que una ciudad desconocida sea distinta de Madrid; por eso la condición no llega a TRUE.
La pregunta importante no es «¿cómo fuerzo NULL?», sino «¿qué significa mi regla?»
Antes de corregir el SQL, define la intención. Hay al menos dos reglas válidas y producen resultados distintos:
| Intención | Tratamiento de NULL | Predicado portable |
|---|---|---|
| Quiero solo ciudades conocidas que no sean Madrid | NULL queda fuera | ciudad IS NOT NULL AND ciudad <> 'Madrid' |
| Quiero todas las filas salvo las confirmadas como Madrid | NULL entra | ciudad <> 'Madrid' OR ciudad IS NULL |
La primera regla hace explícito que una ciudad desconocida no es suficiente para clasificar al cliente como «fuera de Madrid». La segunda expresa otra política: solo excluye las filas que sabemos que son Madrid y conserva las que todavía no tienen ciudad.
Corrección portable cuando quieres conservar los valores desconocidos
WITH clientes(id, nombre, ciudad) AS (
VALUES
(1, 'Ana', 'Madrid'),
(2, 'Luis', 'Valencia'),
(3, 'Marta', NULL),
(4, 'Pablo', 'Sevilla'),
(5, 'Sara', NULL)
)
SELECT id, nombre, ciudad
FROM clientes
WHERE ciudad <> 'Madrid'
OR ciudad IS NULL
ORDER BY id;| id | nombre | ciudad |
|---|---|---|
| 2 | Luis | Valencia |
| 3 | Marta | NULL |
| 4 | Pablo | Sevilla |
| 5 | Sara | NULL |
Esta forma es portable y deja la política visible: «ciudad distinta de Madrid o ciudad desconocida». No uses COALESCE(ciudad, '...') con un valor inventado salvo que ese valor tenga una semántica real y no pueda colisionar con datos válidos.
Predicados null-safe: útiles, pero no tienen la misma sintaxis en todos los motores
Algunos motores ofrecen comparaciones que siempre devuelven verdadero o falso aunque intervenga NULL. PostgreSQL y SQLite admiten IS DISTINCT FROM; SQL Server lo admite en versiones modernas, y MySQL utiliza el operador null-safe <=> para igualdad. Si tu objetivo es portabilidad, <> ... OR ... IS NULL suele ser más explícito entre motores.
| Motor | Regla relevante | Alternativa null-safe |
|---|---|---|
| PostgreSQL | Las comparaciones ordinarias con NULL producen resultado desconocido. | ciudad IS DISTINCT FROM 'Madrid' |
| MySQL 8.4 | =, <> y otras comparaciones ordinarias con NULL producen NULL. | NOT (ciudad <=> 'Madrid') |
| SQL Server | Con el comportamiento ANSI vigente, una comparación con NULL produce UNKNOWN. | IS DISTINCT FROM en versiones que lo soportan; si buscas portabilidad, usa la condición explícita. |
| SQLite | Los operadores ordinarios suelen devolver NULL cuando un operando es NULL. | IS DISTINCT FROM; también tiene semántica propia para IS/IS NOT. |
Método de diagnóstico
- Comprueba si la columna admite NULL. Si nunca puede ser nula, este problema no explica las filas ausentes.
- Ejecuta el filtro positivo. Prueba
ciudad = 'Madrid'para separar coincidencias conocidas de valores desconocidos. - Cuenta o localiza los NULL. Usa
WHERE ciudad IS NULLy verifica cuántas filas pertenecen a ese estado. - Define la regla del negocio. Decide si «no es Madrid» significa «sé que es otra ciudad» o «no está confirmado como Madrid».
- Escribe esa regla de forma explícita. No añadas
OR ... IS NULLpor costumbre: añádelo solo cuando los desconocidos deban entrar.
Error frecuente: asumir que NOT convierte UNKNOWN en TRUE
Negar una comparación desconocida sigue dejando un resultado desconocido
NOT (ciudad = 'Madrid') no recupera automáticamente los NULL. Cuando ciudad es NULL, ciudad = 'Madrid' es desconocido y su negación también lo es. Si quieres incluir esos registros, expresa el caso con OR ciudad IS NULL o usa un predicado null-safe apropiado para tu motor.
Práctica correctiva
Excluye solo los pedidos confirmados como cancelados
La columna estado puede ser 'enviado', 'cancelado', 'pendiente' o NULL. Devuelve todos los pedidos salvo los que estén confirmados como cancelados. Los estados desconocidos deben conservarse.
estado <> 'cancelado' no conserva los NULL por sí solo.estado IS NULL mediante OR.Solución razonada
WITH pedidos(id, estado) AS (
VALUES
(1, 'enviado'),
(2, 'cancelado'),
(3, NULL),
(4, 'pendiente'),
(5, NULL)
)
SELECT id, estado
FROM pedidos
WHERE estado <> 'cancelado'
OR estado IS NULL
ORDER BY id;La comprobación local devuelve los pedidos 1, 3, 4 y 5. El pedido 2 queda fuera porque está confirmado como cancelado; los pedidos 3 y 5 permanecen porque la regla dice explícitamente que un estado desconocido no debe excluirse.
Qué debes recordar
columna <> valorno incluye automáticamente las filas dondecolumnaes NULL.- Una comparación ordinaria con NULL suele producir UNKNOWN, y
WHEREsolo conserva TRUE. - Decide primero si un valor desconocido debe entrar o salir según la regla del dato.
- Para conservarlo de forma portable, expresa la condición:
columna <> valor OR columna IS NULL. - Los predicados null-safe cambian entre motores; no los presentes como sintaxis universal.
Conceptos relacionados
Fuentes técnicas
El comportamiento de comparaciones, lógica de tres valores y alternativas null-safe se contrastó con documentación primaria: