PostgreSQL
NULL y lógica de tres valores
Explica NULL como ausencia de valor y la lógica de tres valores que afecta comparaciones, filtros, constraints, agregaciones y joins en SQL.
- Última actualización
- Actualizada
- Nivel
- Fundamentos
PostgreSQL
Explica NULL como ausencia de valor y la lógica de tres valores que afecta comparaciones, filtros, constraints, agregaciones y joins en SQL.
NULL no representa un valor especial comparable como cero o string vacío. Representa ausencia o desconocimiento, y por eso introduce una tercera posibilidad lógica: UNKNOWN.
En SQL, una expresión booleana puede producir:
TRUE
FALSE
UNKNOWNWHERE conserva únicamente filas cuya condición es TRUE. Tanto FALSE como UNKNOWN quedan fuera.
Esto explica por qué:
WHERE phone = NULLno encuentra teléfonos ausentes. La comparación no produce TRUE; produce UNKNOWN.
La forma correcta es:
WHERE phone IS NULLLos sistemas necesitan representar datos que:
Sin NULL, los equipos suelen inventar sentinels:
0
''
'unknown'
'1970-01-01'
-1Esos valores mezclan ausencia con datos reales y obligan a recordar convenciones ocultas.
NULL ofrece una representación estándar, pero exige diseñar bien su significado y comprender su impacto en consultas.
valor conocido
→ puede compararse normalmente
NULL
→ la comparación no puede afirmarse ni negarse
→ resultado UNKNOWNEjemplo:
SELECT NULL = NULL;No devuelve TRUE, porque no se sabe si dos valores desconocidos son iguales.
TRUE AND UNKNOWN → UNKNOWN
FALSE AND UNKNOWN → FALSELa segunda expresión es falsa porque, aunque el valor desconocido fuera true, FALSE AND TRUE seguiría siendo false.
TRUE OR UNKNOWN → TRUE
FALSE OR UNKNOWN → UNKNOWNNOT UNKNOWN → UNKNOWNEste modelo es esencial para filtros combinados.
CREATE TABLE customers (
id bigint PRIMARY KEY,
email text NOT NULL,
phone text
);Consultas:
SELECT * FROM customers WHERE phone IS NULL;
SELECT * FROM customers WHERE phone IS NOT NULL;phone puede faltar. email no, porque el modelo exige NOT NULL.
SELECT old_value IS DISTINCT FROM new_value;IS DISTINCT FROM trata NULL como comparable:
NULL IS DISTINCT FROM NULL → FALSE
NULL IS DISTINCT FROM 10 → TRUE
10 IS DISTINCT FROM 10 → FALSESu opuesto:
IS NOT DISTINCT FROMEs útil en:
SELECT COALESCE(discount_amount, 0)
FROM orders;Devuelve el primer argumento no NULL.
Esto es correcto solo si “sin descuento” equivale a cero. Si NULL significa “aún no calculado”, convertirlo en cero oculta un estado distinto.
Pregunta de diseño:
NULL significa ausencia real
o
valor pendiente/desconocidoEl fallback debe respetar esa semántica.
SELECT revenue / NULLIF(order_count, 0);Si order_count es cero, NULLIF devuelve NULL y evita división por cero.
Esto no siempre es la mejor respuesta. Puede ser preferible devolver error, cero o excluir la fila según el dominio.
Muchas operaciones con NULL producen NULL:
quantity * unit_price
first_name || ' ' || last_nameSi cualquier parte es NULL, el resultado puede ser NULL.
Para strings:
concat_ws(' ', first_name, middle_name, last_name)puede omitir valores NULL, pero debes confirmar que esa semántica es correcta.
Cuenta filas.
Cuenta valores no NULL.
Ignoran NULL.
Ejemplo:
SELECT
count(*) AS customers,
count(phone) AS customers_with_phone
FROM customers;Si no existen filas o todos los valores son NULL, sum y avg pueden devolver NULL:
SELECT COALESCE(sum(total_amount), 0)
FROM orders
WHERE status = 'paid';Aquí cero puede ser un resultado agregado razonable porque la suma de un conjunto vacío se quiere presentar como cero en el contrato de la aplicación, aunque SQL devuelva NULL.
Por defecto, un unique constraint permite múltiples NULL:
CREATE TABLE users (
external_code text UNIQUE
);Varias filas pueden tener external_code = NULL.
Si el requisito es “solo una fila puede tener este valor, incluso NULL”:
UNIQUE NULLS NOT DISTINCT (external_code)Esta sintaxis depende de versiones modernas de PostgreSQL. Para versiones anteriores pueden utilizarse índices de expresión o parciales según el caso.
Una constraint solo falla cuando la expresión es FALSE; TRUE y UNKNOWN pasan.
Ejemplo problemático:
CHECK (price > 0)Si price es NULL, la expresión es UNKNOWN y la fila se acepta.
Para obligar valor positivo:
price numeric NOT NULL CHECK (price > 0)CHECK y NOT NULL cumplen responsabilidades diferentes.
Solo conserva coincidencias donde la condición es TRUE.
Conserva filas del lado izquierdo y rellena con NULL cuando no existe coincidencia.
Ejemplo:
SELECT c.id, o.id AS order_id
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id;Una fila con order_id IS NULL puede significar que el customer no tiene orders.
Filtro correcto:
WHERE o.id IS NULLPero mover una condición del ON al WHERE puede convertir de facto el LEFT JOIN en INNER JOIN:
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.status = 'paid'Las filas sin order tienen o.status = NULL, la condición resulta UNKNOWN y se eliminan.
WHERE product_id NOT IN (
SELECT product_id FROM blocked_products
)Si la subquery devuelve NULL, el resultado puede volverse UNKNOWN para todas las filas no coincidentes.
Alternativa más segura:
WHERE NOT EXISTS (
SELECT 1
FROM blocked_products b
WHERE b.product_id = products.id
)NOT EXISTS expresa directamente ausencia de coincidencia.
ORDER BY delivered_at NULLS LAST;Sin indicarlo, PostgreSQL tiene defaults distintos según ASC/DESC. Si el orden forma parte de una API o paginación, especifica NULLS FIRST o NULLS LAST.
Los B-tree incluyen entradas NULL y pueden soportar:
WHERE cancelled_at IS NULLUn índice parcial puede ser más pequeño:
CREATE INDEX active_orders_idx
ON orders(created_at)
WHERE cancelled_at IS NULL;Es útil cuando la consulta frecuente opera sobre un subconjunto pequeño.
cancelled_at timestamptzNULL significa “la orden no está cancelada”.
payment_card_number NULL
bank_account NULL
cash_received NULLMuchas columnas opcionales pueden indicar subtipos de pago mezclados en una tabla.
A veces una columna NULL oculta un lifecycle:
processed_at NULLPuede ser suficiente, o puede necesitar status = pending|processing|completed|failed para distinguir estados.
Existen diferencias entre:
{}{"field": null}y una columna SQL NULL.
Las consultas y operadores deben distinguirlos.
Requisito: listar órdenes y mostrar fecha de entrega solo cuando exista.
SELECT
id,
status,
delivered_at,
CASE
WHEN delivered_at IS NULL THEN 'not_delivered'
ELSE 'delivered'
END AS delivery_state
FROM orders;Flujo:
IS NULL clasifica ausencia.CASE produce una representación para lectura.La unicidad puede permitir combinaciones aparentemente repetidas si alguna columna es NULL. Revisa NULLS NOT DISTINCT o índices parciales.
Un array puede ser NULL, vacío o contener elementos NULL. Son estados diferentes.
is_active boolean nullable tiene tres estados. Si no existe un tercero real, añade NOT NULL.
Una query como:
WHERE status = COALESCE($1, status)puede comportarse mal con NULL e impedir índices. Construye filtros explícitos o usa:
WHERE ($1 IS NULL OR status = $1)con análisis de plan.
Resultado UNKNOWN. Usa IS NULL.
Pierde significado y oculta datos faltantes.
Por defecto permite varios.
UNKNOWN pasa; combina con NOT NULL.
Puede devolver cero filas inesperadamente.
Elimina filas sin coincidencia.
Prueba siempre:
Una tabla pequeña de ejemplos suele revelar errores antes de producción.
IS NULL y IS DISTINCT FROM son operadores específicos.price > 0 no impide price NULL?Constraints e integridad de datos convierte reglas del dominio en estados que PostgreSQL puede impedir incluso bajo concurrencia.