Esta es la lección de la deuda. Desde 01-03 vienes tropezándote con el mismo fenómeno con distintos disfraces: empleado_id = NULL devolvió cero filas cuando había diez; empleado_id <> 4 devolvió seis en lugar de dieciséis; los diez pedidos web desaparecieron de un INNER JOIN; una condición sobre la tabla derecha en el WHERE degradó un LEFT JOIN a INNER; y ayer mismo, NOT IN (4, 5, NULL) devolvió exactamente nada. Cada vez te prometimos que "se explica en 04-03".

Aquí está. Todos esos casos son la misma cosa, y esa cosa se llama lógica de tres valores. Al terminar esta lección no habrás aprendido cinco reglas nuevas: habrás aprendido una sola, y todos esos síntomas dejarán de sorprenderte porque los podrás predecir. También verás la parte que casi nadie explica: que el estándar SQL trata los NULL de una forma en el WHERE y de otra distinta en DISTINCT, GROUP BY y ORDER BY, y que conocer esa incoherencia es lo que separa a quien sufre los nulos de quien los usa.

Contenido

  1. Qué es NULL y qué no es
  2. Los tres NULL de TiendaVerde y qué significan
  3. La lógica de tres valores: TRUE, FALSE y UNKNOWN
  4. Las tablas de verdad completas
  5. IS NULL e IS NOT NULL
  6. IS DISTINCT FROM e IS NOT DISTINCT FROM
  7. Dónde NULL sí se agrupa con NULL
  8. NULL y la restricción UNIQUE
  9. NULL en concatenaciones y en aritmética
  10. La resolución de los casos pendientes
  11. Diseñar con NULL, con centinelas o con NOT NULL
  12. Errores Comunes y Consejos
  13. Ejercicios
  14. Conclusión

  1. Qué es NULL y qué no es

NULL es la ausencia de valor. No es un valor especial: es la marca de que en esa celda no hay ninguno.

La confusión más extendida consiste en tratarlo como si fuera un valor concreto. No lo es, y la diferencia es visible en cuanto comparas:

NULL no es… Por qué importa
Cero 0 es un número: significa "ninguna unidad". NULL significa "no sé cuántas unidades". 0 + 5 = 5; NULL + 5 = NULL
Cadena vacía '' '' es una cadena de longitud 0, un valor real. LENGTH('') es 0; LENGTH(NULL) es NULL
FALSE FALSE es una respuesta. NULL es la falta de respuesta
Otro NULL Dos ausencias no son iguales: no sabes qué había en ninguna de las dos

Ese último punto es el corazón de todo. Si en productos.coste hay dos filas con NULL, ¿son iguales sus costes? No lo sabes. Podrían ser 3,20 € y 47 €. SQL, con criterio, se niega a afirmar que son iguales; y también se niega a afirmar que son distintos.

La frase que resume la lección: NULL no significa "vacío", significa "desconocido". Y todo lo que comparas con algo desconocido produce un resultado desconocido.

  1. Los tres NULL de TiendaVerde y qué significan

En un esquema bien diseñado, cada columna que admite nulos lo hace por una razón de negocio concreta. TiendaVerde tiene tres, y las tres significan cosas distintas:

Columna Filas con NULL Qué significa en el negocio
pedidos.empleado_id 10 de 20 Pedido entrado por la web, sin comercial asignado. No es que se haya perdido el dato: es que no existe
clientes.referido_por_id 7 de 15 Cliente que llegó por su cuenta, sin programa de referidos
empleados.jefe_id 1 de 8 La dirección general: Rosa Alcázar Vives no tiene jefe porque está arriba del organigrama

Vamos a verlos.

SELECT id,
       cliente_id,
       fecha_pedido,
       estado,
       empleado_id
FROM pedidos
WHERE empleado_id IS NULL
ORDER BY id;
id cliente_id fecha_pedido estado empleado_id
1 1 2025-03-04 entregado (null)
3 3 2025-04-02 entregado (null)
5 1 2025-05-07 entregado (null)
7 6 2025-06-11 entregado (null)
9 8 2025-07-15 entregado (null)
11 2 2025-09-09 entregado (null)
13 11 2025-10-22 entregado (null)
15 1 2025-12-02 entregado (null)
17 7 2026-01-13 enviado (null)
19 6 2026-02-09 pagado (null)

10 filas, el 50 % del canal de venta.

SELECT id,
       nombre || ' ' || apellidos AS cliente,
       fecha_registro,
       referido_por_id
FROM clientes
WHERE referido_por_id IS NULL
ORDER BY id;
id cliente fecha_registro referido_por_id
1 Lucía Martínez Soler 2025-01-10 (null)
4 Javier Ortega Ruiz 2025-02-14 (null)
6 Pau Llorens Vidal 2025-03-09 (null)
7 Sofia Moreira Costa 2025-03-21 (null)
9 Camille Dubois 2025-04-18 (null)
12 Diego Ramos Herrera 2025-06-01 (null)
14 Hugo Iglesias Pardo 2025-09-12 (null)

7 filas. Los otros 8 clientes llegaron recomendados.

SELECT id,
       nombre || ' ' || apellidos AS empleado,
       puesto,
       jefe_id
FROM empleados
WHERE jefe_id IS NULL;
id empleado puesto jefe_id
1 Rosa Alcázar Vives Directora general (null)

1 fila. Este NULL no es un dato ausente ni un dato desconocido: es una afirmación estructural ("aquí termina la jerarquía"), y por eso el SELF JOIN de 03-06 necesitaba un LEFT JOIN para no perder a Rosa.

Y todavía hay un cuarto tipo de NULL que ya has visto y que no está en ninguna tabla: los que fabrica el motor al construir un LEFT JOIN (03-03, sección 3). Esos no estaban en los datos; aparecen porque no hubo pareja.

Origen del NULL Ejemplo ¿Está en disco?
Dato que no existe (regla de negocio) empleado_id de un pedido web
Dato desconocido Un coste que el proveedor no ha comunicado
Frontera estructural jefe_id de la dirección general
Generado por un LEFT JOIN sin pareja pe.estado de Núria Bosch No, lo crea la consulta
Resultado de una operación con nulos precio * NULL No, lo crea la expresión

  1. La lógica de tres valores: TRUE, FALSE y UNKNOWN

Fuera de SQL, una condición es verdadera o falsa. En SQL hay tres resultados posibles, porque una comparación con NULL no puede decidirse:

5 = 5        → TRUE
5 = 4        → FALSE
5 = NULL     → UNKNOWN
NULL = NULL  → UNKNOWN
NULL <> NULL → UNKNOWN

UNKNOWN no es un tercer estado exótico: es exactamente lo que dice la palabra. "¿El empleado del pedido 1 es el número 4?" — no lo sabemos, porque no hay empleado registrado.

Y ahora la regla que lo gobierna todo, ya enunciada en 02-03 y que conviene repetir textualmente:

WHERE conserva únicamente las filas cuya condición se evalúa a TRUE. Las que dan FALSE se descartan, y las que dan UNKNOWN también.

Lo mismo vale para el ON de un JOIN y para el HAVING que verás en 04-06. Un solo criterio, tres cláusulas.

flowchart TD
    A["Condición evaluada<br/>sobre una fila"] --> B{"¿Resultado?"}
    B -->|"TRUE"| C["✅ La fila pasa"]
    B -->|"FALSE"| D["❌ La fila se descarta"]
    B -->|"UNKNOWN"| E["❌ La fila se descarta<br/>(igual que FALSE)"]

Ahí está la clave de por qué FALSE y UNKNOWN parecen lo mismo mirando el resultado de una sola consulta: ambos descartan la fila. La diferencia solo se hace visible cuando niegas la condición, porque NOT FALSE es TRUE pero NOT UNKNOWN sigue siendo UNKNOWN.

  1. Las tablas de verdad completas

AND

AND TRUE FALSE UNKNOWN
TRUE TRUE FALSE UNKNOWN
FALSE FALSE FALSE FALSE
UNKNOWN UNKNOWN FALSE UNKNOWN

Las dos celdas destacadas son las importantes: FALSE AND UNKNOWN es FALSE, no UNKNOWN. Si uno de los factores ya es falso, el conjunto es falso aunque el otro sea desconocido. Da igual lo que no sepas: la conjunción ya está decidida.

OR

OR TRUE FALSE UNKNOWN
TRUE TRUE TRUE TRUE
FALSE TRUE FALSE UNKNOWN
UNKNOWN TRUE UNKNOWN UNKNOWN

Simétricamente: TRUE OR UNKNOWN es TRUE. Si una alternativa ya es cierta, la disyunción es cierta.

Estas dos celdas explican por sí solas la asimetría de 04-02:

Operador Se expande a Celda que decide Resultado con un NULL en la lista
IN OR encadenados TRUE OR UNKNOWN = TRUE Funciona con normalidad
NOT IN AND encadenados TRUE AND UNKNOWN = UNKNOWN Cero filas siempre

NOT

Entrada NOT entrada
TRUE FALSE
FALSE TRUE
UNKNOWN UNKNOWN

Negar lo desconocido sigue siendo desconocido. De aquí sale la observación práctica más útil de toda la lección:

Una condición y su negación no cubren todas las filas. Si estado = 'entregado' da 14 filas y estado <> 'entregado' da 6, y la tabla tiene 20, la lógica cierra porque estado es NOT NULL. Con empleado_id = 4 (4 filas) y empleado_id <> 4 (6 filas), 4 + 6 = 10 ≠ 20: las 10 que faltan son las nulas. Esa suma es tu mejor detector de nulos.

Y una advertencia sobre el orden de evaluación

Las tablas de verdad describen el significado, no el orden en que PostgreSQL evalúa las condiciones. El motor puede reordenar los operandos de un AND libremente si eso abarata el plan. Por eso no puedes usar un AND como protección:

-- ⚠️ NO garantiza que la división nunca se ejecute con cero
WHERE stock <> 0 AND (100 / stock) > 2

PostgreSQL podría evaluar la división primero. La forma segura es CASE o NULLIF, ambos del módulo 6.

  1. IS NULL e IS NOT NULL

Como = NULL nunca es TRUE, SQL ofrece un predicado específico. IS NULL no es una comparación: es una pregunta sobre el estado de la celda, y por eso devuelve siempre TRUE o FALSE, nunca UNKNOWN.

SELECT NULL = NULL      AS igualdad,
       NULL IS NULL     AS es_nulo,
       NULL IS NOT NULL AS no_es_nulo;
igualdad es_nulo no_es_nulo
(null) true false

La primera columna es NULL (es decir, UNKNOWN); las otras dos son booleanos de verdad. Esa es toda la diferencia, y explica por qué WHERE empleado_id IS NULL funciona y WHERE empleado_id = NULL no.

Escritura Tipo de resultado ¿Devuelve filas?
columna = NULL UNKNOWN siempre Nunca
columna <> NULL UNKNOWN siempre Nunca
columna IS NULL TRUE / FALSE Sí, las nulas
columna IS NOT NULL TRUE / FALSE Sí, las no nulas

Y la pareja IS NULL / IS NOT NULL parte la tabla en dos mitades exactas:

SELECT COUNT(*)                                       AS total,
       COUNT(*) FILTER (WHERE empleado_id IS NULL)     AS sin_comercial,
       COUNT(*) FILTER (WHERE empleado_id IS NOT NULL) AS con_comercial
FROM pedidos;
total sin_comercial con_comercial
20 10 10

10 + 10 = 20. Cierra. (FILTER y COUNT son el tema de la lección siguiente; aquí solo hacen de instrumento de medida.)

Nota de dialecto: algunos motores permiten configurar que = NULL se comporte como IS NULL —SQL Server lo hace con SET ANSI_NULLS OFF, hoy obsoleto—. No lo actives nunca. Convierte tu SQL en algo que solo funciona en tu servidor y rompe la lógica que acabas de aprender.

  1. IS DISTINCT FROM e IS NOT DISTINCT FROM

A veces sí quieres comparar tratando NULL como "un valor más": que dos nulos se consideren iguales y que un nulo se considere distinto de cualquier valor. Para eso existe este par de operadores, que nunca devuelven UNKNOWN.

Expresión = / <> IS [NOT] DISTINCT FROM
5 = 5 / 5 IS NOT DISTINCT FROM 5 TRUE TRUE
5 = 4 / 5 IS NOT DISTINCT FROM 4 FALSE FALSE
5 = NULL / 5 IS NOT DISTINCT FROM NULL UNKNOWN FALSE
NULL = NULL / NULL IS NOT DISTINCT FROM NULL UNKNOWN TRUE

Con datos reales, la diferencia salta a la vista. "Todos los pedidos que no gestionó Óscar Peris (empleado 4)":

-- ⚠️ INCORRECTA para la pregunta: pierde los pedidos web
SELECT id, cliente_id, empleado_id, estado
FROM pedidos
WHERE empleado_id <> 4
ORDER BY id;
id cliente_id empleado_id estado
4 4 5 entregado
8 7 5 entregado
12 10 5 entregado
14 12 6 entregado
18 5 5 pagado
20 9 6 pendiente

6 filas. Es el resultado que te sorprendió en 02-03.

-- ✅ CORRECTA: un pedido sin comercial tampoco lo gestionó Óscar
SELECT id, cliente_id, empleado_id, estado
FROM pedidos
WHERE empleado_id IS DISTINCT FROM 4
ORDER BY id;

16 filas: las 6 anteriores más los 10 pedidos web. Ahora sí, 4 + 16 = 20.

La forma larga equivalente sería WHERE empleado_id <> 4 OR empleado_id IS NULL, que es lo que escribiste a mano en la solución 3 de 02-03. IS DISTINCT FROM dice lo mismo en cuatro palabras y sin riesgo de olvidar el segundo término.

Su uso más valioso, sin embargo, aparece al comparar dos columnas que pueden ser nulas las dos:

-- ¿Han cambiado los datos entre dos versiones de una fila?
WHERE nuevo.coste IS DISTINCT FROM viejo.coste

Con <>, una fila cuyo coste pasa de NULL a 12.00 no se detectaría como cambio (la comparación daría UNKNOWN). Con IS DISTINCT FROM, sí. Es el operador de referencia para detección de cambios, comparación de versiones y procesos de sincronización.

Nota de dialecto: IS DISTINCT FROM es SQL estándar y existe en PostgreSQL, SQLite y Oracle 23ai. MySQL usa el operador <=> ("null-safe equal"), que equivale a IS NOT DISTINCT FROM. SQL Server no tenía nada hasta 2022, que introdujo IS [NOT] DISTINCT FROM; en versiones anteriores hay que escribir la forma larga con OR ... IS NULL.

  1. Dónde NULL sí se agrupa con NULL

Aquí llega la parte que descoloca a mucha gente: el estándar SQL no es coherente consigo mismo. Acabas de aprender que NULL = NULL es UNKNOWN, y sin embargo:

SELECT DISTINCT empleado_id
FROM pedidos
ORDER BY empleado_id;
empleado_id
4
5
6
(null)

4 filas. Diez pedidos tienen empleado_id nulo y DISTINCT los ha colapsado en una sola fila. Es decir: para DISTINCT, esos diez nulos sí son iguales entre sí.

Lo mismo ocurre al agrupar:

SELECT empleado_id,
       COUNT(*) AS pedidos
FROM pedidos
GROUP BY empleado_id
ORDER BY empleado_id;
empleado_id pedidos
4 4
5 4
6 2
(null) 10

4 grupos, y los NULL forman uno propio con 10 filas. Esto es enormemente útil —de hecho es lo que permite contar el canal web de un vistazo— y lo desarrollará la lección 04-05.

Y al ordenar, los nulos también se agrupan entre sí, aunque su posición sea configurable (02-05):

SELECT id, empleado_id, estado
FROM pedidos
ORDER BY empleado_id NULLS FIRST, id
LIMIT 12;
id empleado_id estado
1 (null) entregado
3 (null) entregado
5 (null) entregado
7 (null) entregado
9 (null) entregado
11 (null) entregado
13 (null) entregado
15 (null) entregado
17 (null) enviado
19 (null) pagado
2 4 entregado
6 4 entregado

(12 primeras de 20 filas.)

La tabla que conviene tener a mano

Contexto ¿Dos NULL se consideran iguales? Consecuencia práctica
WHERE, ON, HAVING con = No La fila se descarta
IS NOT DISTINCT FROM Comparación segura ante nulos
DISTINCT / DISTINCT ON Todos los nulos colapsan en uno
GROUP BY Los nulos forman un grupo
ORDER BY Van todos juntos (NULLS FIRST/LAST)
UNION, INTERSECT, EXCEPT Deduplican los nulos como cualquier valor
Restricción UNIQUE No (por defecto) Se admiten varios nulos en la columna
Funciones de agregación Se ignoran AVG promedia solo los no nulos (04-04)
COUNT(*) Se cuentan Cuenta filas, no valores

La justificación oficial del estándar es que WHERE responde a preguntas sobre hechos (y de un desconocido no puedes afirmar nada), mientras que GROUP BY y DISTINCT responden a preguntas sobre agrupación de filas (y ahí "sin valor" es una categoría tan válida como cualquier otra). Es una explicación razonable, pero no cambia el hecho de que el mismo símbolo se comporte de dos formas. Apréndete la tabla; no intentes deducirla.

  1. NULL y la restricción UNIQUE

Consecuencia directa de la fila penúltima de la tabla anterior: en PostgreSQL, una columna con restricción UNIQUE admite tantos NULL como quieras, porque dos nulos no se consideran duplicados.

-- Si productos tuviera una columna 'codigo_ean' UNIQUE que admitiera nulos:
-- estas dos filas convivirían sin problema
INSERT INTO productos (nombre, codigo_ean, ...) VALUES ('Producto A', NULL, ...);
INSERT INTO productos (nombre, codigo_ean, ...) VALUES ('Producto B', NULL, ...);

Esto sorprende, y a veces es justo lo que quieres ("el EAN es único, pero no todos los productos lo tienen todavía") y a veces no ("solo puede haber un cliente sin verificar"). PostgreSQL 15 añadió la forma de exigir lo contrario:

-- PostgreSQL 15+: los NULL cuentan como duplicados entre sí
ALTER TABLE productos
  ADD CONSTRAINT productos_ean_uk UNIQUE NULLS NOT DISTINCT (codigo_ean);

En TiendaVerde las dos columnas UNIQUEcategorias.nombre y clientes.email— son además NOT NULL, así que la cuestión no llega a plantearse. Pero es un detalle de diseño que hay que decidir conscientemente, no descubrir en producción.

Nota de dialecto: este es uno de los puntos donde los motores más divergen. PostgreSQL, MySQL, SQLite y Oracle admiten varios nulos en una columna UNIQUE; SQL Server admite solo uno (trata todos los nulos como el mismo valor a efectos del índice único). Una tabla que funciona en PostgreSQL puede fallar al migrarla a SQL Server por este motivo exacto. Las restricciones se estudian en el módulo 5.

Y recuerda que una clave primaria no tiene este debate: PRIMARY KEY implica NOT NULL, siempre y en todos los motores.

  1. NULL en concatenaciones y en aritmética

La regla es de una simplicidad brutal: casi cualquier operación con NULL devuelve NULL. Se dice que el nulo se propaga.

SELECT 100 + NULL      AS suma,
       100 * NULL      AS producto,
       'Hola' || NULL  AS concatenacion,
       UPPER(NULL)     AS mayusculas;
suma producto concatenacion mayusculas
(null) (null) (null) (null)

Lo viste en 02-02 con la concatenación, y en 03-03 con los LEFT JOIN. Aquí está el porqué: si no sabes cuál es el segundo sumando, no puedes saber cuál es la suma.

Sobre datos reales, el efecto en un LEFT JOIN:

SELECT c.id,
       c.nombre || ' ' || c.apellidos AS cliente,
       pe.id           AS pedido_id,
       pe.gastos_envio,
       pe.gastos_envio * 2 AS envio_doble
FROM clientes AS c
LEFT JOIN pedidos AS pe ON pe.cliente_id = c.id
WHERE c.id IN (12, 13, 14, 15)
ORDER BY c.id, pe.id;
id cliente pedido_id gastos_envio envio_doble
12 Diego Ramos Herrera 14 4.95 9.90
13 Núria Bosch Ferrer (null) (null) (null)
14 Hugo Iglesias Pardo (null) (null) (null)
15 Inés Carrasco Vega (null) (null) (null)

envio_doble vale NULL, no 0.00, para los tres clientes sin pedidos. En un informe eso puede aparecer como una celda vacía, y quien la lea puede interpretarla como un cero. No lo es: es "no aplica".

Las excepciones

No todo propaga el nulo. Conviene conocer las excepciones porque son precisamente las herramientas para tratarlo:

Construcción Con NULL Comentario
IS NULL / IS NOT NULL Devuelve booleano El predicado de esta lección
IS DISTINCT FROM Devuelve booleano Sección 6
COALESCE(a, b, c) Devuelve el primer no nulo 06-04
NULLIF(a, b) Convierte un valor en NULL 06-04
CASE WHEN ... IS NULL THEN ... Permite decidir 06-05
CONCAT('a', NULL, 'b') Ignora los nulos → 'ab' La alternativa a ||, en 06-01
Funciones de agregación Ignoran los nulos 04-04, la lección siguiente
COUNT(*) Cuenta la fila igualmente 04-04

Las tres primeras filas ya las manejas. Las de COALESCE, NULLIF y CASE —las que sustituyen un nulo por un valor presentable— son el contenido de 06-04 y 06-05; ahí escribirás COALESCE(pe.gastos_envio, 0) para que la celda del informe muestre 0.00. Y la de los agregados es la primera cosa que verás mañana.

  1. La resolución de los casos pendientes

Vamos a cerrar, uno a uno, todos los cabos sueltos del curso. Los cinco tienen la misma explicación.

Caso 1: WHERE empleado_id = NULL devolvió 0 filas (02-03)

empleado_id = NULL se evalúa a UNKNOWN para las veinte filas: para las diez con valor porque no puedes comparar un número con lo desconocido, y para las diez nulas porque NULL = NULL tampoco es cierto. El WHERE descarta todo lo que no sea TRUE. Cero filas, y no podría ser de otro modo. Lo correcto es IS NULL.

Caso 2: WHERE empleado_id <> 4 devolvió 6 y no 16 (02-03)

Los diez pedidos web evalúan NULL <> 4UNKNOWN → descartados. Quedan los 10 con comercial, menos los 4 de Óscar: 6 filas. Si la pregunta era "los que no gestionó Óscar", la respuesta correcta tiene 16 filas y se escribe WHERE empleado_id IS DISTINCT FROM 4, como en la sección 6.

Caso 3: el INNER JOIN con empleados perdió 10 pedidos (03-02)

La condición ON pe.empleado_id = e.id es una comparación como cualquier otra. Para los pedidos web da UNKNOWN, ninguna fila de empleados la satisface, y el INNER JOIN los descarta. El ON sigue la misma regla que el WHERE.

Caso 4: una condición en el WHERE degradó un LEFT JOIN a INNER (03-03)

El LEFT JOIN fabrica una fila para Núria con todas las columnas de pedidos a NULL. Después, WHERE pe.estado = 'entregado' evalúa NULL = 'entregado'UNKNOWN → descartada. Por eso 18 filas se convertían en 14 y 15 clientes en 12. La condición debe ir en el ON, que actúa antes de fabricar esos nulos.

Caso 5: NOT IN (4, 5, NULL) devolvió 0 filas (04-02)

Se expande a ... AND empleado_id <> NULL, y ese factor es UNKNOWN para toda fila. TRUE AND UNKNOWN = UNKNOWN (tabla de verdad de la sección 4). Cero filas garantizadas, para cualquier tabla y cualquier dato.

El patrón común

flowchart LR
    A["Un NULL entra<br/>en una comparación"] --> B["El resultado es UNKNOWN"]
    B --> C["WHERE / ON / HAVING<br/>solo dejan pasar TRUE"]
    C --> D["La fila desaparece<br/>sin error y sin aviso"]
    D --> E["🔍 Síntoma: el filtro y su<br/>negación no suman el total"]

Los cinco casos son el mismo caso. Una vez que lo ves así, dejan de ser cinco reglas que memorizar y pasan a ser una que aplicar.

  1. Diseñar con NULL, con centinelas o con NOT NULL

Cuando te toque diseñar una tabla (módulo 5) tendrás que decidir, columna a columna, si admite nulos. Hay tres estrategias y ninguna es universalmente correcta.

Estrategia Ejemplo A favor En contra
Permitir NULL pedidos.empleado_id Modela con honestidad "no hay valor". No inventa datos. Los agregados lo ignoran solos Obliga a manejar la lógica de tres valores en cada consulta
Valor centinela Un empleado ficticio con id = 0 llamado "Web" Las consultas se simplifican: JOIN normales, sin LEFT, sin IS NULL El centinela es un dato falso: aparece en los recuentos, en los DISTINCT y en los informes. Hay que acordarse de excluirlo siempre
NOT NULL con DEFAULT pedidos.gastos_envio NOT NULL DEFAULT 0 La columna nunca sorprende. Sumar es sumar Solo vale cuando existe un valor por defecto verdadero. 0.00 de portes es un hecho; un coste a 0.00 sería mentira

Criterios para elegir:

  1. ¿Existe un valor por defecto que sea cierto? Si sí, NOT NULL DEFAULT. Los portes gratuitos son 0,00 € de verdad, no una ausencia.
  2. ¿La ausencia significa algo para el negocio? Si sí, NULL y documéntalo. "Pedido web" es una información valiosa, no un hueco.
  3. ¿Estás tentado de usar -1, 0, 'N/A' o '9999-12-31'? Cuidado: eso es un centinela disfrazado. AVG lo promediará, MIN lo devolverá como mínimo y COUNT lo contará. TiendaVerde sería un desastre si el canal web se hubiera codificado como empleado_id = 0: cualquier "media de pedidos por comercial" quedaría envenenada.

La postura sensata: los NULL son parte del modelo relacional y esconderlos con centinelas no elimina el problema, solo lo hace invisible. Usa NOT NULL siempre que puedas justificar un valor real por defecto; usa NULL cuando la ausencia sea legítima; y no uses centinelas salvo que tengas una razón muy concreta y la dejes escrita en la documentación del esquema.

Errores Comunes y Consejos

  • Escribir = NULL o <> NULL. Cero filas siempre. Es IS NULL / IS NOT NULL.
  • Creer que NULL es cero o cadena vacía. NULL + 5 es NULL; 0 + 5 es 5. NULL || 'a' es NULL; '' || 'a' es 'a'.
  • Olvidar que <>, NOT IN, NOT LIKE y NOT BETWEEN descartan las filas nulas. Comprueba siempre que la condición y su negación sumen el total.
  • Usar NOT IN con una lista que puede traer un NULL. Cero filas, garantizadas (04-02).
  • Poner una condición sobre la tabla derecha en el WHERE de un LEFT JOIN. Los nulos fabricados no la superan y el LEFT se degrada a INNER (03-03).
  • Suponer que si A es NULL entonces NOT A es verdad. NOT UNKNOWN es UNKNOWN.
  • Interpretar una celda vacía de un informe como un cero. Puede ser "no aplica". COALESCE (06-04) lo hace explícito.
  • Esperar que UNIQUE impida varias filas nulas. No lo hace, salvo NULLS NOT DISTINCT en PostgreSQL 15+ o en SQL Server, que va al revés.
  • Confiar en el orden de evaluación de un AND para protegerse de un nulo o de una división por cero. El planificador reordena. Usa CASE o NULLIF (módulo 6).
  • Consejo: al leer un esquema, mira primero qué columnas admiten NULL. \d tabla en psql lo dice. Son exactamente los sitios donde tus consultas pueden perder filas.
  • Consejo: cuando una consulta devuelva menos filas de las esperadas, sospecha de los nulos antes que de los datos. Es la causa más frecuente y la más silenciosa.
  • Consejo: usa IS DISTINCT FROM por defecto al comparar dos columnas nullables. Te ahorra el OR ... IS NULL y hace la intención explícita.

Ejercicios

Ejercicio 1

Sin ejecutar nada, predice el resultado (true, false o *(null)*) de cada expresión. Después ejecútalas todas en una sola consulta y comprueba.

SELECT NULL = NULL                    AS a,
       NULL IS NULL                   AS b,
       NULL <> NULL                   AS c,
       NOT (NULL = NULL)              AS d,
       (1 = 1) OR (NULL = 1)          AS e,
       (1 = 1) AND (NULL = 1)         AS f,
       (1 = 2) AND (NULL = 1)         AS g,
       NULL IS NOT DISTINCT FROM NULL AS h,
       5 IS DISTINCT FROM NULL        AS i;

Explica en una frase cada resultado que te haya sorprendido.

Ejercicio 2

El departamento de marketing quiere medir el programa de referidos. Sobre clientes:

  1. ¿Cuántos clientes llegaron por recomendación y cuántos por su cuenta? Escribe dos consultas y comprueba que suman 15.
  2. Escribe la consulta que devuelve, para cada cliente, su nombre completo y el nombre completo de quien lo recomendó, incluyendo los que no fueron recomendados por nadie. (Pista: relación reflexiva, 03-06.)
  3. En esa consulta, ¿por qué la columna del recomendador aparece como *(null)* y no como una cadena vacía? Da las dos razones.

Ejercicio 3

Un compañero ha escrito este control de calidad y afirma que "en TiendaVerde no hay ningún pedido raro":

-- ⚠️ INCORRECTA
SELECT COUNT(*) AS pedidos_sin_comercial_valido
FROM pedidos
WHERE empleado_id <> 4
  AND empleado_id <> 5
  AND empleado_id <> 6;
pedidos_sin_comercial_valido
0
  1. ¿Qué está midiendo realmente esa consulta y por qué el 0 no demuestra lo que él cree?
  2. Escribe la versión que cuenta de verdad los pedidos cuyo empleado_id no es ni 4, ni 5, ni 6 (contando los nulos como "no es ninguno de los tres"). ¿Cuántos son?
  3. Escribe la versión que cuenta los pedidos con un empleado_id presente pero distinto de esos tres. ¿Cuántos son y qué significa ese número?

Soluciones

Solución 1

Expresión Resultado Razón
a: NULL = NULL (null) Dos ausencias no son comparables
b: NULL IS NULL true IS NULL no es una comparación: es una pregunta sobre el estado
c: NULL <> NULL (null) Tampoco puedes afirmar que sean distintas
d: NOT (NULL = NULL) (null) NOT UNKNOWN = UNKNOWN
e: (1=1) OR (NULL=1) true TRUE OR UNKNOWN = TRUE. Una alternativa cierta basta
f: (1=1) AND (NULL=1) (null) TRUE AND UNKNOWN = UNKNOWN. Es el caso de NOT IN
g: (1=2) AND (NULL=1) false FALSE AND UNKNOWN = FALSE. Ya está decidido
h: NULL IS NOT DISTINCT FROM NULL true Este operador trata los nulos como iguales
i: 5 IS DISTINCT FROM NULL true Y trata un valor como distinto de un nulo

Las tres que suelen sorprender son d, f y g. d porque uno espera que negar algo lo convierta en verdadero; f y g porque parece contradictorio que AND con un UNKNOWN a veces dé UNKNOWN y a veces FALSE — y sin embargo es lo lógico: si un factor ya es falso, no hace falta saber el otro.

Solución 2

1. Las dos mitades:

SELECT COUNT(*) AS clientes_referidos
FROM clientes
WHERE referido_por_id IS NOT NULL;
clientes_referidos
8
SELECT COUNT(*) AS clientes_espontaneos
FROM clientes
WHERE referido_por_id IS NULL;
clientes_espontaneos
7

8 + 7 = 15. Cierra, porque IS NULL e IS NOT NULL sí particionan la tabla. Si hubieras escrito WHERE referido_por_id <> 0 para "los referidos", habrías obtenido 8 igualmente por casualidad, y WHERE referido_por_id = 0 habría dado 0 en lugar de 7.

2. Autounión con LEFT JOIN:

SELECT c.id,
       c.nombre || ' ' || c.apellidos AS cliente,
       ref.nombre || ' ' || ref.apellidos AS recomendado_por
FROM clientes AS c
LEFT JOIN clientes AS ref ON c.referido_por_id = ref.id
ORDER BY c.id;
id cliente recomendado_por
1 Lucía Martínez Soler (null)
2 Carlos Ferrer Ibáñez Lucía Martínez Soler
3 Marta Sanchis Gil Lucía Martínez Soler
4 Javier Ortega Ruiz (null)
5 Ana Belmonte Roca Carlos Ferrer Ibáñez
6 Pau Llorens Vidal (null)
7 Sofia Moreira Costa (null)
8 Tiago Almeida Nunes Sofia Moreira Costa
9 Camille Dubois (null)
10 Julien Moreau Camille Dubois
11 Elena Navarro Puig Pau Llorens Vidal
12 Diego Ramos Herrera (null)
13 Núria Bosch Ferrer Ana Belmonte Roca
14 Hugo Iglesias Pardo (null)
15 Inés Carrasco Vega Lucía Martínez Soler

15 filas, los 15 clientes. Con INNER JOIN serían 8 y perderías a los espontáneos. Fíjate en el detalle: Lucía ha recomendado a tres clientes (2, 3 y 15), lo que la convierte en la mejor prescriptora de TiendaVerde.

3. Las dos razones por las que recomendado_por sale *(null)*:

  1. El LEFT JOIN no encontró pareja. Para los siete clientes con referido_por_id nulo, la condición c.referido_por_id = ref.id da UNKNOWN y no casa con ninguna fila, así que el motor rellena todas las columnas de ref con NULL (03-03, sección 3).
  2. La concatenación propaga el nulo. Aunque solo una de las dos columnas fuera nula, ref.nombre || ' ' || ref.apellidos daría NULL igualmente, porque || con un operando nulo devuelve nulo (02-02 y sección 9 de esta lección).

Que sea *(null)* y no '' es importante: la cadena vacía significaría "se llama así, con cero caracteres". El nulo significa "no hay nadie ahí". Para que el informe muestre algo legible —"Sin recomendador", por ejemplo— hace falta COALESCE, en 06-04.

Solución 3

1. Qué mide realmente. Los tres factores están unidos por AND, y para los diez pedidos web cada uno vale UNKNOWN. UNKNOWN AND UNKNOWN AND UNKNOWN es UNKNOWN, así que esas diez filas se descartan. Para los otros diez, cada pedido tiene un empleado_id que es 4, 5 o 6, de modo que uno de los tres factores es FALSE y la conjunción es FALSE.

Resultado: cero filas. Y ese 0 no demuestra lo que él cree, porque la consulta es incapaz de ver la mitad de la tabla: los diez pedidos web nunca llegan a evaluarse como candidatos. Si el criterio de "pedido raro" incluye "sin comercial asignado", esta consulta jamás lo detectaría, hoy ni nunca. Un control de calidad que estructuralmente no puede dar positivo no es un control de calidad.

2. Contando los nulos como "no es ninguno de los tres":

-- ✅ CORRECTA
SELECT COUNT(*) AS pedidos_sin_comercial_conocido
FROM pedidos
WHERE empleado_id IS DISTINCT FROM 4
  AND empleado_id IS DISTINCT FROM 5
  AND empleado_id IS DISTINCT FROM 6;
pedidos_sin_comercial_conocido
10

10 pedidos: exactamente los diez del canal web. IS DISTINCT FROM nunca devuelve UNKNOWN, así que la conjunción se resuelve limpiamente. La forma equivalente y más idiomática para este caso concreto sería:

SELECT COUNT(*) AS pedidos_sin_comercial_conocido
FROM pedidos
WHERE empleado_id IS NULL
   OR empleado_id NOT IN (4, 5, 6);

que devuelve el mismo 10.

3. Solo los que tienen comercial y no es ninguno de esos tres:

SELECT COUNT(*) AS pedidos_comercial_desconocido
FROM pedidos
WHERE empleado_id IS NOT NULL
  AND empleado_id NOT IN (4, 5, 6);
pedidos_comercial_desconocido
0

0 pedidos, y este cero sí significa algo: no hay ningún pedido asignado a un empleado que no sea Óscar, Laia o Marc. Es decir, los otros cinco empleados —Rosa, Andrés, Beatriz, Irene y Daniel— no gestionan pedidos, tal como describía 01-06.

La comprobación final: 10 (con comercial 4, 5 o 6) + 10 (sin comercial) + 0 (con otro comercial) = 20. Ahora la lógica cierra, y ese es el criterio para saber que la consulta está bien escrita.

Conclusión

La deuda queda saldada, y con una sola idea:

  • NULL es la ausencia de valor, no cero, ni cadena vacía, ni falso, ni igual a otro NULL. Significa desconocido.
  • En TiendaVerde hay tres NULL con tres significados de negocio distintos —canal web, cliente espontáneo, cúspide del organigrama— más los que fabrica el motor en cada LEFT JOIN.
  • SQL usa lógica de tres valores: TRUE, FALSE y UNKNOWN. WHERE, ON y HAVING conservan solo lo que es TRUE, así que UNKNOWN se descarta igual que FALSE — pero al negarlo se comportan distinto, porque NOT UNKNOWN sigue siendo UNKNOWN.
  • Las dos celdas que hay que memorizar son TRUE OR UNKNOWN = TRUE y TRUE AND UNKNOWN = UNKNOWN. De ellas sale toda la asimetría entre IN y NOT IN.
  • IS NULL / IS NOT NULL son predicados, no comparaciones, y sí parten la tabla en dos mitades exactas. IS DISTINCT FROM compara tratando el nulo como un valor más, y es la herramienta correcta para comparar columnas nullables.
  • El estándar es incoherente a propósito: NULL = NULL es desconocido, pero en DISTINCT, GROUP BY, ORDER BY y en las operaciones de conjuntos los nulos sí se consideran iguales entre sí. En cambio, una restricción UNIQUE los considera distintos y admite varios.
  • El nulo se propaga por la aritmética y la concatenación: precio * NULL es NULL, no cero. COALESCE, NULLIF y CASE (06-04 y 06-05) son las herramientas para sustituirlo al presentar.
  • Los cinco casos pendientes del curso —= NULL, <> 4, el INNER JOIN que perdía pedidos, el WHERE que degradaba el LEFT JOIN y el NOT IN con nulo— son el mismo caso: una comparación que da UNKNOWN y una fila que desaparece sin aviso.
  • Al diseñar, elige conscientemente entre NULL, valor centinela y NOT NULL DEFAULT. Los centinelas no eliminan el problema: lo esconden dentro de las medias y los recuentos.

Con esto termina la primera mitad del módulo. Ya sabes filtrar con precisión: por patrones, por listas, por rangos y por ausencia de valor. En la lección siguiente, funciones de agregación, empieza la segunda mitad y cambia la naturaleza de lo que haces: hasta ahora cada fila del resultado venía de una fila de la tabla; a partir de ahora muchas filas entrarán y saldrá un solo valor. Verás COUNT, SUM, AVG, MIN y MAX, y la primera cosa que aprenderás de ellas es que todas ignoran los NULL —todas menos COUNT(*)—, lo que convierte a pedidos.empleado_id en el mejor banco de pruebas posible: 20, 10 y 3 según cómo cuentes. Y resolveremos, por fin, el aviso repetido tres veces en el módulo 3 sobre sumar gastos de envío después de unir con el detalle.

Curso de SQL

Módulo 1: Introducción a SQL

Módulo 2: Consultas básicas de SQL

Módulo 3: Trabajando con múltiples tablas

Módulo 4: Filtrado avanzado de datos

Módulo 5: Manipulación de datos

Módulo 6: Funciones avanzadas de SQL

Módulo 7: Subconsultas y consultas anidadas

Módulo 8: Índices y optimización de rendimiento

Módulo 9: Transacciones y concurrencia

Módulo 10: Temas avanzados

Módulo 11: SQL en la práctica

Módulo 12: Proyecto final

© Copyright 2026. Todos los derechos reservados