1. Qué es
NULL no es cero. No es cadena vacía. No es "falso". NULL significa desconocido.
Y de ahí sale todo lo demás: si un valor es desconocido, cualquier comparación con él es también desconocida. No verdadera, no falsa: desconocida. SQL tiene tres valores lógicos, no dos:
TRUE · FALSE · UNKNOWN
WHERE solo deja pasar TRUE. FALSE y UNKNOWN se descartan igual — y esa es la fuente de casi todos los resultados silenciosamente incompletos que verás en tu vida.
En Northwind hay NULL por todas partes y significan cosas distintas:
| Columna | NULL significa |
|---|---|
customers.region |
El país no usa regiones (60 de 91 clientes) |
orders.shipped_date |
Todavía no se ha enviado (21 pedidos) |
employees.reports_to |
No tiene jefe — es el director |
Fíjate: en un caso es "no aplica", en otro "aún no", y en otro "no existe". SQL usa el mismo NULL para los tres.* Interpretarlo es tu trabajo, no del motor.
2. Sintaxis
columna IS NULL
columna IS NOT NULL
columna IS DISTINCT FROM valor -- comparación que sí trata NULL como un valor
columna IS NOT DISTINCT FROM valor
coalesce(a, b, c) -- el primero que no sea NULL
nullif(a, b) -- NULL si a = b; si no, a
= NULL no da error y nunca es cierto. Ese es el problema: no avisa.
3. Ejemplos sobre Northwind
La demostración de una línea:
SELECT NULL = NULL AS igual, NULL <> NULL AS distinto, NULL IS NULL AS es_nulo;
igual | distinto | es_nulo
-------+----------+---------
| | t
Las dos primeras columnas salen vacías: no son t ni f, son NULL. "¿Es un desconocido igual a otro desconocido?" — no se puede saber. La tercera, IS NULL, sí responde: IS NULL es el único operador que pregunta por la ausencia sin caer en la trampa.
El mismo error, en la práctica:
SELECT count(*) FROM customers WHERE region = NULL; -- 0
SELECT count(*) FROM customers WHERE region IS NULL; -- 60
La primera no da error. Devuelve cero y parece una respuesta.
Contar nulos:
SELECT count(*) AS filas,
count(region) AS con_region,
count(*) - count(region) AS nulos
FROM customers;
filas | con_region | nulos
-------+------------+-------
91 | 31 | 60
count(*) cuenta filas. count(columna) cuenta valores no nulos. Restarlos es la forma más corta de auditar una columna, y conviene hacerlo con toda columna nueva antes de fiarse de ella.
La aritmética y el texto se contagian
SELECT 'Lima-' || NULL AS concatenado, 10 + NULL AS suma;
concatenado | suma
-------------+------
|
Ambas dan NULL. Un solo NULL en una expresión la vuelve NULL entera. Si construyes una dirección concatenando calle, ciudad y región, y la región es NULL, te quedas sin dirección, no sin región.
Pero las agregaciones lo ignoran
SELECT count(*) AS filas, count(x) AS no_nulos, sum(x) AS suma, avg(x) AS media
FROM (VALUES (10), (20), (NULL)) t(x);
filas | no_nulos | suma | media
-------+----------+------+---------------------
3 | 2 | 30 | 15.0000000000000000
La media es 15, no 10. avg sumó 30 y dividió entre 2, no entre 3: los NULL no entran en el denominador. Casi siempre es lo que quieres. Casi. Si esos nulos significaban "cero ventas", tu media está inflada y nadie te avisará.
La tabla de verdad
SELECT true AND NULL AS a, false AND NULL AS b,
true OR NULL AS c, false OR NULL AS d,
NOT NULL AS e;
a | b | c | d | e
---+---+---+---+---
| f | t | |
| Expresión | Resultado | Por qué |
|---|---|---|
TRUE AND NULL |
NULL |
Depende de lo que no sé |
FALSE AND NULL |
FALSE |
Ya es falso, da igual lo demás |
TRUE OR NULL |
TRUE |
Ya es cierto, da igual lo demás |
FALSE OR NULL |
NULL |
Depende de lo que no sé |
NOT NULL |
NULL |
Lo contrario de un desconocido sigue siendo desconocido |
Las dos filas en negrita son las únicas donde el NULL no contagia: cuando el otro operando ya decide el resultado por sí solo.
El complemento que no suma
SELECT count(*) FROM orders WHERE shipped_date > required_date; -- 37
SELECT count(*) FROM orders WHERE NOT (shipped_date > required_date); -- 772
37 + 772 = 809. La tabla tiene 830 pedidos. Faltan 21: exactamente los que tienen shipped_date en NULL.
Y esos 21 son los peores posibles: son los que no se han enviado todavía. Si te piden "los pedidos que no llegaron tarde" y escribes el NOT, dejas fuera precisamente los que están sin entregar. La respuesta correcta depende de la pregunta real, y hay que decidirla a mano:
-- "no llegó tarde" incluyendo los aún no enviados como no-tarde
WHERE shipped_date <= required_date OR shipped_date IS NULL
La trampa grande: NOT IN con NULL
Empleados que no son jefe de nadie. La consulta natural:
SELECT employee_id, first_name
FROM employees
WHERE employee_id NOT IN (SELECT reports_to FROM employees);
employee_id | first_name
-------------+------------
(0 filas)
Cero filas. Y es falso: hay siete empleados que no dirigen a nadie.
El motivo: reports_to contiene un NULL (Andrew, el director, no reporta a nadie). Así que NOT IN se convierte en:
employee_id <> 2 AND employee_id <> 5 AND employee_id <> NULL
Y esa última comparación es UNKNOWN para todas las filas. TRUE AND UNKNOWN = UNKNOWN → no pasa el WHERE → ninguna fila sobrevive, pase lo que pase.
Con el NULL fuera de la lista:
SELECT employee_id, first_name
FROM employees
WHERE employee_id NOT IN (SELECT reports_to FROM employees WHERE reports_to IS NOT NULL);
employee_id | first_name
-------------+------------
1 | Nancy
3 | Janet
4 | Margaret
6 | Michael
7 | Robert
8 | Laura
9 | Anne
Siete. La forma inmune a este problema es NOT EXISTS, y está en anti-join-y-semi-join.
Regla:
NOT INsobre una subconsulta es una bomba de relojería. Si la columna admiteNULL, algún día devolverá cero filas. UsaNOT EXISTS.
Sustituir el nulo: coalesce y nullif
SELECT company_name, region, coalesce(region, 'SIN REGION') AS region_limpia
FROM customers
LIMIT 4;
company_name | region | region_limpia
------------------------------------+--------+---------------
Alfreds Futterkiste | | SIN REGION
Ana Trujillo Emparedados y helados | | SIN REGION
coalesce devuelve el primer argumento no nulo, y acepta los que quieras: coalesce(movil, fijo, 'sin teléfono').
nullif es el inverso — convierte un valor en NULL:
SELECT nullif(10, 10) AS a, nullif(10, 5) AS b;
a | b
---+----
| 10
Su uso real es evitar la división por cero: total / nullif(cantidad, 0) devuelve NULL en vez de reventar.
IS DISTINCT FROM — comparar tratando NULL como valor
SELECT NULL IS NOT DISTINCT FROM NULL AS iguales,
'A' IS DISTINCT FROM NULL AS distintos;
iguales | distintos
---------+-----------
t | t
Devuelve siempre TRUE o FALSE, nunca UNKNOWN. Es lo que quieres cuando comparas dos versiones de un dato y "los dos están vacíos" debe contar como iguales.
Y al ordenar
En PostgreSQL, ORDER BY pone los NULL al final en orden ascendente y al principio en descendente. Se controla explícitamente:
SELECT company_name, region FROM customers ORDER BY region NULLS FIRST;
Más en ordenar-y-limitar.
4. Errores comunes
= NULLo<> NULL. Nunca es cierto. No da error. UsaIS NULL/IS NOT NULL.- Creer que
WHERE x <> 'A'devuelve "todo lo que no es A". Devuelve todo lo que se sabe que no es A. Los nulos se quedan fuera. NOT INcon una subconsulta que puede traerNULL. Cero filas silenciosas.- Concatenar sin
coalesce. UnNULLen medio anula la cadena entera. - Confundir
NULLcon cadena vacía.''es un texto de longitud cero: existe.'' IS NULLes falso. En Oracle no —es la excepción del mundo— pero en PostgreSQL y SQL Server son cosas distintas. - Interpretar todos los
NULLigual. "No aplica", "aún no" y "no lo sé" se escriben igual y significan cosas opuestas. Pregunta antes de rellenarlos.
5. Diferencias con SQL Server
| PostgreSQL | SQL Server | |
|---|---|---|
| Primer no nulo | coalesce(a,b) |
COALESCE(a,b) o ISNULL(a,b) |
| Comparar con nulos | IS DISTINCT FROM |
No existe hasta SQL Server 2022; antes se emula con EXISTS(SELECT a INTERSECT SELECT b) |
| Orden de los nulos | Al final en ASC, configurable con NULLS FIRST/LAST |
Al principio en ASC, y no se puede cambiar directamente |
SET ANSI_NULLS OFF |
No existe | Existe (obsoleto): hace que = NULL funcione. No lo uses |
La tercera fila muerde: la misma consulta con ORDER BY da un orden distinto en cada motor. Si el orden importa, escríbelo.
6. Ejercicios
- Cuántos pedidos están sin enviar. Y cuántos clientes no tienen fax.
- Lista los clientes con su región, sustituyendo el nulo por el texto
n/a. - Corrige esta consulta, que pretende dar los productos que no son de la categoría 1:
sql SELECT product_name FROM products WHERE category_id <> 1;¿Cuándo daría un resultado incompleto y cuándo no? - Escribe una consulta que devuelva, por cada columna de
customers, cuántos nulos tiene. Hazlo pararegion,faxypostal_codeen una sola fila. - Sin ejecutarla, di qué devuelve:
sql SELECT count(*) FROM orders WHERE shipped_date IS NULL OR shipped_date IS NOT NULL;
7. Pregunta de negocio
Logística quiere el porcentaje de pedidos entregados con retraso. Te dan la definición: "retraso es cuando la fecha de envío es posterior a la fecha requerida".
Da el número. Y da también la frase que tienes que decir al entregarlo.