Blog>

Óscar Pastor
05 Jul 2024

Cómo sobrevivir a los NULL en SQL

Tiempo de lectura 11 minutos

Sumario Antes de empezar a escribir este artículo, realicé un pequeño experimento entre mis compañeros de trabajo. Escogí a seis de ellos, les planteé una sencilla pregunta y les pedí que respondieran sin dar demasiadas vueltas (y, por su puesto, sin hacer trampas):

Query

SELECT SUM(u.creditos) FROM USUARIO u WHERE u.nombre LIKE ‘Alberto%’

Les pregunté, "¿qué devuelve cuando ningún usuario se llama Alberto?"

Contestaciones

Cuatro contestaron que una fila con valor CERO, uno dijo que ninguna fila y solo una compañera dio la respuesta correcta: Una fila con valor NULL.

Todos ellos son excelentes profesionales, con años de experiencia. Les pedí expresamente que diesen una respuesta rápida porque sabía que si les dejaba pensar acertarían. El objetivo no era hacer un examen sino verificar hasta qué punto las sentencias SQL pueden devolver valores poco (o nada) intuitivos.

Si meto a dos personas en una sala, una con dos monedas y otra con tres, y pregunto cuánto suman las monedas que tienen las personas que hay dentro de la sala, la respuesta es cinco. Si dejo la sala vacía y repito la pregunta muchas personas dirán cero, es lo que nos dice la intuición. Pero la respuesta correcta (al menos en términos algebraicos) es que no es posible determinarlo.

La realidad es que no tiene sentido calcular un agregado de un atributo de los elementos de un conjunto vacío, no tiene sentido sumar las monedas de “Nadie”

Si en lugar de preguntarnos por las monedas, nos preguntamos por la suma de las edades de las personas que hay en una sala vacía, la respuesta se nos hace más obvia “¿de qué personas voy a sumar sus edades si aquí no hay nadie!” Si nos preguntamos por otros agregados como la edad media, la edad máxima o la edad mínima nos damos cuenta de lo absurdo de la situación.

¿Qué quiere decir NULL exactamente?

Antes de seguir exponiendo ejemplos que nos dan resultados anti intuitivos, intentemos entender que significa NULL en SQL

En muchas ocasiones escucharemos que NULL significa “ausencia de valor” y también con frecuencia vamos a ver NULL utilizado para codificar situaciones especiales (por ejemplo, nota del examen a NULL para indicar no presentado).

Pero la auténtica semántica de NULL es “valor desconocido”. Debemos entender que DESCONOCIDO no es un valor como cualquier otro, desconocido significa exactamente eso, la tupla tiene valor para esa columna, pero lo desconocemos.

En el ejemplo que hemos puesto, todos aquellos que tienen NULL en la nota del examen, en términos de SQL tienen una nota (1, 5, 7, 8… la que sea) pero no la sabemos. Otra cosa es el significado especial que nosotros queramos darle, pero debemos saber que al realizar sentencias SQL, ese NULL se va a interpretar como valor desconocido, con las implicaciones que de ello se deriven.

NULL en los operadores LÓGICOS

Los operadores booleanos (AND, OR, NOT) son ampliamente utilizados en cualquier sentencia SQL, los operandos pueden ser columnas de tipo Boolean (en las bases de datos que lo soporten) o expresiones que den como resultado true/false.

Las tablas de valor en nuestro caso implican tres valores posibles (true, false, NULL). Si Ex1 y Ex2 son expresiones booleanas, las tablas de valor resultarían así:

Si NULL fuese un valor como cualquier otro, en las tablas de valor tendríamos que definir mediante alguna heurística el resultado de aplicar el operador dado cuando algún operando es NULL.

Pero recordemos que NULL no es un valor en sí mismo ni tampoco ausencia de valor. Una expresión que evalúa NULL tiene potencialmente un valor true o false, pero no sabemos cuál.

Si tenemos en cuenta esto, los resultados cobran sentido:

Supongamos que Ex1 es true y Ex2 es NULL (desconocemos su valor).

Ex1 AND Ex2 es NULL porque no podemos conocer su resultado (es desconocido) sin conocer el valor de Ex2. Si Ex2 vale true el resultado será true y si vale false el resultado será false.

Supongamos ahora que Ex1 es false y Ex2 es NULL.

Ex1 AND Ex2 resulta false, aunque EX2 es desconocido el resultado será false dado que tanto si Ex2 es true como si es false el resultado de la expresión va a ser false.

Un análisis análogo podríamos hacer fácilmente para los operadores OR y NOT.

Operadores de Comparación

Veamos ahora algunos resultados sorprendentes relacionados con los valores NULL

Definamos la siguiente tabla (POSTGRESQL) para almacenar los alumnos de un instituto:

Ahora vamos a obtener los alumnos mayores y menores de edad:

Y los resultados obtenidos son:

Tenemos por tanto 4 alumnos en total (2 menos de 18 años y dos con 18 años o más). Vamos a comprobarlo:

¿Cinco alumnos? ¿Cómo es posible esto? Veamos la relación de alumnos:

Cómo podemos ver la edad de Roberto es NULL, es decir, desconocida. Cuando ejecutamos una sentencia SQL, únicamente obtenemos los resultados para los que la cláusula WHERE se cumple de manera inequívoca. En el caso de Roberto tanto edad>=18 como edad<18arroja valor desconocido (NULL): si desconocemos la edad de Roberto tampoco podemos saber si es mayor, igual o menor que 18. Por tanto, ninguna de las dos condiciones es inequívocamente cierta para Roberto.

Tendríamos un caso similar para Almudena si seleccionásemos alumnos atendiendo al número de asignaturas.

Sin embargo, si ejecutamos la siguiente consulta:

Vemos que se han seleccionado tanto Roberto como Almudena. Si nos fijamos en las tablas de valor de los operadores lógicos obtendremos la razón:

edad >= 18 es NULL para Roberto, pero asignaturas > 8 es true

TRUE OR NULL = TRUE

Operadores IS NULL e IS NOT NULL

Ya hemos visto lo que sucede si intentamos comparar un atributo de una tupla que tiene valor NULL con cualquier constante. Algo más confuso puede resultar entender qué sucede si comparamos los valores de las tuplas con el valor NULL constante, pero la respuesta también es obvia si lo pensamos detenidamente.

Si ejecutamos las siguientes sentencias:

El resultado es cero filas en ambos casos. De la misma manera que comparar la edad de Roberto (desconocida) con 18 devuelve NULL (es decir, resultado desconocido) comparar las edades de los alumnos con la constante NULL devuelve NULL. Nos estamos preguntando si las edades son iguales/diferentes a un objeto que nos es desconocido, y por tanto el resultado es desconocido.

Los resultados que queremos los obtenemos con los operadores IS NULL e IS NOT NULL.

En este caso, no estamos preguntando si la edad de los alumnos es igual/diferente a un objeto desconocido sino si la propia edad es algo desconocido/conocido y, por tanto, obtenemos las tuplas en las que la columna edad tiene valor NULL o diferente de NULL respectivamente.

Consejos para manejarse con los NULL y no morir en el intento

Como hemos visto, el manejo de NULL’s puede ser problemático. Aunque entendiendo correctamente la semántica subyacente no deberíamos tener dudas acerca de los resultados que obtenemos cuando hay NULL’s implicados, la realidad es que estos resultados resultan con frecuencia poco intuitivos y puede llevarnos a cometer errores, sobre todo cuando las consultas no son tan simples como las que hemos visto en nuestro ejemplo.

Aquí tienes algunos consejos para mitigar dichos efectos:

1. Si una columna de la tabla es obligatoria, márcala como NOT NULL

Parece de Perogrullo, pero con frecuencia encontramos definiciones de tablas sin los correspondientes NOT NULL en las columnas obligatorias. La justificación para ello suele ser que realmente no importa demasiado si una columna tiene el NOT NULL, porque en la práctica nunca vamos a tener valores NULL en esa columna.

Este tipo de planteamientos tienen, al menos, dos problemas:

  • Si dejamos la puerta abierta a insertar valores NULL donde no deberían permitirse, podría darse el caso de que se inserten dichos NULL de manera inadvertida, provocando errores en el funcionamiento del sistema
  • Podemos causar confusión en el resto del equipo (o en desarrolladores que se incorporan nuevos), que no saben que determinada columna es un “NOT NULL implícito” y por tanto codifican asumiendo que puede haber NULL’s.

2. Cuando un atributo no es obligatorio, plantéate si merece la pena que lo sea

A veces no hay gran diferencia entre que un campo sea obligatorio o no. En estos casos conviene plantearse si el plus de flexibilidad que nos aporta hacer que ese campo pueda ser NULL, compensa los problemas que tanto en término de bugs como de claridad del código puede ocasionarnos.

3. Diseñar adecuadamente el modelo de datos, es un primer paso para garantizar el éxito del desarrollo.

En particular, debemos evitar usar los valores NULL para codificar situaciones excepcionales que en realidad escapan a la auténtica semántica del valor NULL.

Veámoslo con un ejemplo:

Imaginemos que tenemos un sistema donde tenemos almacenados datos de vehículos y de la última ITV que han pasado. En particular querremos consultar datos de vehículos, datos de su última ITV y vehículos que no han pasado la ITV llegada la fecha.

En un primer momento podríamos pensar en una solución de este estilo:


fecha_ultima_itv, centro_itv y id_inspector contienen respectivamente la fecha en la que el vehículo pasó la ITV, dónde la paso y un identificador del mecánico que firmó el informe.

Los tres campos pueden ser NULL, ya que hay coches que no han pasado ninguna ITV. La forma de indicar que un coche nunca ha pasado una ITV es poner esos campos a NULL.

Sin embargo, esto nos puede ocasionar varios problemas:

  • Por un lado, podemos tener incoherencias en esos campos. Dado que no hay restricciones, potencialmente podríamos tener alguno de ellos a NULL y los otros no ¿estamos preparados para soportar esa situación?
  • Por otro lado, para determinar que coches no están al día de su ITV, tendremos una query bastante compleja que involucrará la fecha de matriculación, la fecha de la última ITV y la fecha actual. Deberemos tener en cuenta que, según la fecha de matriculación, un coche pasará la ITV cada año o cada dos años y que la primera se pasa a los cuatro años. ¿Podemos asegurar en una query tan compleja que incluimos siempre los coches que nunca han pasado ITV? Y si lo hacemos ¿cuánto complica la query tener en cuenta esa circunstancia?

Optemos por este otro enfoque:

En esta solución, tenemos una tabla de vehículos que incluye la fecha prevista de su próxima ITV y una tabla con los datos de la última ITV de cada vehículo.

La fecha límite para la siguiente ITV es un dato conocido para todos los vehículos, incluso los recién salidos de fábrica.

Con este enfoque, tenemos una solución mucho más clara y limpia:

  • No hay incoherencias posibles en los datos registrados de la última ITV (todos los campos son NOT NULL)
  • Determinar si un coche está fuera de plazo es trivial: Solo hay que comparar la fecha actual con la fecha_limite_itv. No importa si es la primera ITV.
  • Cada vez que un coche pasa la ITV, simplemente haremos un UPDATE de la tabla VEHICULO para actualizar la fecha límite de la siguiente ITV en función de su fecha de matriculación y un DELETE + INSERT de la tabla ITV.
  • Saber los datos de la última ITV de un coche o si nunca ha pasado una ITV es simple y limpio.

Autor

Óscar Pastor
Óscar Pastor

Desarrollador en dev&del

Capitán en Hello, World!

Máxima especialización en tecnologías Java y J2EE.

Tiene un mixto que se llama Alejandro, canta como los ángeles. Un mixto es un cruce entre canario y jilguero. ¡Ah! Por cierto, el que canta bien es Alejandro.

¿Estás interesado?

Déjanos tus datos y contactaremos contigo lo antes posible